diff --git a/.devcontainer/devcontainer.dockerfile b/.devcontainer/devcontainer.dockerfile new file mode 100644 index 0000000000..23c644a110 --- /dev/null +++ b/.devcontainer/devcontainer.dockerfile @@ -0,0 +1,6 @@ +# https://github.com/dotnet/dotnet-docker/blob/main/README.sdk.md +# https://mcr.microsoft.com/en-us/artifact/mar/dotnet/sdk/tags <-- this shows all images +FROM mcr.microsoft.com/dotnet/sdk:10.0 + +# 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 new file mode 100644 index 0000000000..5e9bdf6374 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "C# (.NET)", + "build": { + // Path is relative to the devcontainer.json file. + "dockerfile": "devcontainer.dockerfile" + }, + "postCreateCommand": "dotnet build", + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csdevkit" + ] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..48584f5d6e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,263 @@ +############################### +# Core EditorConfig Options # +############################### + +# dotnet-format requires version 3.1.37601 +# dotnet tool update -g dotnet-format +# remember to have: git config --global core.autocrlf false #(which is usually default) + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +end_of_line = lf + +# (Please don't specify an indent_size here; that has too many unintended consequences.) +spelling_exclusion_path = SpellingExclusions.dic + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Powershell files +[*.ps1] +indent_size = 2 + +# Shell script files +[*.sh] +end_of_line = lf +indent_size = 2 + +# YAML files +[*.yml] +end_of_line = lf +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] +# Use file-scoped namespace +csharp_style_namespace_declarations = file_scoped:warning + +# Member can be made 'readonly' +csharp_style_prefer_readonly_struct_member = true +dotnet_diagnostic.IDE0251.severity = warning + +dotnet_diagnostic.CS1591.severity = silent +// Use primary constructor +csharp_style_prefer_primary_constructors = false + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = false +dotnet_separate_import_directive_groups = false + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_collection_expression = never + +# Whitespace options +dotnet_style_allow_multiple_blank_lines_experimental = false + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +file_header_template = Copyright (C) 2015-2025 The Neo Project.\n\n{fileName} file belongs to the neo project and is free\nsoftware distributed under the MIT software license, see the\naccompanying file LICENSE in the main directory of the\nrepository or http://www.opensource.org/licenses/mit-license.php\nfor more details.\n\nRedistribution and use in source and binary forms with or without\nmodifications are permitted. + +# Require file header +dotnet_diagnostic.IDE0073.severity = warning + +# RS0016: Only enable if API files are present +dotnet_public_api_analyzer.require_api_files = true + +# IDE0055: Fix formatting +# Workaround for https://github.com/dotnet/roslyn/issues/70570 +dotnet_diagnostic.IDE0055.severity = warning + +# CSharp code style settings: +[*.cs] +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Whitespace options +csharp_style_allow_embedded_statements_on_same_line_experimental = false +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# IDE0230: Use UTF-8 string literal +csharp_style_prefer_utf8_string_literals = true:silent + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = none + +[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures,VisualStudio}/**/*.{cs,vb}] + +# Avoid "this." and "Me." if not necessary +dotnet_diagnostic.IDE0003.severity = warning +dotnet_diagnostic.IDE0009.severity = warning + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline:warning +# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 +dotnet_diagnostic.IDE0011.severity = warning + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning + +# IDE0052: Remove unread private member +dotnet_diagnostic.IDE0052.severity = warning + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.IDE0059.severity = warning + +# Use collection expression for array +dotnet_diagnostic.IDE0300.severity = warning + +# CA1012: Abstract types should not have public constructors +dotnet_diagnostic.CA1012.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = warning + +# csharp_style_allow_embedded_statements_on_same_line_experimental +dotnet_diagnostic.IDE2001.severity = warning + +# csharp_style_allow_blank_lines_between_consecutive_braces_experimental +dotnet_diagnostic.IDE2002.severity = warning + +# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental +dotnet_diagnostic.IDE2004.severity = warning + +# csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental +dotnet_diagnostic.IDE2005.severity = warning + +# csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental +dotnet_diagnostic.IDE2006.severity = warning + +[src/{VisualStudio}/**/*.{cs,vb}] +# CA1822: Make member static +# There is a risk of accidentally breaking an internal API that partners rely on though IVT. +dotnet_code_quality.CA1822.api_surface = private diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..3a65aebf75 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,70 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text eol=lf +*.cs text eol=lf +*.csproj text eol=lf +*.props text eol=lf +*.json text eol=lf +*.targets text eol=lf + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +*.sln text eol=crlf +#*.csproj text eol=crlf +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +*.jpg binary +*.png binary +*.gif binary +*.ico binary +*.zip binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..f69eccca73 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to detail an error or unexpected behavior +title: '' +labels: '' +assignees: '' +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open the project, run '...' +2. Type '...' or do '...' +3. ... + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Platform:** + - OS: [e.g. Windows 10 x64] + - Version [e.g. neo-cli 2.10.2] + +**(Optional) Additional context** +Add any other context about the problem here. + +However, if your issue does not fit these aforementioned criteria, or it can be understood in another manner, feel free to open it in a different format. diff --git a/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md new file mode 100644 index 0000000000..ddc181fabb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md @@ -0,0 +1,27 @@ +--- +name: Feature or enhancement request +about: Suggest an idea for Neo +title: '' +labels: Discussion +assignees: '' +--- + +**Summary or problem description** +A summary of the problem you want to solve or metric you want to improve + +**Do you have any solution you want to propose?** +A clear and concise description of what you expect with this change. + +**Where in the software does this update applies to?** +- Compiler +- Consensus +- CLI +- Plugins +- Ledger +- Network Policy +- P2P (TCP) +- RPC (HTTP) +- SDK +- VM +- Other: + diff --git a/.github/ISSUE_TEMPLATE/questions.md b/.github/ISSUE_TEMPLATE/questions.md new file mode 100644 index 0000000000..4945492ae3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/questions.md @@ -0,0 +1,9 @@ +--- +name: Questions +about: Questions about Neo Platform +title: '' +labels: Question +assignees: '' +--- + +**Delete this: We would like to use GitHub for bug reports and feature requests only however if you are unable to get support from our team in: our [Discord](https://discord.io/neo) server or in our [offical documentation](https://docs.neo.org/docs/en-us/index.html), feel encouraged to create an issue here on GitHub.** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..e093c7c141 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,47 @@ +# Description + + + +# Change Log + + +Fixes # (issue) + +## Type of change + + + +- [ ] 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) +- [ ] This change requires a documentation update + +# How Has This Been Tested? + + + +- [ ] Unit Testing +- [ ] Run Application +- [ ] Local Computer Tests +- [ ] No Testing + + +# Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/.github/images/compiler.png b/.github/images/compiler.png new file mode 100644 index 0000000000..affa7ca8ac Binary files /dev/null and b/.github/images/compiler.png differ diff --git a/.github/images/consensus.png b/.github/images/consensus.png new file mode 100644 index 0000000000..b1c6a009c5 Binary files /dev/null and b/.github/images/consensus.png differ diff --git a/.github/images/cosmetic.png b/.github/images/cosmetic.png new file mode 100644 index 0000000000..8793610920 Binary files /dev/null and b/.github/images/cosmetic.png differ diff --git a/.github/images/discord-logo.png b/.github/images/discord-logo.png new file mode 100644 index 0000000000..f53cf1b2e2 Binary files /dev/null and b/.github/images/discord-logo.png differ diff --git a/.github/images/discussion.png b/.github/images/discussion.png new file mode 100644 index 0000000000..c9b09dcfe5 Binary files /dev/null and b/.github/images/discussion.png differ diff --git a/.github/images/enhancement.png b/.github/images/enhancement.png new file mode 100644 index 0000000000..34fc8c9775 Binary files /dev/null and b/.github/images/enhancement.png differ diff --git a/.github/images/house-keeping.png b/.github/images/house-keeping.png new file mode 100644 index 0000000000..84db938661 Binary files /dev/null and b/.github/images/house-keeping.png differ diff --git a/.github/images/ledger.png b/.github/images/ledger.png new file mode 100644 index 0000000000..babf6a3ca7 Binary files /dev/null and b/.github/images/ledger.png differ diff --git a/.github/images/medium-logo.png b/.github/images/medium-logo.png new file mode 100644 index 0000000000..8ff3fb659a Binary files /dev/null and b/.github/images/medium-logo.png differ diff --git a/.github/images/migration.png b/.github/images/migration.png new file mode 100644 index 0000000000..69ca781055 Binary files /dev/null and b/.github/images/migration.png differ diff --git a/.github/images/network-policy.png b/.github/images/network-policy.png new file mode 100644 index 0000000000..84452a613e Binary files /dev/null and b/.github/images/network-policy.png differ diff --git a/.github/images/new-feature.png b/.github/images/new-feature.png new file mode 100644 index 0000000000..fba5012ff4 Binary files /dev/null and b/.github/images/new-feature.png differ diff --git a/.github/images/nnt-logo.jpg b/.github/images/nnt-logo.jpg new file mode 100644 index 0000000000..dfa91f5695 Binary files /dev/null and b/.github/images/nnt-logo.jpg differ diff --git a/.github/images/p2p.png b/.github/images/p2p.png new file mode 100644 index 0000000000..d638bd2342 Binary files /dev/null and b/.github/images/p2p.png differ diff --git a/.github/images/ready-to-implement.png b/.github/images/ready-to-implement.png new file mode 100644 index 0000000000..b5366b2c85 Binary files /dev/null and b/.github/images/ready-to-implement.png differ diff --git a/.github/images/reddit-logo.png b/.github/images/reddit-logo.png new file mode 100644 index 0000000000..be704a89e7 Binary files /dev/null and b/.github/images/reddit-logo.png differ diff --git a/.github/images/rpc.png b/.github/images/rpc.png new file mode 100644 index 0000000000..24b6c35375 Binary files /dev/null and b/.github/images/rpc.png differ diff --git a/.github/images/sdk.png b/.github/images/sdk.png new file mode 100644 index 0000000000..7e46ae0f74 Binary files /dev/null and b/.github/images/sdk.png differ diff --git a/.github/images/solution-design.png b/.github/images/solution-design.png new file mode 100644 index 0000000000..552c083d0b Binary files /dev/null and b/.github/images/solution-design.png differ diff --git a/.github/images/telegram-logo.png b/.github/images/telegram-logo.png new file mode 100644 index 0000000000..7c86be4be6 Binary files /dev/null and b/.github/images/telegram-logo.png differ diff --git a/.github/images/to-review.png b/.github/images/to-review.png new file mode 100644 index 0000000000..25054ecc88 Binary files /dev/null and b/.github/images/to-review.png differ diff --git a/.github/images/twitter-logo.png b/.github/images/twitter-logo.png new file mode 100644 index 0000000000..cb90d62291 Binary files /dev/null and b/.github/images/twitter-logo.png differ diff --git a/.github/images/vm.png b/.github/images/vm.png new file mode 100644 index 0000000000..103970371d Binary files /dev/null and b/.github/images/vm.png differ diff --git a/.github/images/wallet.png b/.github/images/wallet.png new file mode 100644 index 0000000000..42705c14ff Binary files /dev/null and b/.github/images/wallet.png differ diff --git a/.github/images/we-chat-logo.png b/.github/images/we-chat-logo.png new file mode 100644 index 0000000000..201e08c106 Binary files /dev/null and b/.github/images/we-chat-logo.png differ diff --git a/.github/images/weibo-logo.png b/.github/images/weibo-logo.png new file mode 100644 index 0000000000..364b4bc823 Binary files /dev/null and b/.github/images/weibo-logo.png differ diff --git a/.github/images/youtube-logo.png b/.github/images/youtube-logo.png new file mode 100644 index 0000000000..3c79227977 Binary files /dev/null and b/.github/images/youtube-logo.png differ diff --git a/.github/workflows/auto-labels.yml b/.github/workflows/auto-labels.yml new file mode 100644 index 0000000000..80f329540d --- /dev/null +++ b/.github/workflows/auto-labels.yml @@ -0,0 +1,28 @@ +name: Auto-label PRs + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + pull-requests: write + +jobs: + add-label: + runs-on: ubuntu-latest + steps: + - name: Add N4 label to PRs targeting master + if: github.event.pull_request.base.ref == 'master' + uses: actions-ecosystem/action-add-labels@v1.1.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + labels: | + N4 + + - name: Add N3 label to PRs targeting master-n3 + if: github.event.pull_request.base.ref == 'master-n3' + uses: actions-ecosystem/action-add-labels@v1.1.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + labels: | + N3 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..a30e1a4950 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,129 @@ +name: .NET Core Test and Publish + +on: + push: + branches: [master] + pull_request: + +env: + DOTNET_VERSION: 10.0.x + +jobs: + + Check-Vulnerable: + name: Scan for Vulnerable Dependencies + timeout-minutes: 15 + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Restore + run: dotnet restore + - name: Scan for Vulnerable Dependencies + run: dotnet list package --vulnerable --include-transitive + + Test: + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key : ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: ${{ runner.os }}-nuget- + - name: Check Format (*.cs) + if: matrix.os == 'ubuntu-latest' + run: dotnet format --verify-no-changes --verbosity diagnostic + - name: Test (ALL OS) + if: matrix.os != 'ubuntu-latest' + run: | + dotnet test -p:GITHUB_ACTIONS=true + - name: Test (ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + find tests -name *.csproj | xargs -I % dotnet add % package coverlet.msbuild + dotnet test /p:GITHUB_ACTIONS=true /p:CollectCoverage=true /p:CoverletOutput='${{ github.workspace }}/TestResults/coverage/' /p:MergeWith='${{ github.workspace }}/TestResults/coverage/coverage.json' /p:CoverletOutputFormat=\"lcov,json\" -m:1 + - name: Coveralls + if: matrix.os == 'ubuntu-latest' + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + format: lcov + file: ${{ github.workspace }}/TestResults/coverage/coverage.info + + PublishMyGet: + if: github.ref == 'refs/heads/master' && startsWith(github.repository, 'neo-project/') + needs: [Test] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Setup .NET Core + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Set Version + run: git rev-list --count HEAD | xargs printf 'CI%05d' | xargs -I{} echo 'VERSION_SUFFIX={}' >> $GITHUB_ENV + - name : Pack (Everything) + run: dotnet pack -c Debug -o out --include-source --version-suffix ${{ env.VERSION_SUFFIX }} + - name: Publish to myGet + run: dotnet nuget push out/*.nupkg -s https://www.myget.org/F/neo/api/v2/package -k ${MYGET_TOKEN} -ss https://www.myget.org/F/neo/symbols/api/v2/package -sk ${MYGET_TOKEN} + env: + MYGET_TOKEN: ${{ secrets.MYGET_TOKEN }} + + Release: + if: github.ref == 'refs/heads/master' && startsWith(github.repository, 'neo-project/') + needs: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Get version + id: get_version + run: | + sudo apt install xmlstarlet + find src -name Directory.Build.props | xargs xmlstarlet sel -N i=http://schemas.microsoft.com/developer/msbuild/2003 -t -v "concat('version=v',//i:Version/text())" | xargs echo >> $GITHUB_OUTPUT + - name: Check tag + if: steps.get_version.outputs.version != 'v' && startsWith(steps.get_version.outputs.version, 'v') + id: check_tag + run: curl -s -I ${{ format('https://github.com/%7B0%7D/releases/tag/%7B1%7D', github.repository, steps.get_version.outputs.version) }} | head -n 1 | cut -d$' ' -f2 | xargs printf "statusCode=%s" | xargs echo >> $GITHUB_OUTPUT + - name: Create release + if: steps.check_tag.outputs.statusCode == '404' + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.get_version.outputs.version }} + release_name: ${{ steps.get_version.outputs.version }} + prerelease: ${{ contains(steps.get_version.outputs.version, '-') }} + - name: Setup .NET Core + if: steps.check_tag.outputs.statusCode == '404' + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Publish to NuGet + if: steps.check_tag.outputs.statusCode == '404' + run: | + dotnet pack -o out -c Release + dotnet nuget push out/*.nupkg -s https://api.nuget.org/v3/index.json -k ${NUGET_TOKEN} + env: + NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} diff --git a/.github/workflows/pkgs-delete.yml b/.github/workflows/pkgs-delete.yml new file mode 100644 index 0000000000..eef291383a --- /dev/null +++ b/.github/workflows/pkgs-delete.yml @@ -0,0 +1,57 @@ +name: Package Cleanup + +on: + schedule: + - cron: '0 0 * * *' # Run every day at 24:00 + +jobs: + + delete-git-docker-pkgs: + name: Delete Old Docker Images + runs-on: ubuntu-latest + + steps: + - name: Delete Neo Package (docker) + uses: actions/delete-package-versions@v4 + continue-on-error: true + with: + package-name: Neo + package-type: docker + min-versions-to-keep: 1 + token: "${{ secrets.GITHUB_TOKEN }}" + + delete-git-nuget-pkgs: + name: Delete Old Nuget Packages + strategy: + matrix: + pkgs: + - "Neo.Plugins.StatesDumper" + - "Neo.Plugins.StateService" + - "Neo.Plugins.Storage.LevelDBStore" + - "Neo.Plugins.Storage.RocksDBStore" + - "Neo.Plugins.StorageDumper" + - "Neo.Plugins.TokensTracker" + - "Neo.Wallets.SQLite" + - "Neo.Consensus.DBFT" + - "Neo.ConsoleService" + - "Neo.Cryptography.MPT" + - "Neo.Extensions" + - "Neo.Network.RPC.RpcClient" + - "Neo.Plugins.ApplicationLogs" + - "Neo.Plugins.OracleService" + - "Neo.Plugins.RpcServer" + - "Neo.Json" + - "Neo.IO" + - "Neo" + runs-on: ubuntu-latest + + steps: + - name: Delete ${{ matrix.pkgs }} Package + uses: actions/delete-package-versions@v4 + continue-on-error: true + with: + package-name: ${{ matrix.pkgs }} + package-type: nuget + min-versions-to-keep: 3 + delete-only-pre-release-versions: "true" + token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.gitignore b/.gitignore index 88426a8e9b..b8feea9c3d 100644 --- a/.gitignore +++ b/.gitignore @@ -153,10 +153,14 @@ PublishScripts/ # NuGet Packages *.nupkg +*.snupkg + # The packages folder can be ignored because of Package Restore **/packages/* + # except build/, which is used as an MSBuild target. !**/packages/build/ + # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files @@ -255,3 +259,12 @@ paket-files/ *.sln.iml PublishProfiles +/.vscode +launchSettings.json +/coverages +**/.DS_Store + +# Benchmarks +**/BenchmarkDotNet.Artifacts/ +/src/Neo.CLI/neo-cli/ +**/localnet_nodes/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 772858f229..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: csharp - -os: - - linux - - osx - -dist: trusty -osx_image: xcode9.1 - -mono: none -dotnet: 2.0.0 - -before_install: - - cd neo.UnitTests - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ulimit -n 2048; fi - -script: - - dotnet restore - - dotnet test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..5fd0592924 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,82 @@ + +# Contributing to NEO +Neo is an open-source project and it depends on its contributors and constant community feedback to implement the features required for a smart economy. You are more than welcome to join us in the development of Neo. + +Read this document to understand how issues are organized and how you can start contributing. + +*This document covers this repository only and does not include community repositories or repositories managed by NGD Shanghai and NGD Seattle.* + +### Questions and Support +The issue list is reserved exclusively for bug reports and features discussions. If you have questions or need support, please visit us in our [Discord](https://discord.io/neo) server. + +### dApp Development +This document does not relate to dApp development. If you are looking to build a dApp using Neo, please [start here](https://neo.org/dev). + +### Contributing to open source projects +If you are new to open-source development, please [read here](https://opensource.guide/how-to-contribute/#opening-a-pull-request) how to submit your code. + +## Developer Guidance +We try to have as few rules as possible, just enough to keep the project organized: + + +1. **Discuss before coding**. Proposals must be discussed before being implemented. +Avoid implementing issues with the discussion tag. +2. **Tests during code review**. We expect reviewers to test the issue before approving or requesting changes. + +3. **Wait for at least 2 reviews before merging**. Changes can be merged after 2 approvals, for Neo 3.x branch, and 3 approvals for Neo 2.x branch. + +3. **Give time to other developers review an issue**. Even if the code has been approved, you should leave at least 24 hours for others to review it before merging the code. + +4. **Create unit tests**. It is important that the developer includes basic unit tests so reviewers can test it. + +5. **Task assignment**. If a developer wants to work in a specific issue, he may ask the team to assign it to himself. The proposer of an issue has priority in task assignment. + + +### Issues for beginners +If you are looking to start contributing to NEO, we suggest you start working on issues with ![](./.github/images/cosmetic.png) or ![](./.github/images/house-keeping.png) tags since they usually do not depend on extensive NEO platform knowledge. + +### Tags for Issues States + +![Discussion](./.github/images/discussion.png) Whenever someone posts a new feature request, the tag discussion is added. This means that there is no consensus if the feature should be implemented or not. Avoid creating PR to solve issues in this state since it may be completely discarded. + +![Design](./.github/images/solution-design.png) When a feature request is accepted by the team, but there is no consensus about the implementation, the issue is tagged with design. We recommend the team to agree in the solution design before anyone attempts to implement it, using text or UML. It is not recommended, but developers can also present their solution using code. +Note that PRs for issues in this state may also be discarded if the team disagree with the proposed solution. + +![Ready-to-implement](./.github/images/ready-to-implement.png) Once the team has agreed on feature and the proposed solution, the issue is tagged with ready-to-implement. When implementing it, please follow the solution accepted by the team. + +### Tags for Issue Types + +![Cosmetic](./.github/images/cosmetic.png) Issues with the cosmetic tag are usually changes in code or documentation that improve user experience without affecting current functionality. These issues are recommended for beginners because they require little to no knowledge about Neo platform. + +![Enhancement](./.github/images/enhancement.png) Enhancements are platform changes that may affect performance, usability or add new features to existing modules. It is recommended that developers have previous knowledge in the platform to work in these improvements, specially in more complicated modules like the compiler, ledger and consensus. + +![Feature](./.github/images/new-feature.png) New features may include large changes in the code base. Some are complex, but some are not. So, a few issues with new-feature may be recommended for starters, specially those related to the rpc and the sdk module. + +![Migration](./.github/images/migration.png) Issues related to the migration from Neo 2 to Neo 3 are tagged with migration. These issues are usually the most complicated ones since they require a deep knowledge in both versions. + +### Tags for Project Modules +These tags do not necessarily represent each module at code level. Modules consensus and compiler are not recommended for beginners. + +![Compiler](./.github/images/compiler.png) Issues that are related or influence the behavior of our C# compiler. Note that the compiler itself is hosted in the [neo-devpack-dotnet](https://github.com/neo-project/neo-devpack-dotnet) repository. + +![Consensus](./.github/images/consensus.png) Changes to consensus are usually harder to make and test. Avoid implementing issues in this module that are not yet decided. + +![Ledger](./.github/images/ledger.png) The ledger is our 'database', any changes in the way we store information or the data-structures have this tag. + +![House-keeping](./.github/images/house-keeping.png) 'Small' enhancements that need to be done in order to keep the project organised and ensure overall quality. These changes may be applied in any place in code, as long as they are small or do not alter current behavior. + +![Network-policy](./.github/images/network-policy.png) Identify issues that affect the network-policy like fees, access list or other related issues. Voting may also be related to the network policy module. + +![P2P](./.github/images/p2p.png) This module includes peer-to-peer message exchange and network optimisations, at TCP or UDP level (not HTTP). + +![RPC](./.github/images/rpc.png) All HTTP communication is handled by the RPC module. This module usually provides support methods since the main communication protocol takes place at the p2p module. + +![VM](./.github/images/vm.png) New features that affect the Neo Virtual Machine or the Interop layer. + +![SDK](./.github/images/sdk.png) Neo provides an SDK to help developers to interact with the blockchain. Changes in this module must not impact other parts of the software. + +![Wallet](./.github/images/wallet.png) Wallets are used to track funds and interact with the blockchain. Note that this module depends on a full node implementation (data stored on local disk). + + + + diff --git a/README.md b/README.md index a1d7fe9f0d..bd05fb7f08 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,201 @@ -NEO: A distributed network for the Smart Economy -================ +

+ + neo-logo + +

+ +

CSharp implementation of the neo blockchain protocol.

+ +

+ A modern distributed network for the Smart Economy. +
+ Documentation » +
+
+ Neo + · + Neo Modules + · + Neo DevPack +

+

+ + + +   + + + +   + + + +   + + + +   + + + +   + + + +   + + + +   + + + + +   + + + +

+

+ + Current neo version. + + + Coverage Status + + + License. + +

+ +

+ + Open in GitHub Codespaces. + +

+ +## Table of Contents +1. [Overview](#overview) +2. [Project structure](#project-structure) +3. [Related projects](#related-projects) +4. [Opening a new issue](#opening-a-new-issue) +5. [Contributing](#contributing) +6. [Bounty program](#bounty-program) +7. [License](#license) + +## Overview +This repository is a csharp implementation of the [neo](https://neo.org) blockchain. It is jointly maintained by the neo core developers and neo global development community. +Visit the [tutorials](https://docs.neo.org) to get started. + + +## Project structure +An overview of the project folders can be seen below. + +|Folder|Content| +|---|---| +|[/src/neo/Cryptography/](https://github.com/neo-project/neo/tree/master/src/Neo/Cryptography)|General cryptography implementation, including ECC.| +|[/src/neo/IO/](https://github.com/neo-project/neo/tree/master/src/Neo/IO)|Data structures used for caching and collection interaction.| +|[/src/neo/Ledger/](https://github.com/neo-project/neo/tree/master/src/Neo/Ledger)|Classes responsible for the state control, including the `MemoryPool` and `Blockchain`.| +|[/src/neo/Network/](https://github.com/neo-project/neo/tree/master/src/Neo/Network)|Peer-to-peer protocol implementation.| +|[/src/neo/Persistence/](https://github.com/neo-project/neo/tree/master/src/Neo/Persistence)|Classes used to allow other classes to access application state.| +|[/src/neo/Plugins/](https://github.com/neo-project/neo/tree/master/src/Neo/Plugins)|Interfaces used to extend Neo, including the storage interface.| +|[/src/neo/SmartContract/](https://github.com/neo-project/neo/tree/master/src/Neo/SmartContract)|Native contracts, `ApplicationEngine`, `InteropService` and other smart-contract related classes.| +|[/src/neo/Wallets/](https://github.com/neo-project/neo/tree/master/src/Neo/Wallets)|Wallet and account implementation.| +|[/src/Neo.Extensions/](https://github.com/neo-project/neo/tree/master/src/Neo.Extensions)| Extensions to expand the existing functionality.| +|[/src/Neo.Json/](https://github.com/neo-project/neo/tree/master/src/Neo.Json)| Neo's JSON specification.| +|[/tests/](https://github.com/neo-project/neo/tree/master/tests)|All unit tests.| + +## Related projects +Code references are provided for all platform building blocks. That includes the base library, the VM, a command line application and the compiler. + +* [neo:](https://github.com/neo-project/neo/) Included libraries are Neo, Neo-CLI, Neo-GUI, Neo-VM, test and plugin modules. +* [neo-express:](https://github.com/neo-project/neo-express/) A private net optimized for development scenarios. +* [neo-devpack-dotnet:](https://github.com/neo-project/neo-devpack-dotnet/) These are the official tools used to convert a C# smart-contract into a *neo executable file*. +* [neo-proposals:](https://github.com/neo-project/proposals) NEO Enhancement Proposals (NEPs) describe standards for the NEO platform, including core protocol specifications, client APIs, and contract standards. +* [neo-non-native-contracts:](https://github.com/neo-project/non-native-contracts) Includes non-native contracts that live on the blockchain, included but not limited to NeoNameService. + +## Opening a new issue +Please feel free to create new issues to suggest features or ask questions. + +- [Feature request](https://github.com/neo-project/neo/issues/new?assignees=&labels=discussion&template=feature-or-enhancement-request.md&title=) +- [Bug report](https://github.com/neo-project/neo/issues/new?assignees=&labels=&template=bug_report.md&title=) +- [Questions](https://github.com/neo-project/neo/issues/new?assignees=&labels=question&template=questions.md&title=) + +If you found a security issue, please refer to our [security policy](https://github.com/neo-project/neo/security/policy). + +## Contributing + +We welcome contributions to the Neo project! To ensure a smooth collaboration process, please follow these guidelines: + +### Branch Rules + +- **`master`** - Contains the latest stable release version. This branch reflects the current production state. +- **`dev`** - The main development branch where all new features and improvements are integrated. + +### Pull Request Guidelines + +**Important**: All pull requests must be based on the `dev` branch, not `master`. + +1. **Fork the repository** and create your feature branch from `dev`: + ```bash + git checkout dev + git pull origin dev + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes** following the project's coding standards and conventions. + +3. **Test your changes** thoroughly to ensure they don't break existing functionality. + +4. **Commit your changes** with clear, descriptive commit messages: + ```bash + git commit -m "feat: add new feature description" + ``` + +5. **Push to your fork** and create a pull request against the `dev` branch: + ```bash + git push origin feature/your-feature-name + ``` + +6. **Create a Pull Request** targeting the `dev` branch with: + - Clear title and description + - Reference to any related issues + - Summary of changes made -NEO uses digital identity and blockchain technology to digitize assets and leverages smart contracts for autonomously managed digital assets to create a "smart economy" within a decentralized network. - -To learn more about NEO, please read the [White Paper](http://docs.neo.org/en-us/index.html)|[白皮书](http://docs.neo.org/zh-cn/index.html). - -Supported Platforms --------- - -We already support the following platforms: - -* CentOS 7 -* Docker -* macOS 10 + -* Red Hat Enterprise Linux 7.0 + -* Ubuntu 14.04, Ubuntu 14.10, Ubuntu 15.04, Ubuntu 15.10, Ubuntu 16.04, Ubuntu 16.10 -* Windows 7 SP1 +, Windows Server 2008 R2 + - -We will support the following platforms in the future: - -* Debian -* Fedora -* FreeBSD -* Linux Mint -* OpenSUSE -* Oracle Linux - -Development --------- - -To start building peer applications for NEO on Windows, you need to download [Visual Studio 2017](https://www.visualstudio.com/products/visual-studio-community-vs) and install the [.NET Core SDK](https://www.microsoft.com/net/core). - -If you need to develop on Linux or macOS, just install the [.NET Core SDK](https://www.microsoft.com/net/core). - -To install Neo SDK to your project, run the following command in the [Package Manager Console](https://docs.nuget.org/ndocs/tools/package-manager-console): +### Development Workflow ``` -PM> Install-Package Neo +feature/bug-fix → dev → master (via release) ``` -For more information about how to build DAPPs for NEO, please read the [documentation](http://docs.neo.org/en-us/sc/introduction.html)|[文档](http://docs.neo.org/zh-cn/sc/introduction.html). - -How to Contribute --------- - -You can contribute to NEO with [issues](https://github.com/neo-project/neo/issues) and [PRs](https://github.com/neo-project/neo/pulls). Simply filing issues for problems you encounter is a great way to contribute. Contributing implementations is greatly appreciated. - -We use and recommend the following workflow: - -1. Create an issue for your work. - * You can skip this step for trivial changes. - * Reuse an existing issue on the topic, if there is one. - * Clearly state that you are going to take on implementing it, if that's the case. You can request that the issue be assigned to you. Note: The issue filer and the implementer don't have to be the same person. -1. Create a personal fork of the repository on GitHub (if you don't already have one). -1. Create a branch off of master(`git checkout -b mybranch`). - * Name the branch so that it clearly communicates your intentions, such as issue-123 or githubhandle-issue. - * Branches are useful since they isolate your changes from incoming changes from upstream. They also enable you to create multiple PRs from the same fork. -1. Make and commit your changes. -1. Add new tests corresponding to your change, if applicable. -1. Build the repository with your changes. - * Make sure that the builds are clean. - * Make sure that the tests are all passing, including your new tests. -1. Create a pull request (PR) against the upstream repository's master branch. - * Push your changes to your fork on GitHub. +- Feature branches are merged into `dev` +- `dev` is periodically merged into `master` for releases +- Never create PRs directly against `master` -Note: It is OK for your PR to include a large number of commits. Once your change is accepted, you will be asked to squash your commits into one or some appropriately small number of commits before your PR is merged. +For more detailed contribution guidelines, please check our documentation or reach out to the maintainers. -License ------- +## Bounty program +You can be rewarded by finding security issues. Please refer to our [bounty program page](https://neo.org/bounty) for more information. +## License The NEO project is licensed under the [MIT license](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..24894d5e9a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +The purpose of NEO vulnerability bounty program is to be proactive about blockchain security by providing a channel for security researchers to report potential security vulnerabilities identified related to our underlying infrastructure. + +First, it is recommended to read the following page https://neo.org/dev/bounty, if you find a security vulnerability in NEO, please report it as indicated on that page. + +Please withhold public disclosure until the security team has addressed the vulnerability and it has been solved. + +We appreciate your efforts to responsibly disclose your findings, and we will make every effort to acknowledge your contributions. + +The security team will acknowledge your email within 5 business days. You will receive a more detailed response within 10 business days. + +When in doubt, please do send us a report. diff --git a/SpellingExclusions.dic b/SpellingExclusions.dic new file mode 100644 index 0000000000..e69de29bb2 diff --git a/benchmarks/Directory.Build.props b/benchmarks/Directory.Build.props new file mode 100644 index 0000000000..23182611c1 --- /dev/null +++ b/benchmarks/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + Exe + net10.0 + enable + enable + false + + + + + + + diff --git a/benchmarks/Neo.Benchmarks/Benchmarks.Hash.cs b/benchmarks/Neo.Benchmarks/Benchmarks.Hash.cs new file mode 100644 index 0000000000..36ce30108b --- /dev/null +++ b/benchmarks/Neo.Benchmarks/Benchmarks.Hash.cs @@ -0,0 +1,55 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmarks.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 BenchmarkDotNet.Attributes; +using Neo.Cryptography; +using System.Diagnostics; +using System.IO.Hashing; +using System.Text; + +namespace Neo.Benchmarks; + +public class Benchmarks_Hash +{ + // 256 KiB + static readonly byte[] data = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat("Hello, World!^_^", 16 * 1024))); + + static readonly byte[] hash = "9182abedfbb9b18d81a05d8bcb45489e7daa2858".HexToBytes(); + + [Benchmark] + public static void XxHash32_HashToUInt32() + { + var result = XxHash32.HashToUInt32(data); + Debug.Assert(result == 682967318u); + } + + [Benchmark] + public static void XxHash3_HashToUInt64() + { + var result = (uint)XxHash3.HashToUInt64(data); + Debug.Assert(result == 1389469485u); + } + + [Benchmark] + public static void Murmur32_HashToUInt32() + { + var result = data.Murmur32(0); + Debug.Assert(result == 3731881930u); + } + + [Benchmark] + public static void Murmur128_ComputeHash() + { + var result = data.Murmur128(0); + if (result.Length != 16) + throw new InvalidOperationException($"Invalid hash length {result.Length}"); + } +} diff --git a/benchmarks/Neo.Benchmarks/Benchmarks.POC.cs b/benchmarks/Neo.Benchmarks/Benchmarks.POC.cs new file mode 100644 index 0000000000..08a134512d --- /dev/null +++ b/benchmarks/Neo.Benchmarks/Benchmarks.POC.cs @@ -0,0 +1,78 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmarks.POC.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.VM; +using System.Diagnostics; + +namespace Neo.Benchmarks; + +public class Benchmarks_PoCs +{ + private static readonly ProtocolSettings protocol = ProtocolSettings.Load("config.json"); + private static readonly NeoSystem system = new(protocol, (string?)null); + + [Benchmark] + public void NeoIssue2725() + { + // https://github.com/neo-project/neo/issues/2725 + // L00: INITSSLOT 1 + // L01: NEWARRAY0 + // L02: PUSHDATA1 6161616161 //"aaaaa" + // L03: PUSHINT16 500 + // L04: STSFLD0 + // L05: OVER + // L06: OVER + // L07: SYSCALL 95016f61 //System.Runtime.Notify + // L08: LDSFLD0 + // L09: DEC + // L10: DUP + // L11: STSFLD0 + // L12: JMPIF L05 + // L13: CLEAR + // L14: SYSCALL dbfea874 //System.Runtime.GetExecutingScriptHash + // L15: PUSHINT16 8000 + // L16: STSFLD0 + // L17: DUP + // L18: SYSCALL 274335f1 //System.Runtime.GetNotifications + // L19: DROP + // L20: LDSFLD0 + // L21: DEC + // L22: DUP + // L23: STSFLD0 + // L24: JMPIF L17 + Run(nameof(NeoIssue2725), "VgHCDAVhYWFhYQH0AWBLS0GVAW9hWJ1KYCT1SUHb/qh0AUAfYEpBJ0M18UVYnUpgJPU="); + } + + private static void Run(string name, string poc) + { + Random random = new(); + Transaction tx = new() + { + Version = 0, + Nonce = (uint)random.Next(), + SystemFee = 20_00000000, + NetworkFee = 1_00000000, + ValidUntilBlock = ProtocolSettings.Default.MaxTraceableBlocks, + Signers = Array.Empty(), + Attributes = Array.Empty(), + Script = Convert.FromBase64String(poc), + Witnesses = Array.Empty() + }; + using var snapshot = system.GetSnapshotCache(); + using var engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshot, system.GenesisBlock, protocol, tx.SystemFee); + engine.LoadScript(tx.Script); + engine.Execute(); + Debug.Assert(engine.State == VMState.FAULT); + } +} diff --git a/benchmarks/Neo.Benchmarks/Benchmarks.SignData.cs b/benchmarks/Neo.Benchmarks/Benchmarks.SignData.cs new file mode 100644 index 0000000000..f0c6be93c1 --- /dev/null +++ b/benchmarks/Neo.Benchmarks/Benchmarks.SignData.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmarks.SignData.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; +using Neo.Extensions.IO; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; + +namespace Neo.Benchmarks; + +public class Benchmarks_SignData +{ + private static readonly Transaction s_tx = new() + { + Attributes = [], + Script = Array.Empty(), + Signers = [new Signer() { Account = UInt160.Zero, AllowedContracts = [], AllowedGroups = [], Rules = [], Scopes = WitnessScope.Global }], + Witnesses = [] + }; + + /// + /// Gets the data of a object to be hashed. + /// + /// The object to hash. + /// The magic number of the network. + /// The data to hash. + public static byte[] GetSignDataV1(IVerifiable verifiable, uint network) + { + using MemoryStream ms = new(); + using BinaryWriter writer = new(ms); + writer.Write(network); + writer.Write(verifiable.Hash); + writer.Flush(); + return ms.ToArray(); + } + + [Benchmark] + public static void TestOld() + { + GetSignDataV1(s_tx, 0); + } + + [Benchmark] + public static void TestNew() + { + s_tx.GetSignData(0); + } +} diff --git a/benchmarks/Neo.Benchmarks/Benchmarks.UInt160.cs b/benchmarks/Neo.Benchmarks/Benchmarks.UInt160.cs new file mode 100644 index 0000000000..091dda8f68 --- /dev/null +++ b/benchmarks/Neo.Benchmarks/Benchmarks.UInt160.cs @@ -0,0 +1,86 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmarks.UInt160.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; + +namespace Neo.Benchmarks; + +public class Benchmarks_UInt160 +{ + static readonly UInt160 s_newUInt160 = new([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + [Benchmark] + public static void TestGernerator1() + { + _ = new UInt160(); + } + + [Benchmark] + public static void TestGernerator2() + { + _ = new UInt160(new byte[20]); + } + + [Benchmark] + public static void TestCompareTo() + { + UInt160.Zero.CompareTo(UInt160.Zero); + UInt160.Zero.CompareTo(s_newUInt160); + s_newUInt160.CompareTo(UInt160.Zero); + } + + [Benchmark] + public static void TestEquals() + { + UInt160.Zero.Equals(UInt160.Zero); + UInt160.Zero.Equals(s_newUInt160); + s_newUInt160.Equals(null); + } + + [Benchmark] + public static void TestParse() + { + _ = UInt160.Parse("0x0000000000000000000000000000000000000000"); + _ = UInt160.Parse("0000000000000000000000000000000000000000"); + } + + [Benchmark] + public static void TestTryParse() + { + _ = UInt160.TryParse("0x0000000000000000000000000000000000000000", out _); + _ = UInt160.TryParse("0x1230000000000000000000000000000000000000", out _); + _ = UInt160.TryParse("000000000000000000000000000000000000000", out _); + } + + [Benchmark] + public static void TestOperatorLarger() + { + _ = s_newUInt160 > UInt160.Zero; + } + + [Benchmark] + public static void TestOperatorLargerAndEqual() + { + _ = s_newUInt160 >= UInt160.Zero; + } + + [Benchmark] + public static void TestOperatorSmaller() + { + _ = s_newUInt160 < UInt160.Zero; + } + + [Benchmark] + public static void TestOperatorSmallerAndEqual() + { + _ = s_newUInt160 <= UInt160.Zero; + } +} diff --git a/benchmarks/Neo.Benchmarks/IO/Benchmarks.Cache.cs b/benchmarks/Neo.Benchmarks/IO/Benchmarks.Cache.cs new file mode 100644 index 0000000000..04959aa0f9 --- /dev/null +++ b/benchmarks/Neo.Benchmarks/IO/Benchmarks.Cache.cs @@ -0,0 +1,130 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmarks.Cache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; +using Neo.IO.Caching; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Neo.Benchmarks.IO; + +class BenchmarkFIFOCache : FIFOCache +{ + public BenchmarkFIFOCache(int maxCapacity) : base(maxCapacity) { } + + protected override long GetKeyForItem(long item) => item; +} + +class BenchmarkKeyedCollectionSlim : KeyedCollectionSlim +{ + public BenchmarkKeyedCollectionSlim(int capacity) : base(capacity) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override long GetKeyForItem(long item) => item; +} + +public class Benchmarks_Cache +{ + const int CacheSize = 1000; + + private readonly BenchmarkFIFOCache _cache = new(CacheSize); + + private readonly HashSetCache _hashSetCache = new(CacheSize); + + private long[] _items = []; + + [Params(1000, 10000)] + public int OperationCount { get; set; } + + [GlobalSetup] + public void Setup() + { + // Initialize cache with some data + for (int i = 0; i < CacheSize; i++) + { + _cache.Add(i); + } + } + + [Benchmark] + public void FIFOCacheAdd() + { + for (int i = 0; i < OperationCount; i++) + { + _cache.Add(i); + } + } + + [Benchmark] + public void FIFOCacheContains() + { + for (long i = 0; i < OperationCount; i++) + { + var ok = _cache.TryGet(i, out _); + Debug.Assert(ok); + } + } + + [Benchmark] + public void KeyedCollectionSlimAdd() + { + var keyed = new BenchmarkKeyedCollectionSlim(CacheSize); + for (int i = 0; i < OperationCount; i++) + { + keyed.TryAdd(i); + } + } + + [Benchmark] + public void KeyedCollectionSlimMixed() + { + var keyed = new BenchmarkKeyedCollectionSlim(CacheSize); + for (long i = 0; i < OperationCount; i++) + { + keyed.TryAdd(i); + + var ok = keyed.Contains(i); + Debug.Assert(ok); + } + + for (long i = 0; i < OperationCount; i++) + { + var ok = keyed.Remove(i); + Debug.Assert(ok); + } + } + + [GlobalSetup(Target = nameof(HashSetCache))] + public void SetupHashSetCache() + { + _items = new long[OperationCount]; + for (int i = 0; i < OperationCount; i++) + { + _items[i] = i; + } + } + + [Benchmark] + public void HashSetCache() + { + for (int i = 0; i < OperationCount; i++) + { + var ok = _hashSetCache.TryAdd(i); + Debug.Assert(ok); + } + if (_hashSetCache.Count != CacheSize) + throw new Exception($"HashSetCacheAdd: {_hashSetCache.Count}"); + + _hashSetCache.ExceptWith(_items); + if (_hashSetCache.Count > 0) + throw new Exception($"HashSetCacheExceptWith: {_hashSetCache.Count}"); + } +} diff --git a/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj new file mode 100644 index 0000000000..3138e58bad --- /dev/null +++ b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj @@ -0,0 +1,14 @@ + + + + + + + + + PreserveNewest + PreserveNewest + + + + diff --git a/benchmarks/Neo.Benchmarks/Program.cs b/benchmarks/Neo.Benchmarks/Program.cs new file mode 100644 index 0000000000..16255a17ee --- /dev/null +++ b/benchmarks/Neo.Benchmarks/Program.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Program.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Running; + +// List all benchmarks: +// dotnet run -c Release --framework [for example: net9.0] -- --list flat(or tree) +// Run a specific benchmark: +// dotnet run -c Release --framework [for example: net9.0] -- -f [benchmark name] +// Run all benchmarks: +// dotnet run -c Release --framework [for example: net9.0] -- -f * +// Run all benchmarks of a class: +// dotnet run -c Release --framework [for example: net9.0] -- -f '*Class*' +// More options: https://benchmarkdotnet.org/articles/guides/console-args.html +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/benchmarks/Neo.Benchmarks/SmartContract/Benchmarks.StorageKey.cs b/benchmarks/Neo.Benchmarks/SmartContract/Benchmarks.StorageKey.cs new file mode 100644 index 0000000000..a890dc7c37 --- /dev/null +++ b/benchmarks/Neo.Benchmarks/SmartContract/Benchmarks.StorageKey.cs @@ -0,0 +1,62 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmarks.StorageKey.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; +using Neo.SmartContract; +using System.Text; + +namespace Neo.Benchmarks.SmartContract; + +public class Benchmarks_StorageKey +{ + // for avoiding overhead of encoding + private static readonly byte[] testBytes = Encoding.ASCII.GetBytes("StorageKey"); + + private const int prefixSize = sizeof(int) + sizeof(byte); + + [Benchmark] + public static void KeyBuilder_AddInt() + { + var key = new KeyBuilder(1, 0) + .Add(1) + .Add(2) + .Add(3); + + var bytes = key.ToArray(); + if (bytes.Length != prefixSize + 3 * sizeof(int)) + throw new InvalidOperationException(); + } + + [Benchmark] + public static void KeyBuilder_AddBytes() + { + var key = new KeyBuilder(1, 0) + .Add(testBytes) + .Add(testBytes) + .Add(testBytes); + + var bytes = key.ToArray(); + if (bytes.Length != prefixSize + 3 * testBytes.Length) + throw new InvalidOperationException(); + } + + [Benchmark] + public void KeyBuilder_AddUInt160() + { + Span value = stackalloc byte[UInt160.Length]; + var key = new KeyBuilder(1, 0) + .Add(new UInt160(value)); + + var bytes = key.ToArray(); + if (bytes.Length != prefixSize + UInt160.Length) + throw new InvalidOperationException(); + } +} diff --git a/benchmarks/Neo.Benchmarks/config.json b/benchmarks/Neo.Benchmarks/config.json new file mode 100644 index 0000000000..01471e4c76 --- /dev/null +++ b/benchmarks/Neo.Benchmarks/config.json @@ -0,0 +1,71 @@ +{ + "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" + } + }, + "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/benchmarks/Neo.Extensions.Benchmarks/Benchmark.ByteArrayComparer.cs b/benchmarks/Neo.Extensions.Benchmarks/Benchmark.ByteArrayComparer.cs new file mode 100644 index 0000000000..650cf476dc --- /dev/null +++ b/benchmarks/Neo.Extensions.Benchmarks/Benchmark.ByteArrayComparer.cs @@ -0,0 +1,95 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmark.ByteArrayComparer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; + +namespace Neo.Extensions.Benchmarks; + +public class Benchmark_ByteArrayComparer +{ + private ByteArrayComparer comparer = ByteArrayComparer.Default; + + private byte[]? x, y; + + [GlobalSetup] + public void Setup() + { + comparer = ByteArrayComparer.Default; + } + + [GlobalSetup(Target = nameof(NewCompare_50Bytes))] + public void SetupNew50Bytes() + { + comparer = ByteArrayComparer.Default; + + x = new byte[50]; // 50 bytes + y = new byte[50]; // 50 bytes + Array.Fill(x, (byte)0xCC); + Array.Copy(x, y, x.Length); + } + + [Benchmark] + public void NewCompare_50Bytes() + { + comparer.Compare(x, y); + } + + [GlobalSetup(Target = nameof(NewCompare_500Bytes))] + public void SetupNew500Bytes() + { + comparer = ByteArrayComparer.Default; + + x = new byte[500]; // 500 bytes + y = new byte[500]; // 500 bytes + Array.Fill(x, (byte)0xCC); + Array.Copy(x, y, x.Length); + } + + [Benchmark] + public void NewCompare_500Bytes() + { + comparer.Compare(x, y); + } + + [GlobalSetup(Target = nameof(NewCompare_5000Bytes))] + public void SetupNew5000Bytes() + { + comparer = ByteArrayComparer.Default; + + x = new byte[5000]; // 5000 bytes + y = new byte[5000]; // 5000 bytes + Array.Fill(x, (byte)0xCC); + Array.Copy(x, y, x.Length); + } + + [Benchmark] + public void NewCompare_5000Bytes() + { + comparer.Compare(x, y); + } + + [GlobalSetup(Target = nameof(NewCompare_50000Bytes))] + public void SetupNew50000Bytes() + { + comparer = ByteArrayComparer.Default; + + x = new byte[50000]; // 50000 bytes + y = new byte[50000]; // 50000 bytes + Array.Fill(x, (byte)0xCC); + Array.Copy(x, y, x.Length); + } + + [Benchmark] + public void NewCompare_50000Bytes() + { + comparer.Compare(x, y); + } +} diff --git a/benchmarks/Neo.Extensions.Benchmarks/Benchmark.StringExtensions.cs b/benchmarks/Neo.Extensions.Benchmarks/Benchmark.StringExtensions.cs new file mode 100644 index 0000000000..766c820200 --- /dev/null +++ b/benchmarks/Neo.Extensions.Benchmarks/Benchmark.StringExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmark.StringExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; + +namespace Neo.Extensions.Benchmarks; + +public class Benchmark_StringExtensions +{ + private const string _testHex = "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1e1f20"; + + [Benchmark] + public static void HexToBytes() + { + var bytes = _testHex.HexToBytes(); + if (bytes.Length != 32) + throw new Exception("Invalid length"); + } +} diff --git a/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj b/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj new file mode 100644 index 0000000000..954272b073 --- /dev/null +++ b/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/benchmarks/Neo.Extensions.Benchmarks/Program.cs b/benchmarks/Neo.Extensions.Benchmarks/Program.cs new file mode 100644 index 0000000000..16255a17ee --- /dev/null +++ b/benchmarks/Neo.Extensions.Benchmarks/Program.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Program.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Running; + +// List all benchmarks: +// dotnet run -c Release --framework [for example: net9.0] -- --list flat(or tree) +// Run a specific benchmark: +// dotnet run -c Release --framework [for example: net9.0] -- -f [benchmark name] +// Run all benchmarks: +// dotnet run -c Release --framework [for example: net9.0] -- -f * +// Run all benchmarks of a class: +// dotnet run -c Release --framework [for example: net9.0] -- -f '*Class*' +// More options: https://benchmarkdotnet.org/articles/guides/console-args.html +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JBoolean.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JBoolean.cs new file mode 100644 index 0000000000..3026753d47 --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JBoolean.cs @@ -0,0 +1,55 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmark_JBoolean.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; + +namespace Neo.Json.Benchmarks; + +[MemoryDiagnoser] +[CsvMeasurementsExporter] +[MarkdownExporter] +public class Benchmark_JBoolean +{ + private JBoolean _jFalse = new(); + private JBoolean _jTrue = new(true); + + [GlobalSetup] + public void Setup() + { + _jFalse = new JBoolean(); + _jTrue = new JBoolean(true); + } + + [Benchmark] + public void TestAsNumber() + { + _ = _jFalse.AsNumber(); + _ = _jTrue.AsNumber(); + } + + [Benchmark] + public void TestConversionToString() + { + _ = _jTrue.ToString(); + _ = _jFalse.ToString(); + } +} + +/// BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605) +/// 13th Gen Intel Core i9-13900H, 1 CPU, 20 logical and 14 physical cores +/// .NET SDK 9.0.101 +/// [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 [AttachedDebugger] +/// DefaultJob: .NET 9.0.0(9.0.24.52809), X64 RyuJIT AVX2 +/// +/// | Method | Mean | Error | StdDev | Median | Gen0 | Allocated | +/// |----------------------- |-----------:|----------:|----------:|-----------:|-------:|----------:| +/// | TestAsNumber | 0.0535 ns | 0.0233 ns | 0.0239 ns | 0.0427 ns | - | - | +/// | TestConversionToString | 17.8216 ns | 0.2321 ns | 0.1938 ns | 17.7613 ns | 0.0051 | 64 B | diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JNumber.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JNumber.cs new file mode 100644 index 0000000000..abaaf00171 --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JNumber.cs @@ -0,0 +1,54 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmark_JNumber.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; + +namespace Neo.Json.Benchmarks; + +[MemoryDiagnoser] +[CsvMeasurementsExporter] +[MarkdownExporter] +public class Benchmark_JNumber +{ + private JNumber _maxInt = new(JNumber.MAX_SAFE_INTEGER); + private JNumber _zero = new(0); + + [GlobalSetup] + public void Setup() + { + _maxInt = new JNumber(JNumber.MAX_SAFE_INTEGER); + _zero = new JNumber(0); + } + + [Benchmark] + public void TestAsBoolean() + { + _ = _maxInt.AsBoolean(); + _ = _zero.AsBoolean(); + } + + [Benchmark] + public void TestAsString() + { + _ = _maxInt.AsString(); + } +} + +/// BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605) +/// 13th Gen Intel Core i9-13900H, 1 CPU, 20 logical and 14 physical cores +/// .NET SDK 9.0.101 +/// [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 [AttachedDebugger] +/// DefaultJob: .NET 9.0.0(9.0.24.52809), X64 RyuJIT AVX2 +/// +/// | Method | Mean | Error | StdDev | Gen0 | Allocated | +/// |-------------- |----------:|----------:|----------:|-------:|----------:| +/// | TestAsBoolean | 2.510 ns | 0.0603 ns | 0.0564 ns | - | - | +/// | TestAsString | 87.000 ns | 1.2230 ns | 1.1440 ns | 0.0044 | 56 B | diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JObject.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JObject.cs new file mode 100644 index 0000000000..e31eda3d3c --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JObject.cs @@ -0,0 +1,62 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmark_JObject.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; + +namespace Neo.Json.Benchmarks; + +[MemoryDiagnoser] +[CsvMeasurementsExporter] +[MarkdownExporter] +public class Benchmark_JObject +{ + private JObject _alice = new(); + + [GlobalSetup] + public void Setup() + { + _alice = new JObject + { + ["name"] = "Alice", + ["age"] = 30 + }; + } + + [Benchmark] + public void TestAddProperty() + { + _alice["city"] = "New York"; + } + + [Benchmark] + public void TestClone() + { + _ = _alice.Clone(); + } + + [Benchmark] + public static void TestParse() + { + JToken.Parse("{\"name\":\"John\", \"age\":25}"); + } +} + +/// BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605) +/// 13th Gen Intel Core i9-13900H, 1 CPU, 20 logical and 14 physical cores +/// .NET SDK 9.0.101 +/// [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 [AttachedDebugger] +/// DefaultJob: .NET 9.0.0(9.0.24.52809), X64 RyuJIT AVX2 +/// +/// | Method | Mean | Error | StdDev | Gen0 | Allocated | +/// |---------------- |----------:|---------:|---------:|-------:|----------:| +/// | TestAddProperty | 11.35 ns | 0.135 ns | 0.119 ns | 0.0019 | 24 B | +/// | TestClone | 123.72 ns | 1.898 ns | 1.585 ns | 0.0503 | 632 B | +/// | TestParse | 240.81 ns | 2.974 ns | 2.322 ns | 0.0577 | 728 B | diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JPath.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JPath.cs new file mode 100644 index 0000000000..04949d40e8 --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JPath.cs @@ -0,0 +1,54 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmark_JPath.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; + +namespace Neo.Json.Benchmarks; + +[MemoryDiagnoser] +[CsvMeasurementsExporter] +[MarkdownExporter] +public class Benchmark_JPath +{ + private JObject _json = new(); + + [GlobalSetup] + public void Setup() + { + _json = new JObject + { + ["store"] = new JObject + { + ["book"] = new JArray + { + new JObject { ["title"] = "Book A", ["price"] = 10.99 }, + new JObject { ["title"] = "Book B", ["price"] = 15.50 } + } + } + }; + } + + [Benchmark] + public void TestJsonPathQuery() + { + _json.JsonPath("$.store.book[*].title"); + } +} + +/// BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605) +/// 13th Gen Intel Core i9-13900H, 1 CPU, 20 logical and 14 physical cores +/// .NET SDK 9.0.101 +/// [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 [AttachedDebugger] +/// DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 +/// +/// | Method | Mean | Error | StdDev | Gen0 | Allocated | +/// |------------------ |---------:|---------:|--------:|-------:|----------:| +/// | TestJsonPathQuery | 679.7 ns | 11.84 ns | 9.89 ns | 0.1869 | 2.3 KB | diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JString.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JString.cs new file mode 100644 index 0000000000..9133617170 --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JString.cs @@ -0,0 +1,58 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmark_JString.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; + +namespace Neo.Json.Benchmarks; + +[MemoryDiagnoser] +[CsvMeasurementsExporter] +[MarkdownExporter] +public class Benchmark_JString +{ + private JString _testString = new(string.Empty); + + [GlobalSetup] + public void Setup() + { + _testString = new JString("hello world"); + } + + [Benchmark] + public void TestLength() + { + _ = _testString.Value.Length; + } + + [Benchmark] + public void TestConversionToString() + { + _ = _testString.ToString(); + } + + [Benchmark] + public void TestClone() + { + _ = _testString.Clone(); + } +} + +///BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605) +///13th Gen Intel Core i9-13900H, 1 CPU, 20 logical and 14 physical cores +///.NET SDK 9.0.101 +/// [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 [AttachedDebugger] +/// DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 +/// +///| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +///|----------------------- |-----------:|----------:|----------:|-------:|-------:|----------:| +///| TestLength | 0.0050 ns | 0.0044 ns | 0.0041 ns | - | - | - | +///| TestConversionToString | 76.8631 ns | 1.0699 ns | 1.2737 ns | 0.0695 | 0.0001 | 872 B | +///| TestClone | 0.0233 ns | 0.0104 ns | 0.0087 ns | - | - | - | diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonArray.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonArray.cs new file mode 100644 index 0000000000..da09dc19d0 --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonArray.cs @@ -0,0 +1,217 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmark_JsonArray.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; + +namespace Neo.Json.Benchmarks; + +[MemoryDiagnoser] // 用于统计内存使用 +[CsvMeasurementsExporter] // CSV 格式导出 +[MarkdownExporter] // Markdown 格式导出 +public class Benchmark_JsonArray +{ + private JObject _alice = new(); + private JObject _bob = new(); + private JArray _jArray = new(); + + [GlobalSetup] + public void Setup() + { + _alice = new JObject + { + ["name"] = "alice", + ["age"] = 30, + ["score"] = 100.001, + ["gender"] = "female", + ["isMarried"] = true, + ["pet"] = new JObject + { + ["name"] = "Tom", + ["type"] = "cat" + } + }; + + _bob = new JObject + { + ["name"] = "bob", + ["age"] = 100000, + ["score"] = 0.001, + ["gender"] = "male", + ["isMarried"] = false, + ["pet"] = new JObject + { + ["name"] = "Paul", + ["type"] = "dog" + } + }; + + _jArray = new JArray(); + } + + [Benchmark] + public void TestAdd() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray.Add(_bob); + } + + [Benchmark] + public void TestSetItem() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray[0] = _bob; + } + + [Benchmark] + public void TestClear() + { + _jArray.Clear(); + } + + [Benchmark] + public void TestContains() + { + _jArray.Clear(); + _jArray.Add(_alice); + _ = _jArray.Contains(_alice); + } + + [Benchmark] + public void TestCopyTo() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray.Add(_bob); + + var objects = new JObject[2]; + _jArray.CopyTo(objects, 0); + } + + [Benchmark] + public void TestInsert() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray.Insert(0, _bob); + } + + [Benchmark] + public void TestIndexOf() + { + _jArray.Clear(); + _jArray.Add(_alice); + _ = _jArray.IndexOf(_alice); + } + + [Benchmark] + public void TestRemove() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray.Remove(_alice); + } + + [Benchmark] + public void TestRemoveAt() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray.Add(_bob); + _jArray.RemoveAt(1); + } + + [Benchmark] + public void TestGetEnumerator() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray.Add(_bob); + foreach (var _ in _jArray) + { + // Do nothing, just enumerate + } + } + + [Benchmark] + public void TestCount() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray.Add(_bob); + _ = _jArray.Count; + } + + [Benchmark] + public void TestClone() + { + _jArray.Clear(); + _jArray.Add(_alice); + _ = (JArray)_jArray.Clone(); + } + + [Benchmark] + public void TestAddNull() + { + _jArray.Clear(); + _jArray.Add(null); + } + + [Benchmark] + public void TestSetNull() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray[0] = null; + } + + [Benchmark] + public void TestInsertNull() + { + _jArray.Clear(); + _jArray.Add(_alice); + _jArray.Insert(0, null); + } + + [Benchmark] + public void TestRemoveNull() + { + _jArray.Clear(); + _jArray.Add(null); + _jArray.Remove(null); + } +} + +/// BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605) +/// 13th Gen Intel Core i9-13900H, 1 CPU, 20 logical and 14 physical cores +/// .NET SDK 9.0.101 +/// [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 [AttachedDebugger] +/// DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 +/// +/// | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +/// |------------------ |------------:|----------:|----------:|-------:|-------:|----------:| +/// | TestAdd | 10.8580 ns | 0.1532 ns | 0.1433 ns | - | - | - | +/// | TestSetItem | 10.8238 ns | 0.1747 ns | 0.1459 ns | - | - | - | +/// | TestClear | 0.0663 ns | 0.0139 ns | 0.0116 ns | - | - | - | +/// | TestContains | 9.3212 ns | 0.1277 ns | 0.1195 ns | - | - | - | +/// | TestCopyTo | 18.6370 ns | 0.3341 ns | 0.2790 ns | 0.0032 | - | 40 B | +/// | TestInsert | 12.3404 ns | 0.1256 ns | 0.1175 ns | - | - | - | +/// | TestIndexOf | 9.2549 ns | 0.1196 ns | 0.1119 ns | - | - | - | +/// | TestRemove | 8.6535 ns | 0.1912 ns | 0.2276 ns | - | - | - | +/// | TestRemoveAt | 11.2368 ns | 0.0703 ns | 0.0549 ns | - | - | - | +/// | TestGetEnumerator | 17.5149 ns | 0.1480 ns | 0.1384 ns | 0.0032 | - | 40 B | +/// | TestCount | 9.4478 ns | 0.1740 ns | 0.1627 ns | - | - | - | +/// | TestClone | 442.3215 ns | 6.7589 ns | 6.3223 ns | 0.1464 | 0.0005 | 1840 B | +/// | TestAddNull | 2.1299 ns | 0.0309 ns | 0.0289 ns | - | - | - | +/// | TestSetNull | 6.2627 ns | 0.0706 ns | 0.0661 ns | - | - | - | +/// | TestInsertNull | 8.9616 ns | 0.0868 ns | 0.0812 ns | - | - | - | +/// | TestRemoveNull | 5.2719 ns | 0.0489 ns | 0.0457 ns | - | - | - | diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonDeserialize.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonDeserialize.cs new file mode 100644 index 0000000000..0c9ac61e6a --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonDeserialize.cs @@ -0,0 +1,155 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmark_JsonDeserialize.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; +using Newtonsoft.Json; + +namespace Neo.Json.Benchmarks; + +[MemoryDiagnoser] // Enabling Memory Diagnostics +[CsvMeasurementsExporter] // Export results in CSV format +[MarkdownExporter] // Exporting results in Markdown format +public class Benchmark_JsonDeserialize +{ + private string _jsonString = string.Empty; + + [GlobalSetup] + public void Setup() + { + // Reading JSON files + _jsonString = File.ReadAllText("Data/RpcTestCases.json"); + } + + /// + /// Deserialization with Newtonsoft.Json + /// + [Benchmark] + public List? Newtonsoft_Deserialize() + { + return JsonConvert.DeserializeObject>(_jsonString); + } + + /// + /// Deserialization with Neo.Json (supports nested parsing) + /// + [Benchmark] + public List NeoJson_Deserialize() + { + // Parses into JArray + if (JToken.Parse(_jsonString) is not JArray neoJsonObject) + return []; + + var result = new List(); + + foreach (var item in neoJsonObject) + { + var testCase = new RpcTestCase + { + Name = item?["Name"]?.GetString(), + Request = new RpcRequest + { + JsonRpc = item?["Request"]?["jsonrpc"]?.GetString(), + Method = item?["Request"]?["method"]?.GetString(), + Params = ConvertToJTokenArray(item?["Request"]?["params"]), + Id = item?["Request"]?["id"]?.GetNumber() + }, + Response = new RpcResponse + { + JsonRpc = item?["Response"]?["jsonrpc"]?.GetString(), + Id = item?["Response"]?["id"]?.GetNumber(), + Result = item?["Response"]?["result"] + } + }; + result.Add(testCase); + } + return result; + } + + /// + /// Recursively parsing params and stack arrays + /// + private static List ParseParams(JToken? token) + { + var result = new List(); + + if (token is JArray array) + { + // Parsing JArray correctly with foreach + foreach (var item in array) + { + result.Add(ParseParams(item)); + } + } + else if (token is JObject obj) + { + // Properties traversal with Neo.Json.JObject + var dict = new Dictionary(); + foreach (var property in obj.Properties) + { + dict[property.Key] = property.Value?.GetString(); + } + result.Add(dict); + } + else + { + // If it's a normal value, it's straightforward to add + result.Add(token?.GetString()); + } + + return result; + } + + /// + /// Parses any type of JSON into a JToken[] (for nested structures) + /// + private static JToken[] ConvertToJTokenArray(JToken? token) + { + var result = new List(); + + if (token is JArray array) + { + // If it's a JArray, parse it one by one and add it to the result + foreach (var item in array) + { + result.AddRange(ConvertToJTokenArray(item)); + } + } + else if (token is JObject obj) + { + // Convert JObject to JToken (Dictionary type) + var newObj = new JObject(); + foreach (var property in obj.Properties) + newObj[property.Key] = property.Value as JString; + result.Add(newObj); + } + else + { + // Add the base type JToken directly + result.Add(token); + } + + return [.. result!]; // Converting a List to an Array of JTokens + } +} + +// This is benchmark after bug fix. + +// BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2894) +// Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores +// .NET SDK 9.0.102 +// [Host] : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2 +// DefaultJob: .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2 + + +// | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +// | ----------------------- |-----------:| ---------:| ---------:| ---------:| ---------:| --------:| ----------:| +// | Newtonsoft_Deserialize | 1,066.7 us | 19.89 us | 34.84 us | 158.2031 | 115.2344 | - | 978.52 KB | +// | NeoJson_Deserialize | 777.9 us | 11.27 us | 9.41 us | 144.5313 | 70.3125 | 35.1563 | 919.27 KB | diff --git a/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json b/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json new file mode 100644 index 0000000000..bcc82c91d9 --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json @@ -0,0 +1,4034 @@ +[ + { + "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": -6, + "updatecounter": 0, + "hash": "0xd2a4cff31913016155e38e474a2c06d08be276cf", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=", + "checksum": 2663858513 + }, + "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": 7, + "safe": true + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 14, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 21, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 28, + "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": [ -6 ], + "id": 1 + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "id": -6, + "updatecounter": 0, + "hash": "0xd2a4cff31913016155e38e474a2c06d08be276cf", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=", + "checksum": 2663858513 + }, + "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": 7, + "safe": true + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 14, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 21, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 28, + "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": -6, + "updatecounter": 0, + "hash": "0xd2a4cff31913016155e38e474a2c06d08be276cf", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=", + "checksum": 2663858513 + }, + "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": 7, + "safe": true + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 14, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 21, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 28, + "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": -5, + "updatecounter": 1, + "hash": "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=", + "checksum": 1325686241 + }, + "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": 7, + "safe": true + }, + { + "name": "getAccountState", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Array", + "offset": 14, + "safe": true + }, + { + "name": "getAllCandidates", + "parameters": [], + "returntype": "InteropInterface", + "offset": 21, + "safe": true + }, + { + "name": "getCandidateVote", + "parameters": [ + { + "name": "pubKey", + "type": "PublicKey" + } + ], + "returntype": "Integer", + "offset": 28, + "safe": true + }, + { + "name": "getCandidates", + "parameters": [], + "returntype": "Array", + "offset": 35, + "safe": true + }, + { + "name": "getCommittee", + "parameters": [], + "returntype": "Array", + "offset": 42, + "safe": true + }, + { + "name": "getCommitteeAddress", + "parameters": [], + "returntype": "Hash160", + "offset": 49, + "safe": true + }, + { + "name": "getGasPerBlock", + "parameters": [], + "returntype": "Integer", + "offset": 56, + "safe": true + }, + { + "name": "getNextBlockValidators", + "parameters": [], + "returntype": "Array", + "offset": 63, + "safe": true + }, + { + "name": "getRegisterPrice", + "parameters": [], + "returntype": "Integer", + "offset": 70, + "safe": true + }, + { + "name": "registerCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "PublicKey" + } + ], + "returntype": "Boolean", + "offset": 77, + "safe": false + }, + { + "name": "setGasPerBlock", + "parameters": [ + { + "name": "gasPerBlock", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 84, + "safe": false + }, + { + "name": "setRegisterPrice", + "parameters": [ + { + "name": "registerPrice", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 91, + "safe": false + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 98, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 105, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 112, + "safe": false + }, + { + "name": "unclaimedGas", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "end", + "type": "Integer" + } + ], + "returntype": "Integer", + "offset": 119, + "safe": true + }, + { + "name": "unregisterCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "PublicKey" + } + ], + "returntype": "Boolean", + "offset": 126, + "safe": false + }, + { + "name": "vote", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "voteTo", + "type": "PublicKey" + } + ], + "returntype": "Boolean", + "offset": 133, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + }, + { + "name": "CandidateStateChanged", + "parameters": [ + { + "name": "pubkey", + "type": "PublicKey" + }, + { + "name": "registered", + "type": "Boolean" + }, + { + "name": "votes", + "type": "Integer" + } + ] + }, + { + "name": "Vote", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "from", + "type": "PublicKey" + }, + { + "name": "to", + "type": "PublicKey" + }, + { + "name": "amount", + "type": "Integer" + } + ] + }, + { + "name": "CommitteeChanged", + "parameters": [ + { + "name": "old", + "type": "Array" + }, + { + "name": "new", + "type": "Array" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": null + } + } + } + }, + { + "Name": "getcontractstateasync", + "Request": { + "jsonrpc": "2.0", + "id": 1, + "method": "getcontractstate", + "params": [ -5 ] + }, + "Response": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "id": -5, + "updatecounter": 1, + "hash": "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=", + "checksum": 1325686241 + }, + "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": 7, + "safe": true + }, + { + "name": "getAccountState", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Array", + "offset": 14, + "safe": true + }, + { + "name": "getAllCandidates", + "parameters": [], + "returntype": "InteropInterface", + "offset": 21, + "safe": true + }, + { + "name": "getCandidateVote", + "parameters": [ + { + "name": "pubKey", + "type": "PublicKey" + } + ], + "returntype": "Integer", + "offset": 28, + "safe": true + }, + { + "name": "getCandidates", + "parameters": [], + "returntype": "Array", + "offset": 35, + "safe": true + }, + { + "name": "getCommittee", + "parameters": [], + "returntype": "Array", + "offset": 42, + "safe": true + }, + { + "name": "getCommitteeAddress", + "parameters": [], + "returntype": "Hash160", + "offset": 49, + "safe": true + }, + { + "name": "getGasPerBlock", + "parameters": [], + "returntype": "Integer", + "offset": 56, + "safe": true + }, + { + "name": "getNextBlockValidators", + "parameters": [], + "returntype": "Array", + "offset": 63, + "safe": true + }, + { + "name": "getRegisterPrice", + "parameters": [], + "returntype": "Integer", + "offset": 70, + "safe": true + }, + { + "name": "registerCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "PublicKey" + } + ], + "returntype": "Boolean", + "offset": 77, + "safe": false + }, + { + "name": "setGasPerBlock", + "parameters": [ + { + "name": "gasPerBlock", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 84, + "safe": false + }, + { + "name": "setRegisterPrice", + "parameters": [ + { + "name": "registerPrice", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 91, + "safe": false + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 98, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 105, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 112, + "safe": false + }, + { + "name": "unclaimedGas", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "end", + "type": "Integer" + } + ], + "returntype": "Integer", + "offset": 119, + "safe": true + }, + { + "name": "unregisterCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "PublicKey" + } + ], + "returntype": "Boolean", + "offset": 126, + "safe": false + }, + { + "name": "vote", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "voteTo", + "type": "PublicKey" + } + ], + "returntype": "Boolean", + "offset": 133, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + }, + { + "name": "CandidateStateChanged", + "parameters": [ + { + "name": "pubkey", + "type": "PublicKey" + }, + { + "name": "registered", + "type": "Boolean" + }, + { + "name": "votes", + "type": "Integer" + } + ] + }, + { + "name": "Vote", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "from", + "type": "PublicKey" + }, + { + "name": "to", + "type": "PublicKey" + }, + { + "name": "amount", + "type": "Integer" + } + ] + }, + { + "name": "CommitteeChanged", + "parameters": [ + { + "name": "old", + "type": "Array" + }, + { + "name": "new", + "type": "Array" + } + ] + } + ] + }, + "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": -5, + "updatecounter": 1, + "hash": "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", + "nef": { + "magic": 860243278, + "compiler": "neo-core-v3.0", + "source": "", + "tokens": [], + "script": "EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=", + "checksum": 1325686241 + }, + "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": 7, + "safe": true + }, + { + "name": "getAccountState", + "parameters": [ + { + "name": "account", + "type": "Hash160" + } + ], + "returntype": "Array", + "offset": 14, + "safe": true + }, + { + "name": "getAllCandidates", + "parameters": [], + "returntype": "InteropInterface", + "offset": 21, + "safe": true + }, + { + "name": "getCandidateVote", + "parameters": [ + { + "name": "pubKey", + "type": "PublicKey" + } + ], + "returntype": "Integer", + "offset": 28, + "safe": true + }, + { + "name": "getCandidates", + "parameters": [], + "returntype": "Array", + "offset": 35, + "safe": true + }, + { + "name": "getCommittee", + "parameters": [], + "returntype": "Array", + "offset": 42, + "safe": true + }, + { + "name": "getCommitteeAddress", + "parameters": [], + "returntype": "Hash160", + "offset": 49, + "safe": true + }, + { + "name": "getGasPerBlock", + "parameters": [], + "returntype": "Integer", + "offset": 56, + "safe": true + }, + { + "name": "getNextBlockValidators", + "parameters": [], + "returntype": "Array", + "offset": 63, + "safe": true + }, + { + "name": "getRegisterPrice", + "parameters": [], + "returntype": "Integer", + "offset": 70, + "safe": true + }, + { + "name": "registerCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "PublicKey" + } + ], + "returntype": "Boolean", + "offset": 77, + "safe": false + }, + { + "name": "setGasPerBlock", + "parameters": [ + { + "name": "gasPerBlock", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 84, + "safe": false + }, + { + "name": "setRegisterPrice", + "parameters": [ + { + "name": "registerPrice", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 91, + "safe": false + }, + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 98, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 105, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 112, + "safe": false + }, + { + "name": "unclaimedGas", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "end", + "type": "Integer" + } + ], + "returntype": "Integer", + "offset": 119, + "safe": true + }, + { + "name": "unregisterCandidate", + "parameters": [ + { + "name": "pubkey", + "type": "PublicKey" + } + ], + "returntype": "Boolean", + "offset": 126, + "safe": false + }, + { + "name": "vote", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "voteTo", + "type": "PublicKey" + } + ], + "returntype": "Boolean", + "offset": 133, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + }, + { + "name": "CandidateStateChanged", + "parameters": [ + { + "name": "pubkey", + "type": "PublicKey" + }, + { + "name": "registered", + "type": "Boolean" + }, + { + "name": "votes", + "type": "Integer" + } + ] + }, + { + "name": "Vote", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "from", + "type": "PublicKey" + }, + { + "name": "to", + "type": "PublicKey" + }, + { + "name": "amount", + "type": "Integer" + } + ] + }, + { + "name": "CommitteeChanged", + "parameters": [ + { + "name": "old", + "type": "Array" + }, + { + "name": "new", + "type": "Array" + } + ] + } + ] + }, + "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 + } + ], + "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" + ] + } + } + } + }, + { + "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": [], + "signers": [ + { + "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": [], + "signers": [ + { + "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": [], + "signers": [ + { + "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/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj b/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj new file mode 100644 index 0000000000..80c11fde9e --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + + Always + + + + diff --git a/benchmarks/Neo.Json.Benchmarks/Program.cs b/benchmarks/Neo.Json.Benchmarks/Program.cs new file mode 100644 index 0000000000..16255a17ee --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/Program.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Program.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Running; + +// List all benchmarks: +// dotnet run -c Release --framework [for example: net9.0] -- --list flat(or tree) +// Run a specific benchmark: +// dotnet run -c Release --framework [for example: net9.0] -- -f [benchmark name] +// Run all benchmarks: +// dotnet run -c Release --framework [for example: net9.0] -- -f * +// Run all benchmarks of a class: +// dotnet run -c Release --framework [for example: net9.0] -- -f '*Class*' +// More options: https://benchmarkdotnet.org/articles/guides/console-args.html +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/benchmarks/Neo.Json.Benchmarks/RpcTestCase.cs b/benchmarks/Neo.Json.Benchmarks/RpcTestCase.cs new file mode 100644 index 0000000000..70ff6967d1 --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/RpcTestCase.cs @@ -0,0 +1,55 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// RpcTestCase.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Json.Benchmarks; + +public class RpcTestCase +{ + public string? Name { get; set; } + + public RpcRequest? Request { get; set; } + + public RpcResponse? Response { get; set; } +} + +public class RpcRequest +{ + public JToken? Id { get; set; } + + public string? JsonRpc { get; set; } + + public string? Method { get; set; } + + public JToken[]? Params { get; set; } +} + +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 class RpcResponseError +{ + public int Code { get; set; } + + public string? Message { get; set; } + + public JToken? Data { get; set; } +} diff --git a/benchmarks/Neo.Json.Benchmarks/RpcTestCaseN.cs b/benchmarks/Neo.Json.Benchmarks/RpcTestCaseN.cs new file mode 100644 index 0000000000..757213b672 --- /dev/null +++ b/benchmarks/Neo.Json.Benchmarks/RpcTestCaseN.cs @@ -0,0 +1,55 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// RpcTestCaseN.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Json.Benchmarks; + +public class RpcTestCaseN +{ + public string? Name { get; set; } + + public RpcRequestN? Request { get; set; } + + public RpcResponseN? Response { get; set; } +} + +public class RpcRequestN +{ + public Newtonsoft.Json.Linq.JToken? Id { get; set; } + + public string? JsonRpc { get; set; } + + public string? Method { get; set; } + + public Newtonsoft.Json.Linq.JToken[]? Params { get; set; } +} + +public class RpcResponseN +{ + public Newtonsoft.Json.Linq.JToken? Id { get; set; } + + public string? JsonRpc { get; set; } + + public RpcResponseErrorN? Error { get; set; } + + public Newtonsoft.Json.Linq.JToken? Result { get; set; } + + public string? RawResponse { get; set; } + +} + +public class RpcResponseErrorN +{ + public int Code { get; set; } + + public string? Message { get; set; } + + public Newtonsoft.Json.Linq.JToken? Data { get; set; } +} diff --git a/docs/handlers.md b/docs/handlers.md new file mode 100644 index 0000000000..43d864b209 --- /dev/null +++ b/docs/handlers.md @@ -0,0 +1,73 @@ +## Neo Core Events + +### 1. Block Committing Event + +**Event Name** `Committing` + +**Handler Interface:** `ICommittingHandler` + +This event is triggered when a transaction is in the process of being committed to the blockchain. Implementing the `ICommittingHandler` interface allows you to define custom actions that should be executed during this phase. + +### 2. Block Committed Event + +**Event Name** `Committed` + +**Handler Interface:** `ICommittedHandler` + +This event occurs after a transaction has been successfully committed to the blockchain. By implementing the `ICommittedHandler` interface, you can specify custom actions to be performed after the transaction is committed. + +### 3. Logging Event +**Event Name** `Logging` + +**Handler Interface:** `ILoggingHandler` + +This event is related to logging within the blockchain system. Implement the `ILoggingHandler` interface to define custom logging behaviors for different events and actions that occur in the blockchain. + +### 4. General Log Event +**Event Name** `Log` + +**Handler Interface:** `ILogHandler` + +This event pertains to general logging activities. The `ILogHandler` interface allows you to handle logging for specific actions or errors within the blockchain system. + +### 5. Notification Event +**Event Name** `Notify` + +**Handler Interface:** `INotifyHandler` + +This event is triggered when a notification needs to be sent. By implementing the `INotifyHandler` interface, you can specify custom actions for sending notifications when certain events occur within the blockchain system. + +### 6. Service Added Event +**Event Name** `ServiceAdded` + +**Handler Interface:** `IServiceAddedHandler` + +This event occurs when a new service is added to the blockchain system. Implement the `IServiceAddedHandler` interface to define custom actions that should be executed when a new service is added. + +### 7. Transaction Added Event +**Event Name** `TransactionAdded` + +**Handler Interface:** `ITransactionAddedHandler` + +This event is triggered when a new transaction is added to the blockchain system. By implementing the `ITransactionAddedHandler` interface, you can specify custom actions to be performed when a new transaction is added. + +### 8. Transaction Removed Event +**Event Name** `TransactionRemoved` + +**Handler Interface:** `ITransactionRemovedHandler` + +This event occurs when a transaction is removed from the blockchain system. Implement the `ITransactionRemovedHandler` interface to define custom actions that should be taken when a transaction is removed. + +### 9. Wallet Changed Event +**Event Name** `WalletChanged` + +**Handler Interface:** `IWalletChangedHandler` + +This event is triggered when changes occur in the wallet, such as balance updates or new transactions. By implementing the `IWalletChangedHandler` interface, you can specify custom actions to be taken when there are changes in the wallet. + +### 10. Remote Node MessageReceived Event +**Event Name** `MessageReceived` + +**Handler Interface:** `IMessageReceivedHandler` + +This event is triggered when a new message is received from a peer remote node. diff --git a/docs/persistence-architecture.md b/docs/persistence-architecture.md new file mode 100644 index 0000000000..153ebee0d9 --- /dev/null +++ b/docs/persistence-architecture.md @@ -0,0 +1,122 @@ +# Neo Persistence System - Class Relationships + +## Interface Hierarchy + +``` +IDisposable + │ + ├── IReadOnlyStore + ├── IWriteStore + └── IStoreProvider + │ + ▼ + IStore ◄─── IStoreSnapshot +``` + +## Class Structure + +``` +StoreFactory + │ + ├── MemoryStoreProvider ──creates──> MemoryStore + ├── LevelDBStore (Plugin) ──creates──> LevelDBStore + └── RocksDBStore (Plugin) ──creates──> RocksDBStore + │ + ▼ + IStoreSnapshot + │ + ▼ + Cache Layer + │ + ┌─────────┼─────────┐ + │ │ │ + DataCache StoreCache ClonedCache +``` + +## Interface Definitions + +### IStore +```csharp +public interface IStore : IReadOnlyStore, IWriteStore, IDisposable +{ + IStoreSnapshot GetSnapshot(); +} +``` + +### IStoreSnapshot +```csharp +public interface IStoreSnapshot : IReadOnlyStore, IWriteStore, IDisposable +{ + IStore Store { get; } + void Commit(); +} +``` + +### IReadOnlyStore +```csharp +public interface IReadOnlyStore where TKey : class? +{ + TValue this[TKey key] { get; } + bool TryGet(TKey key, out TValue? value); + bool Contains(TKey key); + IEnumerable<(TKey Key, TValue Value)> Find(TKey? key_prefix = null, SeekDirection direction = SeekDirection.Forward); +} +``` + +### IWriteStore +```csharp +public interface IWriteStore +{ + void Delete(TKey key); + void Put(TKey key, TValue value); + void PutSync(TKey key, TValue value) => Put(key, value); +} +``` + +### IStoreProvider +```csharp +public interface IStoreProvider +{ + string Name { get; } + IStore GetStore(string path); +} +``` + +## Core Classes + +### StoreFactory +- Static registry for storage providers +- Manages provider registration and discovery +- Creates store instances + +## Cache System + +### Why Three Cache Classes? + +The Neo persistence system uses three cache classes to separate different responsibilities: + +1. **DataCache** - Provides common caching infrastructure and change tracking +2. **StoreCache** - Connects cache to actual storage (database/memory) +3. **ClonedCache** - Creates isolated copies to prevent data corruption + +### Relationships + +``` +DataCache (Abstract) + │ + ├── StoreCache ──connects to──> IStore/IStoreSnapshot + └── ClonedCache ──wraps──> Any DataCache +``` + +### When to Use Each + +**StoreCache**: +- Direct access to storage +- When you need to read/write to database +- Base layer for other caches + +**ClonedCache**: +- When you need isolated data manipulation +- Preventing accidental mutations between components +- Creating temporary working environments +- Smart contract execution (isolated from main state) diff --git a/docs/serialization-format.md b/docs/serialization-format.md new file mode 100644 index 0000000000..6cbf0f8c95 --- /dev/null +++ b/docs/serialization-format.md @@ -0,0 +1,311 @@ +# Neo Serialization Format + +This document describes the binary serialization format used by the Neo blockchain platform. The format is designed for efficient serialization and deserialization of blockchain data structures. + +## Overview + +Neo uses a custom binary serialization format that supports: +- Primitive data types (integers, booleans, bytes) +- Variable-length integers (VarInt) +- Strings (fixed and variable length) +- Arrays and collections +- Custom serializable objects +- Nullable objects + +## Core Interfaces + +### ISerializable + +All serializable objects in Neo implement the `ISerializable` interface: + +```csharp +public interface ISerializable +{ + int Size { get; } + + void Serialize(BinaryWriter writer); + + void Deserialize(ref MemoryReader reader); +} +``` + +- `Size`: Returns the serialized size in bytes +- `Serialize`: Writes the object to a BinaryWriter +- `Deserialize`: Reads the object from a MemoryReader + +## Primitive Data Types + +### Integers + +Neo supports both little-endian and big-endian integer formats: + +| Type | Size | Endianness | Description | +|------|------|------------|-------------| +| `sbyte` | 1 byte | N/A | Signed 8-bit integer | +| `byte` | 1 byte | N/A | Unsigned 8-bit integer | +| `short` | 2 bytes | Little-endian | Signed 16-bit integer | +| `ushort` | 2 bytes | Little-endian | Unsigned 16-bit integer | +| `int` | 4 bytes | Little-endian | Signed 32-bit integer | +| `uint` | 4 bytes | Little-endian | Unsigned 32-bit integer | +| `long` | 8 bytes | Little-endian | Signed 64-bit integer | +| `ulong` | 8 bytes | Little-endian | Unsigned 64-bit integer | + +Big-endian variants are available for `short`, `ushort`, `int`, `uint`, `long`, and `ulong`. + +### Boolean + +Booleans are serialized as single bytes: +- `false` → `0x00` +- `true` → `0x01` +- Any other value throws `FormatException` + +### Variable-Length Integers (VarInt) + +Neo uses a compact variable-length integer format: + +| Value Range | Format | Size | +|-------------|--------|------| +| 0-252 | Direct value | 1 byte | +| 253-65535 | `0xFD` + 2-byte little-endian | 3 bytes | +| 65536-4294967295 | `0xFE` + 4-byte little-endian | 5 bytes | +| 4294967296+ | `0xFF` + 8-byte little-endian | 9 bytes | + +**Serialization:** +```csharp +if (value < 0xFD) +{ + writer.Write((byte)value); +} +else if (value <= 0xFFFF) +{ + writer.Write((byte)0xFD); + writer.Write((ushort)value); +} +else if (value <= 0xFFFFFFFF) +{ + writer.Write((byte)0xFE); + writer.Write((uint)value); +} +else +{ + writer.Write((byte)0xFF); + writer.Write(value); +} +``` + +**Deserialization:** +```csharp +var b = ReadByte(); +var value = b switch +{ + 0xfd => ReadUInt16(), + 0xfe => ReadUInt32(), + 0xff => ReadUInt64(), + _ => b +}; +``` + +## Strings + +### Fixed-Length Strings + +Fixed-length strings are padded with null bytes: + +**Format:** `[UTF-8 bytes][zero padding]` + +**Serialization:** +```csharp +var bytes = value.ToStrictUtf8Bytes(); +if (bytes.Length > length) + throw new ArgumentException(); +writer.Write(bytes); +if (bytes.Length < length) + writer.Write(new byte[length - bytes.Length]); +``` + +**Deserialization:** +```csharp +var end = currentOffset + length; +var offset = currentOffset; +while (offset < end && _span[offset] != 0) offset++; +var data = _span[currentOffset..offset]; +for (; offset < end; offset++) + if (_span[offset] != 0) + throw new FormatException(); +currentOffset = end; +return data.ToStrictUtf8String(); +``` + +### Variable-Length Strings + +Variable-length strings use VarInt for length prefix: + +**Format:** `[VarInt length][UTF-8 bytes]` + +**Serialization:** +```csharp +writer.WriteVarInt(value.Length); +writer.Write(value.ToStrictUtf8Bytes()); +``` + +**Deserialization:** +```csharp +var length = (int)ReadVarInt((ulong)max); +EnsurePosition(length); +var data = _span.Slice(currentOffset, length); +currentOffset += length; +return data.ToStrictUtf8String(); +``` + +## Byte Arrays + +### Fixed-Length Byte Arrays + +**Format:** `[raw bytes]` + +### Variable-Length Byte Arrays + +**Format:** `[VarInt length][raw bytes]` + +**Serialization:** +```csharp +writer.WriteVarInt(value.Length); +writer.Write(value); +``` + +**Deserialization:** +```csharp +return ReadMemory((int)ReadVarInt((ulong)max)); +``` + +## Collections + +### Serializable Arrays + +**Format:** `[VarInt count][item1][item2]...[itemN]` + +**Serialization:** +```csharp +writer.WriteVarInt(value.Count); +foreach (T item in value) +{ + item.Serialize(writer); +} +``` + +**Deserialization:** +```csharp +var array = new T[reader.ReadVarInt((ulong)max)]; +for (var i = 0; i < array.Length; i++) +{ + array[i] = new T(); + array[i].Deserialize(ref reader); +} +return array; +``` + +### Nullable Arrays + +**Format:** `[VarInt count][bool1][item1?][bool2][item2?]...[boolN][itemN?]` + +**Serialization:** +```csharp +writer.WriteVarInt(value.Length); +foreach (var item in value) +{ + var isNull = item is null; + writer.Write(!isNull); + if (isNull) continue; + item!.Serialize(writer); +} +``` + +**Deserialization:** +```csharp +var array = new T[reader.ReadVarInt((ulong)max)]; +for (var i = 0; i < array.Length; i++) + array[i] = reader.ReadBoolean() ? reader.ReadSerializable() : null; +return array; +``` + +## UTF-8 Encoding + +Neo uses strict UTF-8 encoding with the following characteristics: + +- **Strict Mode**: Invalid UTF-8 sequences throw exceptions +- **No Fallback**: No replacement characters for invalid sequences +- **Exception Handling**: Detailed error messages for debugging + +**String to Bytes:** +```csharp +public static byte[] ToStrictUtf8Bytes(this string value) +{ + return StrictUTF8.GetBytes(value); +} +``` + +**Bytes to String:** +```csharp +public static string ToStrictUtf8String(this ReadOnlySpan value) +{ + return StrictUTF8.GetString(value); +} +``` + +## Error Handling + +The serialization format includes comprehensive error handling: + +- **FormatException**: Invalid data format or corrupted data +- **ArgumentNullException**: Null values where not allowed +- **ArgumentException**: Invalid arguments (e.g., string too long) +- **ArgumentOutOfRangeException**: Values outside allowed ranges +- **DecoderFallbackException**: Invalid UTF-8 sequences +- **EncoderFallbackException**: Characters that cannot be encoded + +## Examples + +### Simple Object Serialization + +```csharp +public class SimpleData : ISerializable +{ + public string Name { get; set; } + public int Value { get; set; } + + public int Size => Name.GetStrictUtf8ByteCount() + sizeof(int); + + public void Serialize(BinaryWriter writer) + { + writer.WriteVarString(Name); + writer.Write(Value); + } + + public void Deserialize(ref MemoryReader reader) + { + Name = reader.ReadVarString(); + Value = reader.ReadInt32(); + } +} +``` + +### Array Serialization + +```csharp +public class DataArray : ISerializable +{ + public SimpleData[] Items { get; set; } + + public int Size => Items.Sum(item => item.Size) + GetVarSize(Items.Length); + + public void Serialize(BinaryWriter writer) + { + writer.Write(Items); + } + + public void Deserialize(ref MemoryReader reader) + { + Items = reader.ReadSerializableArray(); + } +} +``` diff --git a/neo.UnitTests/TestBlockchain.cs b/neo.UnitTests/TestBlockchain.cs deleted file mode 100644 index 3e7e222b46..0000000000 --- a/neo.UnitTests/TestBlockchain.cs +++ /dev/null @@ -1,161 +0,0 @@ -using Neo.Core; -using Neo.Cryptography.ECC; -using Neo.IO.Caching; -using System; -using System.Collections.Generic; - -namespace Neo.UnitTests -{ - public class TestBlockchain : Blockchain - { - private UInt256 _assetId; - - /// - /// Return true if haven't got valid handle - /// - public override bool IsDisposed => false; - - public TestBlockchain(UInt256 assetId) - { - _assetId = assetId; - } - - public override UInt256 CurrentBlockHash => throw new NotImplementedException(); - - public override UInt256 CurrentHeaderHash => throw new NotImplementedException(); - - public override uint HeaderHeight => throw new NotImplementedException(); - - public override uint Height => throw new NotImplementedException(); - - public override bool AddBlock(Block block) - { - throw new NotImplementedException(); - } - - public override bool ContainsBlock(UInt256 hash) - { - return true; // for verify in UT_Block - } - - public override bool ContainsTransaction(UInt256 hash) - { - throw new NotImplementedException(); - } - - public override bool ContainsUnspent(UInt256 hash, ushort index) - { - throw new NotImplementedException(); - } - - public override MetaDataCache GetMetaData() - { - return new TestMetaDataCache(); - } - - public override DataCache GetStates() - { - return new TestDataCache(); - } - - public override void Dispose() - { - // do nothing - } - - public override AccountState GetAccountState(UInt160 script_hash) - { - throw new NotImplementedException(); - } - - public override AssetState GetAssetState(UInt256 asset_id) - { - if (asset_id == UInt256.Zero) return null; - UInt160 val = new UInt160(TestUtils.GetByteArray(20, asset_id.ToArray()[0])); - return new AssetState() { Issuer = val }; - } - - public override Block GetBlock(UInt256 hash) - { - throw new NotImplementedException(); - } - - public override UInt256 GetBlockHash(uint height) - { - throw new NotImplementedException(); - } - - public override ContractState GetContract(UInt160 hash) - { - throw new NotImplementedException(); - } - - public override IEnumerable GetEnrollments() - { - ECPoint ecp = TestUtils.StandbyValidators[0]; - return new ValidatorState[] { new ValidatorState() { PublicKey = ecp } }; - } - - public override Header GetHeader(uint height) - { - throw new NotImplementedException(); - } - - public override Header GetHeader(UInt256 hash) - { - throw new NotImplementedException(); - } - - public override Block GetNextBlock(UInt256 hash) - { - throw new NotImplementedException(); - } - - public override UInt256 GetNextBlockHash(UInt256 hash) - { - throw new NotImplementedException(); - } - - public override StorageItem GetStorageItem(StorageKey key) - { - throw new NotImplementedException(); - } - - public override long GetSysFeeAmount(UInt256 hash) - { - throw new NotImplementedException(); - } - - public override Transaction GetTransaction(UInt256 hash, out int height) - { - height = 0; - // take part of the trans hash and use that for the scripthash of the testtransaction - return new TestTransaction(_assetId, TransactionType.ClaimTransaction, new UInt160(TestUtils.GetByteArray(20, hash.ToArray()[0]))); - } - - public override Dictionary GetUnclaimed(UInt256 hash) - { - throw new NotImplementedException(); - } - - public override TransactionOutput GetUnspent(UInt256 hash, ushort index) - { - throw new NotImplementedException(); - } - - public override IEnumerable GetUnspent(UInt256 hash) - { - throw new NotImplementedException(); - } - - public override bool IsDoubleSpend(Transaction tx) - { - throw new NotImplementedException(); - } - - protected override void AddHeaders(IEnumerable
headers) - { - throw new NotImplementedException(); - } - } -} diff --git a/neo.UnitTests/TestDataCache.cs b/neo.UnitTests/TestDataCache.cs deleted file mode 100644 index d2d3afdabf..0000000000 --- a/neo.UnitTests/TestDataCache.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Neo.IO; -using Neo.IO.Caching; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Neo.UnitTests -{ - public class TestDataCache : DataCache - where TKey : IEquatable, ISerializable - where TValue : class, ICloneable, ISerializable, new() - { - public override void DeleteInternal(TKey key) - { - } - - protected override void AddInternal(TKey key, TValue value) - { - } - - protected override IEnumerable> FindInternal(byte[] key_prefix) - { - return Enumerable.Empty>(); - } - - protected override TValue GetInternal(TKey key) - { - throw new NotImplementedException(); - } - - protected override TValue TryGetInternal(TKey key) - { - return null; - } - - protected override void UpdateInternal(TKey key, TValue value) - { - } - } -} diff --git a/neo.UnitTests/TestMetaDataCache.cs b/neo.UnitTests/TestMetaDataCache.cs deleted file mode 100644 index b274b25c0a..0000000000 --- a/neo.UnitTests/TestMetaDataCache.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Neo.IO; -using Neo.IO.Caching; - -namespace Neo.UnitTests -{ - public class TestMetaDataCache : MetaDataCache where T : class, ISerializable, new() - { - public TestMetaDataCache() - : base(null) - { - } - - protected override T TryGetInternal() - { - return null; - } - } -} diff --git a/neo.UnitTests/TestTransaction.cs b/neo.UnitTests/TestTransaction.cs deleted file mode 100644 index 6036a8d558..0000000000 --- a/neo.UnitTests/TestTransaction.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Neo.Core; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Neo.UnitTests -{ - public class TestTransaction : Transaction - { - public TestTransaction(UInt256 assetId, TransactionType type, UInt160 scriptHash) : base(type) - { - TransactionOutput transVal = new TransactionOutput(); - transVal.Value = Fixed8.FromDecimal(50); - transVal.AssetId = assetId; - transVal.ScriptHash = scriptHash; - base.Outputs = new TransactionOutput[1] { transVal }; - } - } -} diff --git a/neo.UnitTests/TestUtils.cs b/neo.UnitTests/TestUtils.cs deleted file mode 100644 index ba3c595870..0000000000 --- a/neo.UnitTests/TestUtils.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using Neo.Core; -using Neo.Cryptography.ECC; -using Neo.VM; -using Neo.Wallets; -using Neo.SmartContract; - -namespace Neo.UnitTests -{ - public static class TestUtils - { - public static byte[] GetByteArray(int length, byte firstByte) - { - byte[] array = new byte[length]; - array[0] = firstByte; - for (int i = 1; i < length; i++) - { - array[i] = 0x20; - } - return array; - } - - public static readonly ECPoint[] StandbyValidators = new ECPoint[] { ECPoint.DecodePoint("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c".HexToBytes(), ECCurve.Secp256r1) }; - - public static ClaimTransaction GetClaimTransaction() - { - return new ClaimTransaction - { - Claims = new CoinReference[0], - Attributes = new TransactionAttribute[0], - Inputs = new CoinReference[0], - Outputs = new TransactionOutput[0], - Scripts = new Witness[0] - }; - } - - public static MinerTransaction GetMinerTransaction() - { - return new MinerTransaction - { - Nonce = 2083236893, - Attributes = new TransactionAttribute[0], - Inputs = new CoinReference[0], - Outputs = new TransactionOutput[0], - Scripts = new Witness[0] - }; - } - - public static IssueTransaction GetIssueTransaction(bool inputVal, decimal outputVal, UInt256 assetId) - { - TestUtils.SetupTestBlockchain(assetId); - - CoinReference[] inputsVal; - if (inputVal) - { - inputsVal = new[] - { - TestUtils.GetCoinReference(null) - }; - } - else - { - inputsVal = new CoinReference[0]; - } - - return new IssueTransaction - { - Attributes = new TransactionAttribute[0], - Inputs = inputsVal, - Outputs = new[] - { - new TransactionOutput - { - AssetId = assetId, - Value = Fixed8.FromDecimal(outputVal), - ScriptHash = Contract.CreateMultiSigRedeemScript(1, TestUtils.StandbyValidators).ToScriptHash() - } - }, - Scripts = new[] - { - new Witness - { - InvocationScript = new byte[0], - VerificationScript = new[] { (byte)OpCode.PUSHT } - } - } - }; - } - - public static CoinReference GetCoinReference(UInt256 prevHash) - { - if (prevHash == null) prevHash = UInt256.Zero; - return new CoinReference - { - PrevHash = prevHash, - PrevIndex = 0 - }; - } - - public static void SetupTestBlockchain(UInt256 assetId) - { - Blockchain testBlockchain = new TestBlockchain(assetId); - Blockchain.RegisterBlockchain(testBlockchain); - } - - public static void SetupHeaderWithValues(Header header, UInt256 val256, out UInt256 merkRootVal, out UInt160 val160, out uint timestampVal, out uint indexVal, out ulong consensusDataVal, out Witness scriptVal) - { - setupBlockBaseWithValues(header, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal); - } - - public static void SetupBlockWithValues(Block block, UInt256 val256, out UInt256 merkRootVal, out UInt160 val160, out uint timestampVal, out uint indexVal, out ulong consensusDataVal, out Witness scriptVal, out Transaction[] transactionsVal, int numberOfTransactions) - { - setupBlockBaseWithValues(block, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal); - - transactionsVal = new Transaction[numberOfTransactions]; - if (numberOfTransactions > 0) - { - for (int i = 0; i < numberOfTransactions; i++) - { - transactionsVal[i] = TestUtils.GetMinerTransaction(); - } - } - - block.Transactions = transactionsVal; - } - - private static void setupBlockBaseWithValues(BlockBase bb, UInt256 val256, out UInt256 merkRootVal, out UInt160 val160, out uint timestampVal, out uint indexVal, out ulong consensusDataVal, out Witness scriptVal) - { - bb.PrevHash = val256; - merkRootVal = new UInt256(new byte[] { 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251 }); - bb.MerkleRoot = merkRootVal; - timestampVal = new DateTime(1968, 06, 01, 0, 0, 0, DateTimeKind.Utc).ToTimestamp(); - bb.Timestamp = timestampVal; - indexVal = 0; - bb.Index = indexVal; - consensusDataVal = 30; - bb.ConsensusData = consensusDataVal; - val160 = UInt160.Zero; - bb.NextConsensus = val160; - scriptVal = new Witness - { - InvocationScript = new byte[0], - VerificationScript = new[] { (byte)OpCode.PUSHT } - }; - bb.Script = scriptVal; - } - } -} diff --git a/neo.UnitTests/TestVerifiable.cs b/neo.UnitTests/TestVerifiable.cs deleted file mode 100644 index e02ce3b9ac..0000000000 --- a/neo.UnitTests/TestVerifiable.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.IO; -using Neo.Core; - -namespace Neo.UnitTests -{ - public class TestVerifiable : IVerifiable - { - private string testStr = "testStr"; - - public Witness[] Scripts { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public int Size => throw new NotImplementedException(); - - public void Deserialize(BinaryReader reader) - { - throw new NotImplementedException(); - } - - public void DeserializeUnsigned(BinaryReader reader) - { - throw new NotImplementedException(); - } - - public byte[] GetMessage() - { - throw new NotImplementedException(); - } - - public UInt160[] GetScriptHashesForVerifying() - { - throw new NotImplementedException(); - } - - public void Serialize(BinaryWriter writer) - { - throw new NotImplementedException(); - } - - public void SerializeUnsigned(BinaryWriter writer) - { - writer.Write((string) testStr); - } - } -} \ No newline at end of file diff --git a/neo.UnitTests/UT_AccountState.cs b/neo.UnitTests/UT_AccountState.cs deleted file mode 100644 index 9e18dfef87..0000000000 --- a/neo.UnitTests/UT_AccountState.cs +++ /dev/null @@ -1,273 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.Cryptography.ECC; -using Neo.IO; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_AccountState - { - AccountState uut; - - [TestInitialize] - public void TestSetup() - { - uut = new AccountState(); - } - - [TestMethod] - public void ScriptHash_Get() - { - uut.ScriptHash.Should().BeNull(); - } - - [TestMethod] - public void ScriptHash_Set() - { - UInt160 val = new UInt160(); - uut.ScriptHash = val; - uut.ScriptHash.Should().Be(val); - } - - [TestMethod] - public void IsFrozen_Get() - { - uut.IsFrozen.Should().Be(false); - } - - [TestMethod] - public void IsFrozen_Set() - { - uut.IsFrozen = true; - uut.IsFrozen.Should().Be(true); - } - - [TestMethod] - public void Votes_Get() - { - uut.Votes.Should().BeNull(); - } - - [TestMethod] - public void Votes_Set() - { - ECPoint val = new ECPoint(); - ECPoint[] array = new ECPoint[] { val }; - uut.Votes = array; - uut.Votes[0].Should().Be(val); - } - - [TestMethod] - public void Balances_Get() - { - uut.Balances.Should().BeNull(); - } - - [TestMethod] - public void Balances_Set() - { - UInt256 key = new UInt256(); - Fixed8 val = new Fixed8(); - Dictionary dict = new Dictionary(); - dict.Add(key, val); - uut.Balances = dict; - uut.Balances[key].Should().Be(val); - } - - [TestMethod] - public void Size_Get_0_Votes_0_Balances() - { - UInt160 val = new UInt160(); - ECPoint[] array = new ECPoint[0]; - Dictionary dict = new Dictionary(); - - uut.ScriptHash = val; - uut.Votes = array; - uut.Balances = dict; - - uut.Size.Should().Be(24); // 1 + 20 + 1 + 1 + 1 + 0 * (32 + 8) - } - - [TestMethod] - public void Size_Get_1_Vote_0_Balances() - { - UInt160 val = new UInt160(); - ECPoint[] array = new ECPoint[] { new ECPoint() }; - Dictionary dict = new Dictionary(); - - uut.ScriptHash = val; - uut.Votes = array; - uut.Balances = dict; - - uut.Size.Should().Be(25); // 1 + 20 + 1 + 2 + 1 + 0 * (32 + 8) - } - - [TestMethod] - public void Size_Get_5_Votes_0_Balances() - { - UInt160 val = new UInt160(); - ECPoint[] array = new ECPoint[] { new ECPoint(), new ECPoint(), new ECPoint(), new ECPoint(), new ECPoint() }; - Dictionary dict = new Dictionary(); - - uut.ScriptHash = val; - uut.Votes = array; - uut.Balances = dict; - - uut.Size.Should().Be(29); // 1 + 20 + 1 + 6 + 1 + 0 * (32 + 8) - } - - [TestMethod] - public void Size_Get_0_Votes_1_Balance() - { - UInt160 val = new UInt160(); - ECPoint[] array = new ECPoint[0]; - Dictionary dict = new Dictionary(); - dict.Add(new UInt256(), new Fixed8()); - - uut.ScriptHash = val; - uut.Votes = array; - uut.Balances = dict; - - uut.Size.Should().Be(64); // 1 + 20 + 1 + 1 + 1 + 1 * (32 + 8) - } - - [TestMethod] - public void Size_Get_0_Votes_5_Balance() - { - UInt160 val = new UInt160(); - ECPoint[] array = new ECPoint[0]; - Dictionary dict = new Dictionary(); - dict.Add(new UInt256(), new Fixed8()); - dict.Add(new UInt256(TestUtils.GetByteArray(32, 0x20)), new Fixed8()); - dict.Add(new UInt256(TestUtils.GetByteArray(32, 0x21)), new Fixed8()); - dict.Add(new UInt256(TestUtils.GetByteArray(32, 0x22)), new Fixed8()); - dict.Add(new UInt256(TestUtils.GetByteArray(32, 0x23)), new Fixed8()); - - uut.ScriptHash = val; - uut.Votes = array; - uut.Balances = dict; - - uut.Size.Should().Be(224); // 1 + 20 + 1 + 1 + 1 + 5 * (32 + 8) - } - - [TestMethod] - public void Size_Get_5_Votes_5_Balance() - { - UInt160 val = new UInt160(); - ECPoint[] array = new ECPoint[] { new ECPoint(), new ECPoint(), new ECPoint(), new ECPoint(), new ECPoint() }; - Dictionary dict = new Dictionary(); - dict.Add(new UInt256(), new Fixed8()); - dict.Add(new UInt256(TestUtils.GetByteArray(32, 0x20)), new Fixed8()); - dict.Add(new UInt256(TestUtils.GetByteArray(32, 0x21)), new Fixed8()); - dict.Add(new UInt256(TestUtils.GetByteArray(32, 0x22)), new Fixed8()); - dict.Add(new UInt256(TestUtils.GetByteArray(32, 0x23)), new Fixed8()); - - uut.ScriptHash = val; - uut.Votes = array; - uut.Balances = dict; - - uut.Size.Should().Be(229); // 1 + 20 + 1 + 6 + 1 + 5 * (32 + 8) - } - - private void setupAccountStateWithValues(AccountState accState, out UInt160 scriptHashVal, out ECPoint votesVal, out UInt256 key, out Fixed8 dictVal) - { - scriptHashVal = new UInt160(); - accState.ScriptHash = scriptHashVal; - accState.IsFrozen = true; - votesVal = new ECPoint(); - ECPoint[] array = new ECPoint[] { votesVal }; - accState.Votes = array; - key = new UInt256(); - dictVal = new Fixed8(); - Dictionary dict = new Dictionary(); - dict.Add(key, dictVal); - accState.Balances = dict; - } - - [TestMethod] - public void Clone() - { - UInt160 scriptHashVal; - ECPoint votesVal; - UInt256 key; - Fixed8 dictVal; - setupAccountStateWithValues(uut, out scriptHashVal, out votesVal, out key, out dictVal); - - AccountState newAs = ((ICloneable)uut).Clone(); - newAs.ScriptHash.Should().Be(scriptHashVal); - newAs.IsFrozen.Should().Be(true); - newAs.Votes[0].Should().Be(votesVal); - newAs.Balances.Count.Should().Be(1); - newAs.Balances.Should().ContainKey(key); - newAs.Balances[key].Should().Be(dictVal); - } - - [TestMethod] - public void FromReplica() - { - AccountState accState = new AccountState(); - UInt160 scriptHashVal; - ECPoint votesVal; - UInt256 key; - Fixed8 dictVal; - setupAccountStateWithValues(accState, out scriptHashVal, out votesVal, out key, out dictVal); - - ((ICloneable)uut).FromReplica(accState); - uut.ScriptHash.Should().Be(scriptHashVal); - uut.IsFrozen.Should().Be(true); - uut.Votes[0].Should().Be(votesVal); - uut.Balances.Count.Should().Be(1); - uut.Balances.Should().ContainKey(key); - uut.Balances[key].Should().Be(dictVal); - } - - [TestMethod] - public void Deserialize() - { - byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0 }; - int index = 0; - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - { - using (BinaryReader reader = new BinaryReader(ms)) - { - uut.Deserialize(reader); - } - } - uut.IsFrozen.Should().Be(true); - } - - - [TestMethod] - public void Serialize() - { - UInt160 scriptHashVal; - ECPoint votesVal; - UInt256 key; - Fixed8 dictVal; - setupAccountStateWithValues(uut, out scriptHashVal, out votesVal, out key, out dictVal); - - byte[] data; - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - uut.Serialize(writer); - data = stream.ToArray(); - } - } - - byte[] requiredData = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0 }; - - data.Length.Should().Be(25); - for (int i=0; i<25; i++) - { - data[i].Should().Be(requiredData[i]); - } - } - } -} diff --git a/neo.UnitTests/UT_AssetState.cs b/neo.UnitTests/UT_AssetState.cs deleted file mode 100644 index 7dd1bbab95..0000000000 --- a/neo.UnitTests/UT_AssetState.cs +++ /dev/null @@ -1,518 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.Cryptography.ECC; -using Neo.IO; -using System.Globalization; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_AssetState - { - AssetState uut; - - [TestInitialize] - public void TestSetup() - { - uut = new AssetState(); - } - - [TestMethod] - public void AssetId_Get() - { - uut.AssetId.Should().BeNull(); - } - - [TestMethod] - public void AssetId_Set() - { - UInt256 val = new UInt256(); - uut.AssetId = val; - uut.AssetId.Should().Be(val); - } - - [TestMethod] - public void AssetType_Get() - { - uut.AssetType.Should().Be(AssetType.GoverningToken); // Uninitialised AssetType defaults to this enum value, be careful - } - - [TestMethod] - public void AssetType_Set() - { - AssetType val = new AssetType(); - uut.AssetType = val; - uut.AssetType.Should().Be(val); - } - - [TestMethod] - public void Name_Get() - { - uut.Name.Should().BeNull(); - } - - [TestMethod] - public void Name_Set() - { - string val = "wake up neo"; - uut.Name = val; - uut.Name.Should().Be(val); - } - - [TestMethod] - public void Amount_Get() - { - uut.Amount.Should().Be(new Fixed8(0)); // defaults to 0 - } - - [TestMethod] - public void Amount_Set() - { - Fixed8 val = new Fixed8(); - uut.Amount = val; - uut.Amount.Should().Be(val); - } - - [TestMethod] - public void Available_Get() - { - uut.Available.Should().Be(new Fixed8(0)); // defaults to 0 - } - - [TestMethod] - public void Available_Set() - { - Fixed8 val = new Fixed8(); - uut.Available = val; - uut.Available.Should().Be(val); - } - - [TestMethod] - public void Precision_Get() - { - uut.Precision.Should().Be(0x00); // defaults to 0 - } - - [TestMethod] - public void Precision_Set() - { - byte val = 0x42; - uut.Precision = val; - uut.Precision.Should().Be(val); - } - - [TestMethod] - public void FeeMode_Get() - { - AssetState.FeeMode.Should().Be(0); - } - - [TestMethod] - public void Fee_Get() - { - uut.Fee.Should().Be(new Fixed8(0)); // defaults to 0 - } - - [TestMethod] - public void Fee_Set() - { - Fixed8 val = new Fixed8(); - uut.Fee = val; - uut.Fee.Should().Be(val); - } - - [TestMethod] - public void FeeAddress_Get() - { - uut.FeeAddress.Should().BeNull(); - } - - [TestMethod] - public void FeeAddress_Set() - { - UInt160 val = new UInt160(); - uut.FeeAddress = val; - uut.FeeAddress.Should().Be(val); - } - - [TestMethod] - public void Owner_Get() - { - uut.Owner.Should().BeNull(); - } - - [TestMethod] - public void Owner_Set() - { - ECPoint val = new ECPoint(); - uut.Owner = val; - uut.Owner.Should().Be(val); - } - - [TestMethod] - public void Admin_Get() - { - uut.Admin.Should().BeNull(); - } - - [TestMethod] - public void Admin_Set() - { - UInt160 val = new UInt160(); - uut.Admin = val; - uut.Admin.Should().Be(val); - } - - [TestMethod] - public void Issuer_Get() - { - uut.Issuer.Should().BeNull(); - } - - [TestMethod] - public void Issuer_Set() - { - UInt160 val = new UInt160(); - uut.Issuer = val; - uut.Issuer.Should().Be(val); - } - - [TestMethod] - public void Expiration_Get() - { - uut.Expiration.Should().Be(0u); - } - - [TestMethod] - public void Expiration_Set() - { - uint val = 42; - uut.Expiration = val; - uut.Expiration.Should().Be(val); - } - - [TestMethod] - public void IsFrozen_Get() - { - uut.IsFrozen.Should().Be(false); - } - - [TestMethod] - public void IsFrozen_Set() - { - uut.IsFrozen = true; - uut.IsFrozen.Should().Be(true); - } - - private void setupAssetStateWithValues(AssetState assetState, out UInt256 assetId, out AssetType assetType, out string name, out Fixed8 amount, out Fixed8 available, out byte precision, out Fixed8 fee, out UInt160 feeAddress, out ECPoint owner, out UInt160 admin, out UInt160 issuer, out uint expiration, out bool isFrozen) - { - assetId = new UInt256(TestUtils.GetByteArray(32, 0x20)); - assetState.AssetId = assetId; - assetType = AssetType.Token; - assetState.AssetType = assetType; - - name = "neo"; - assetState.Name = name; - - amount = new Fixed8(42); - assetState.Amount = amount; - - available = new Fixed8(43); - assetState.Available = available; - - precision = 0x42; - assetState.Precision = precision; - - fee = new Fixed8(44); - assetState.Fee = fee; - - feeAddress = new UInt160(TestUtils.GetByteArray(20, 0x21)); - assetState.FeeAddress = feeAddress; - - owner = ECPoint.DecodePoint(TestUtils.GetByteArray(1,0x00), ECCurve.Secp256r1); - assetState.Owner = owner; - - admin = new UInt160(TestUtils.GetByteArray(20, 0x22)); - assetState.Admin = admin; - - issuer = new UInt160(TestUtils.GetByteArray(20, 0x23)); - assetState.Issuer = issuer; - - expiration = 42u; - assetState.Expiration = expiration; - - isFrozen = true; - assetState.IsFrozen = isFrozen; - } - - [TestMethod] - public void Size_Get_Default() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(uut, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - - uut.Size.Should().Be(130); // 1 + 32 + 1 + 4 + 8 + 8 + 1 + 1 + 8 + 20 + 1 + 20 + 20 + 4 + 1 - } - - [TestMethod] - public void Clone() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(uut, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - - AssetState newAs = ((ICloneable)uut).Clone(); - - newAs.AssetId.Should().Be(assetId); - newAs.AssetType.Should().Be(assetType); - newAs.Name.Should().Be(name); - newAs.Amount.Should().Be(amount); - newAs.Available.Should().Be(available); - newAs.Precision.Should().Be(precision); - newAs.Fee.Should().Be(fee); - newAs.FeeAddress.Should().Be(feeAddress); - newAs.Owner.Should().Be(owner); - newAs.Admin.Should().Be(admin); - newAs.Issuer.Should().Be(issuer); - newAs.Expiration.Should().Be(expiration); - newAs.IsFrozen.Should().Be(isFrozen); - } - - [TestMethod] - public void FromReplica() - { - AssetState assetState = new AssetState(); - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(assetState, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - - - ((ICloneable)uut).FromReplica(assetState); - uut.AssetId.Should().Be(assetId); - uut.AssetType.Should().Be(assetType); - uut.Name.Should().Be(name); - uut.Amount.Should().Be(amount); - uut.Available.Should().Be(available); - uut.Precision.Should().Be(precision); - uut.Fee.Should().Be(fee); - uut.FeeAddress.Should().Be(feeAddress); - uut.Owner.Should().Be(owner); - uut.Admin.Should().Be(admin); - uut.Issuer.Should().Be(issuer); - uut.Expiration.Should().Be(expiration); - uut.IsFrozen.Should().Be(isFrozen); - } - - [TestMethod] - public void Deserialize() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(new AssetState(), out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - - byte[] data = new byte[] { 0, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 96, 3, 110, 101, 111, 42, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 66, 0, 44, 0, 0, 0, 0, 0, 0, 0, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 34, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 35, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 42, 0, 0, 0, 1 }; - int index = 0; - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - { - using (BinaryReader reader = new BinaryReader(ms)) - { - uut.Deserialize(reader); - } - } - - uut.AssetId.Should().Be(assetId); - uut.AssetType.Should().Be(assetType); - uut.Name.Should().Be(name); - uut.Amount.Should().Be(amount); - uut.Available.Should().Be(available); - uut.Precision.Should().Be(precision); - uut.Fee.Should().Be(fee); - uut.FeeAddress.Should().Be(feeAddress); - uut.Owner.Should().Be(owner); - uut.Admin.Should().Be(admin); - uut.Issuer.Should().Be(issuer); - uut.Expiration.Should().Be(expiration); - uut.IsFrozen.Should().Be(isFrozen); - } - - - [TestMethod] - public void Serialize() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(uut, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - - byte[] data; - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - uut.Serialize(writer); - data = stream.ToArray(); - } - } - - byte[] requiredData = new byte[] { 0, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 96, 3, 110, 101, 111, 42, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, 66, 0, 44, 0, 0, 0, 0, 0, 0, 0, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 34, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 35, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 42, 0, 0, 0, 1 }; - data.Length.Should().Be(130); - for (int i = 0; i < 130; i++) - { - data[i].Should().Be(requiredData[i]); - } - } - - [TestMethod] - public void GetName() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(uut, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - - uut.GetName().Should().Be("neo"); - // The base class GetName() method should be be optimised to avoid the slow try / catch - } - - [TestMethod] - public void GetName_Culture() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(uut, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - uut.Name = "[{\"lang\":\"zh-CN\",\"name\":\"小蚁股\"},{\"lang\":\"en\",\"name\":\"Neo\"}]"; - - uut.GetName(new CultureInfo("zh-CN")).Should().Be("小蚁股"); - uut.GetName(new CultureInfo("en")).Should().Be("Neo"); - } - - [TestMethod] - public void GetName_Culture_En() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(uut, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - uut.Name = "[{\"lang\":\"zh-CN\",\"name\":\"小蚁股\"},{\"lang\":\"en\",\"name\":\"Neo\"}]"; - - CultureInfo.CurrentCulture = new CultureInfo("en"); - uut.GetName().Should().Be("Neo"); - } - - [TestMethod] - public void GetName_Culture_Cn() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(uut, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - uut.Name = "[{\"lang\":\"zh-CN\",\"name\":\"小蚁股\"},{\"lang\":\"en\",\"name\":\"Neo\"}]"; - - CultureInfo.CurrentCulture = new CultureInfo("zh-CN"); - uut.GetName().Should().Be("小蚁股"); - } - - [TestMethod] - public void GetName_Culture_Unknown() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(uut, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - uut.Name = "[{\"lang\":\"zh-CN\",\"name\":\"小蚁股\"},{\"lang\":\"en\",\"name\":\"Neo\"}]"; - - CultureInfo.CurrentCulture = new CultureInfo("de-DE"); - uut.GetName().Should().Be("Neo"); // defaults to english IF english is in the name - } - - [TestMethod] - public void GetName_Culture_Unknown_NoEn() - { - UInt256 assetId; - AssetType assetType; - string name; - Fixed8 amount, available, fee; - byte precision; - UInt160 feeAddress, admin, issuer; - ECPoint owner; - uint expiration; - bool isFrozen; - setupAssetStateWithValues(uut, out assetId, out assetType, out name, out amount, out available, out precision, out fee, out feeAddress, out owner, out admin, out issuer, out expiration, out isFrozen); - uut.Name = "[{\"lang\":\"zh-CN\",\"name\":\"小蚁股\"},{\"lang\":\"foo\",\"name\":\"bar\"}]"; - - CultureInfo.CurrentCulture = new CultureInfo("de-DE"); - uut.GetName().Should().Be("小蚁股"); // defaults to first name IF english is not in the name - } - - - } -} diff --git a/neo.UnitTests/UT_Block.cs b/neo.UnitTests/UT_Block.cs deleted file mode 100644 index fce6367bab..0000000000 --- a/neo.UnitTests/UT_Block.cs +++ /dev/null @@ -1,640 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO.Json; -using Neo.SmartContract; -using Neo.VM; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_Block - { - Block uut; - - [TestInitialize] - public void TestSetup() - { - uut = new Block(); - } - - [TestMethod] - public void Transactions_Get() - { - uut.Transactions.Should().BeNull(); - } - - [TestMethod] - public void Transactions_Set() - { - Transaction[] val = new Transaction[10]; - uut.Transactions = val; - uut.Transactions.Length.Should().Be(10); - } - - - - [TestMethod] - public void Header_Get() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Header.Should().NotBeNull(); - uut.Header.PrevHash.Should().Be(val256); - uut.Header.MerkleRoot.Should().Be(merkRootVal); - uut.Header.Timestamp.Should().Be(timestampVal); - uut.Header.Index.Should().Be(indexVal); - uut.Header.ConsensusData.Should().Be(consensusDataVal); - uut.Header.Script.Should().Be(scriptVal); - } - - [TestMethod] - public void Size_Get() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - // blockbase 4 + 32 + 32 + 4 + 4 + 8 + 20 + 1 + 3 - // block 1 - uut.Size.Should().Be(109); - } - - private IssueTransaction getIssueTransaction(bool inputVal, decimal outputVal, UInt256 assetId) - { - TestUtils.SetupTestBlockchain(assetId); - - CoinReference[] inputsVal; - if (inputVal) - { - inputsVal = new[] - { - TestUtils.GetCoinReference(null) - }; - } - else - { - inputsVal = new CoinReference[0]; - } - - - return new IssueTransaction - { - Attributes = new TransactionAttribute[0], - Inputs = inputsVal, - Outputs = new[] - { - new TransactionOutput - { - AssetId = assetId, - Value = Fixed8.FromDecimal(outputVal), - ScriptHash = Contract.CreateMultiSigRedeemScript(1, TestUtils.StandbyValidators).ToScriptHash() - } - }, - Scripts = new[] - { - new Witness - { - InvocationScript = new byte[0], - VerificationScript = new[] { (byte)OpCode.PUSHT } - } - } - }; - } - - private ContractTransaction getContractTransaction(bool inputVal, decimal outputVal, UInt256 assetId) - { - TestUtils.SetupTestBlockchain(assetId); - - CoinReference[] inputsVal; - if (inputVal) - { - inputsVal = new[] - { - TestUtils.GetCoinReference(null) - }; - } - else - { - inputsVal = new CoinReference[0]; - } - - return new ContractTransaction - { - Attributes = new TransactionAttribute[0], - Inputs = inputsVal, - Outputs = new[] - { - new TransactionOutput - { - AssetId = assetId, - Value = Fixed8.FromDecimal(outputVal), - ScriptHash = Contract.CreateMultiSigRedeemScript(1, TestUtils.StandbyValidators).ToScriptHash() - } - }, - Scripts = new[] - { - new Witness - { - InvocationScript = new byte[0], - VerificationScript = new[] { (byte)OpCode.PUSHT } - } - } - }; - } - - - - [TestMethod] - public void Size_Get_1_Transaction() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Transactions = new Transaction[1] { - TestUtils.GetMinerTransaction() - }; - - // blockbase 4 + 32 + 32 + 4 + 4 + 8 + 20 + 1 + 3 - // block 11 - uut.Size.Should().Be(119); - } - - [TestMethod] - public void Size_Get_3_Transaction() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Transactions = new Transaction[3] { - TestUtils.GetMinerTransaction(), - TestUtils.GetMinerTransaction(), - TestUtils.GetMinerTransaction() - }; - - // blockbase 4 + 32 + 32 + 4 + 4 + 8 + 20 + 1 + 3 - // block 31 - uut.Size.Should().Be(139); - } - - [TestMethod] - public void CalculateNetFee_EmptyTransactions() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - Block.CalculateNetFee(uut.Transactions).Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void CalculateNetFee_Ignores_MinerTransactions() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Transactions = new Transaction[1] { - TestUtils.GetMinerTransaction() - }; - - Block.CalculateNetFee(uut.Transactions).Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void CalculateNetFee_Ignores_ClaimTransactions() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Transactions = new Transaction[1] { - TestUtils.GetClaimTransaction() - }; - - Block.CalculateNetFee(uut.Transactions).Should().Be(Fixed8.Zero); - } - - - [TestMethod] - public void CalculateNetFee_Out() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Transactions = new Transaction[1] { - getContractTransaction(false, 100, Blockchain.UtilityToken.Hash) - }; - - Block.CalculateNetFee(uut.Transactions).Should().Be(Fixed8.FromDecimal(-100)); - } - - [TestMethod] - public void CalculateNetFee_In() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Transactions = new Transaction[1] { - getContractTransaction(true, 0, Blockchain.UtilityToken.Hash) - }; - - Block.CalculateNetFee(uut.Transactions).Should().Be(Fixed8.FromDecimal(50)); - } - - [TestMethod] - public void CalculateNetFee_In_And_Out() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Transactions = new Transaction[1] { - getContractTransaction(true, 100, Blockchain.UtilityToken.Hash) - }; - - Block.CalculateNetFee(uut.Transactions).Should().Be(Fixed8.FromDecimal(-50)); - } - - [TestMethod] - public void CalculateNetFee_SystemFee() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Transactions = new Transaction[1] { - TestUtils.GetIssueTransaction(true, 0, new UInt256(TestUtils.GetByteArray(32, 0x42))) - }; - - Block.CalculateNetFee(uut.Transactions).Should().Be(Fixed8.FromDecimal(-500)); - } - - [TestMethod] - public void Serialize() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - - byte[] data; - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - uut.Serialize(writer); - data = stream.ToArray(); - } - } - - byte[] requiredData = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 128, 171, 4, 253, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 81, 1, 0, 0, 29, 172, 43, 124, 0, 0, 0, 0 }; - - data.Length.Should().Be(119); - for (int i = 0; i < 119; i++) - { - data[i].Should().Be(requiredData[i]); - } - } - - [TestMethod] - public void Deserialize() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(new Block(), val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - - uut.MerkleRoot = merkRoot; // need to set for deserialise to be valid - - byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 128, 171, 4, 253, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 81, 1, 0, 0, 29, 172, 43, 124, 0, 0, 0, 0 }; - int index = 0; - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - { - using (BinaryReader reader = new BinaryReader(ms)) - { - uut.Deserialize(reader); - } - } - - assertStandardBlockTestVals(val256, merkRoot, val160, timestampVal, indexVal, consensusDataVal, scriptVal, transactionsVal); - } - - private void assertStandardBlockTestVals(UInt256 val256, UInt256 merkRoot, UInt160 val160, uint timestampVal, uint indexVal, ulong consensusDataVal, Witness scriptVal, Transaction[] transactionsVal, bool testTransactions = true) - { - uut.PrevHash.Should().Be(val256); - uut.MerkleRoot.Should().Be(merkRoot); - uut.Timestamp.Should().Be(timestampVal); - uut.Index.Should().Be(indexVal); - uut.ConsensusData.Should().Be(consensusDataVal); - uut.NextConsensus.Should().Be(val160); - uut.Script.InvocationScript.Length.Should().Be(0); - uut.Script.Size.Should().Be(scriptVal.Size); - uut.Script.VerificationScript[0].Should().Be(scriptVal.VerificationScript[0]); - if (testTransactions) - { - uut.Transactions.Length.Should().Be(1); - uut.Transactions[0].Should().Be(transactionsVal[0]); - } - } - - [TestMethod] - public void Equals_SameObj() - { - uut.Equals(uut).Should().BeTrue(); - } - - [TestMethod] - public void Equals_DiffObj() - { - Block newBlock = new Block(); - UInt256 val256 = UInt256.Zero; - UInt256 prevHash = new UInt256(TestUtils.GetByteArray(32, 0x42)); - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(newBlock, val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - TestUtils.SetupBlockWithValues(uut, prevHash, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 0); - - uut.Equals(newBlock).Should().BeFalse(); - } - - [TestMethod] - public void Equals_Null() - { - uut.Equals(null).Should().BeFalse(); - } - - [TestMethod] - public void Equals_SameHash() - { - - Block newBlock = new Block(); - UInt256 prevHash = new UInt256(TestUtils.GetByteArray(32, 0x42)); - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(newBlock, prevHash, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - TestUtils.SetupBlockWithValues(uut, prevHash, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - - uut.Equals(newBlock).Should().BeTrue(); - } - - [TestMethod] - public void Trim() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - - byte[] data = uut.Trim(); - byte[] requiredData = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 128, 171, 4, 253, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 81, 1, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251 }; - - data.Length.Should().Be(141); - for (int i = 0; i < 141; i++) - { - data[i].Should().Be(requiredData[i]); - } - } - - [TestMethod] - public void FromTrimmedData() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(new Block(), val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - - byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 128, 171, 4, 253, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 81, 1, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251 }; - - uut = Block.FromTrimmedData(data, 0, x => TestUtils.GetMinerTransaction()); - - assertStandardBlockTestVals(val256, merkRoot, val160, timestampVal, indexVal, consensusDataVal, scriptVal, transactionsVal); - uut.Transactions[0].Should().Be(TestUtils.GetMinerTransaction()); - } - - [TestMethod] - public void FromTrimmedData_MultipleTx() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(new Block(), val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 3); - - byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 128, 171, 4, 253, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 81, 3, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251 }; - - uut = Block.FromTrimmedData(data, 0, x => TestUtils.GetMinerTransaction()); - - assertStandardBlockTestVals(val256, merkRoot, val160, timestampVal, indexVal, consensusDataVal, scriptVal, transactionsVal, testTransactions: false); - uut.Transactions.Length.Should().Be(3); - uut.Transactions[0].Should().Be(TestUtils.GetMinerTransaction()); - uut.Transactions[1].Should().Be(TestUtils.GetMinerTransaction()); - uut.Transactions[2].Should().Be(TestUtils.GetMinerTransaction()); - } - - [TestMethod] - public void RebuildMerkleRoot_Updates() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - - UInt256 merkleRoot = uut.MerkleRoot; - - TestUtils.SetupBlockWithValues(uut, val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 3); - uut.RebuildMerkleRoot(); - - uut.MerkleRoot.Should().NotBe(merkleRoot); - } - - [TestMethod] - public void ToJson() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - - JObject jObj = uut.ToJson(); - jObj.Should().NotBeNull(); - jObj["hash"].AsString().Should().Be("0x4520462a8c80056291f871da523bff0eb17e29d44ab4317e69ff7a42083cb39d"); - jObj["size"].AsNumber().Should().Be(119); - jObj["version"].AsNumber().Should().Be(0); - jObj["previousblockhash"].AsString().Should().Be("0x0000000000000000000000000000000000000000000000000000000000000000"); - jObj["merkleroot"].AsString().Should().Be("0xfb5bd72b2d6792d75dc2f1084ffa9e9f70ca85543c717a6b13d9959b452a57d6"); - jObj["time"].AsNumber().Should().Be(4244941696); - jObj["index"].AsNumber().Should().Be(0); - jObj["nonce"].AsString().Should().Be("000000000000001e"); - jObj["nextconsensus"].AsString().Should().Be("AFmseVrdL9f9oyCzZefL9tG6UbvhPbdYzM"); - - JObject scObj = jObj["script"]; - scObj["invocation"].AsString().Should().Be(""); - scObj["verification"].AsString().Should().Be("51"); - - jObj["tx"].Should().NotBeNull(); - JArray txObj = (JArray)jObj["tx"]; - txObj[0]["txid"].AsString().Should().Be("0xfb5bd72b2d6792d75dc2f1084ffa9e9f70ca85543c717a6b13d9959b452a57d6"); - txObj[0]["size"].AsNumber().Should().Be(10); - txObj[0]["type"].AsString().Should().Be("MinerTransaction"); - txObj[0]["version"].AsNumber().Should().Be(0); - ((JArray)txObj[0]["attributes"]).Count.Should().Be(0); - ((JArray)txObj[0]["vin"]).Count.Should().Be(0); - ((JArray)txObj[0]["vout"]).Count.Should().Be(0); - txObj[0]["sys_fee"].AsString().Should().Be("0"); - txObj[0]["net_fee"].AsString().Should().Be("0"); - ((JArray)txObj[0]["scripts"]).Count.Should().Be(0); - txObj[0]["nonce"].AsNumber().Should().Be(2083236893); - } - - [TestMethod] - public void Verify_CompletelyFalse() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - - TestUtils.SetupTestBlockchain(UInt256.Zero); - - uut.Verify(false).Should().BeTrue(); - } - - [TestMethod] - public void Verify_CompletelyFalse_MinerTransaction_After_First() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 3); - - TestUtils.SetupTestBlockchain(UInt256.Zero); - - uut.Verify(false).Should().BeFalse(); - } - - [TestMethod] - public void Verify_CompletelyTrue_NextConsensus_Fail() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - Transaction[] transactionsVal; - TestUtils.SetupBlockWithValues(uut, val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal, out transactionsVal, 1); - // passing NextConsensus below - // uut.NextConsensus = new UInt160(new byte[] { 23, 52, 98, 203, 0, 206, 138, 37, 140, 16, 251, 231, 61, 120, 218, 200, 182, 125, 120, 73 }); - - TestUtils.SetupTestBlockchain(UInt256.Zero); - - uut.Verify(true).Should().BeFalse(); - } - } -} diff --git a/neo.UnitTests/UT_ClaimTransaction.cs b/neo.UnitTests/UT_ClaimTransaction.cs deleted file mode 100644 index 969b166abb..0000000000 --- a/neo.UnitTests/UT_ClaimTransaction.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO.Json; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_ClaimTransaction - { - ClaimTransaction uut; - - [TestInitialize] - public void TestSetup() - { - uut = new ClaimTransaction(); - } - - [TestMethod] - public void Claims_Get() - { - uut.Claims.Should().BeNull(); - } - - [TestMethod] - public void Claims_Set() - { - CoinReference val = new CoinReference(); - CoinReference[] refs = new CoinReference[] { val }; - uut.Claims = refs; - uut.Claims.Length.Should().Be(1); - uut.Claims[0].Should().Be(val); - } - - [TestMethod] - public void NetworkFee_Get() - { - uut.NetworkFee.Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void Size__Get_0_Claims() - { - CoinReference[] refs = new CoinReference[0]; - uut.Claims = refs; - - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - uut.Size.Should().Be(7); // 1, 1, 1, 1, 1, 1 + claims 1 - } - - [TestMethod] - public void Size__Get_1_Claims() - { - CoinReference[] refs = new[] { TestUtils.GetCoinReference(null) }; - uut.Claims = refs; - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - uut.Size.Should().Be(41); // 1, 1, 1, 1, 1, 1 + claims 35 - } - - [TestMethod] - public void Size__Get_3_Claims() - { - CoinReference[] refs = new[] { TestUtils.GetCoinReference(null), TestUtils.GetCoinReference(null), TestUtils.GetCoinReference(null) }; - uut.Claims = refs; - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - uut.Size.Should().Be(109); // 1, 1, 1, 1, 1, 1 + claims 103 - } - - [TestMethod] - public void GetScriptHashesForVerifying_0_Claims() - { - uut.Claims = new CoinReference[0]; - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - uut.GetScriptHashesForVerifying().Length.Should().Be(0); - } - - [TestMethod] - public void GetScriptHashesForVerifying_1_Claim() - { - CoinReference[] refs = new[] { TestUtils.GetCoinReference(new UInt256(TestUtils.GetByteArray(32, 0x42))) }; - uut.Claims = refs; - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - TestUtils.SetupTestBlockchain(UInt256.Zero); - - UInt160[] res = uut.GetScriptHashesForVerifying(); - res.Length.Should().Be(1); - } - - - [TestMethod] - public void GetScriptHashesForVerifying_2_Claim() - { - CoinReference[] refs = new[] { TestUtils.GetCoinReference(new UInt256(TestUtils.GetByteArray(32, 0x42))), TestUtils.GetCoinReference(new UInt256(TestUtils.GetByteArray(32, 0x48))) }; - uut.Claims = refs; - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - TestUtils.SetupTestBlockchain(UInt256.Zero); - - UInt160[] res = uut.GetScriptHashesForVerifying(); - res.Length.Should().Be(2); - } - - [TestMethod] - public void ToJson() - { - CoinReference[] refs = new[] { TestUtils.GetCoinReference(new UInt256(TestUtils.GetByteArray(32, 0x42))) }; - uut.Claims = refs; - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - JObject jObj = uut.ToJson(); - jObj.Should().NotBeNull(); - jObj["txid"].AsString().Should().Be("0x45a5c537ca95d62add6b331cad0fd742ce29ada02c50ea7e4d709f83563972b9"); - jObj["size"].AsNumber().Should().Be(41); - jObj["type"].AsString().Should().Be("ClaimTransaction"); - jObj["version"].AsNumber().Should().Be(0); - ((JArray)jObj["attributes"]).Count.Should().Be(0); - ((JArray)jObj["vin"]).Count.Should().Be(0); - ((JArray)jObj["vout"]).Count.Should().Be(0); - jObj["sys_fee"].AsString().Should().Be("0"); - jObj["net_fee"].AsString().Should().Be("0"); - ((JArray)jObj["scripts"]).Count.Should().Be(0); - - JArray claims = (JArray) jObj["claims"]; - claims.Count.Should().Be(1); - claims[0]["txid"].AsString().Should().Be("0x2020202020202020202020202020202020202020202020202020202020202042"); - claims[0]["vout"].AsNumber().Should().Be(0); - } - - } -} diff --git a/neo.UnitTests/UT_CoinReference.cs b/neo.UnitTests/UT_CoinReference.cs deleted file mode 100644 index e850285542..0000000000 --- a/neo.UnitTests/UT_CoinReference.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using System.IO; -using Neo.IO; -using Neo.IO.Json; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_CoinReference - { - CoinReference uut; - - [TestInitialize] - public void TestSetup() - { - uut = new CoinReference(); - } - - [TestMethod] - public void PrevHash_Get() - { - uut.PrevHash.Should().BeNull(); - } - - [TestMethod] - public void PrevHash_Set() - { - UInt256 val = new UInt256(TestUtils.GetByteArray(32, 0x42)); - uut.PrevHash = val; - - uut.PrevHash.Should().Be(val); - } - - [TestMethod] - public void PrevIndex_Get() - { - uut.PrevIndex.Should().Be(0); - } - - [TestMethod] - public void PrevIndex_Set() - { - ushort val = 42; - uut.PrevIndex = val; - - uut.PrevIndex.Should().Be(val); - } - - [TestMethod] - public void Size() - { - uut.PrevHash = new UInt256(TestUtils.GetByteArray(32, 0x42)); - uut.Size.Should().Be(34); - } - - private void setupCoinReferenceWithVals(CoinReference coinRef, out UInt256 prevHashVal, out ushort prevIndexVal) - { - prevHashVal = new UInt256(TestUtils.GetByteArray(32, 0x42)); - prevIndexVal = 22; - coinRef.PrevHash = prevHashVal; - coinRef.PrevIndex = prevIndexVal; - } - - [TestMethod] - public void Serialize() - { - UInt256 prevHashVal; - ushort prevIndexVal; - setupCoinReferenceWithVals(uut, out prevHashVal, out prevIndexVal); - - byte[] data; - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - ((ISerializable)uut).Serialize(writer); - data = stream.ToArray(); - } - } - - byte[] requiredData = new byte[] { 66, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 22, 0 }; - data.Length.Should().Be(34); - for (int i = 0; i < 34; i++) - { - data[i].Should().Be(requiredData[i]); - } - } - - [TestMethod] - public void Deserialize() - { - UInt256 prevHashVal; - ushort prevIndexVal; - setupCoinReferenceWithVals(uut, out prevHashVal, out prevIndexVal); - - byte[] data = new byte[] { 66, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 22, 0 }; - int index = 0; - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - { - using (BinaryReader reader = new BinaryReader(ms)) - { - ((ISerializable)uut).Deserialize(reader); - } - } - - uut.PrevHash.Should().Be(prevHashVal); - uut.PrevIndex.Should().Be(prevIndexVal); - } - - [TestMethod] - public void Equals_SameObj() - { - uut.Equals(uut).Should().BeTrue(); - } - - [TestMethod] - public void Equals_Null() - { - uut.Equals(null).Should().BeFalse(); - } - - [TestMethod] - public void Equals_SameHash() - { - - UInt256 prevHashVal; - ushort prevIndexVal; - setupCoinReferenceWithVals(uut, out prevHashVal, out prevIndexVal); - CoinReference newCoinRef = new CoinReference(); - setupCoinReferenceWithVals(newCoinRef, out prevHashVal, out prevIndexVal); - - uut.Equals(newCoinRef).Should().BeTrue(); - } - - [TestMethod] - public void Equals_DiffHash() - { - - UInt256 prevHashVal; - ushort prevIndexVal; - setupCoinReferenceWithVals(uut, out prevHashVal, out prevIndexVal); - CoinReference newCoinRef = new CoinReference(); - setupCoinReferenceWithVals(newCoinRef, out prevHashVal, out prevIndexVal); - newCoinRef.PrevHash = new UInt256(TestUtils.GetByteArray(32, 0x78)); - - uut.Equals(newCoinRef).Should().BeFalse(); - } - - - [TestMethod] - public void Equals_SameIndex() - { - - UInt256 prevHashVal; - ushort prevIndexVal; - setupCoinReferenceWithVals(uut, out prevHashVal, out prevIndexVal); - CoinReference newCoinRef = new CoinReference(); - setupCoinReferenceWithVals(newCoinRef, out prevHashVal, out prevIndexVal); - - uut.Equals(newCoinRef).Should().BeTrue(); - } - - [TestMethod] - public void Equals_DiffIndex() - { - - UInt256 prevHashVal; - ushort prevIndexVal; - setupCoinReferenceWithVals(uut, out prevHashVal, out prevIndexVal); - CoinReference newCoinRef = new CoinReference(); - setupCoinReferenceWithVals(newCoinRef, out prevHashVal, out prevIndexVal); - newCoinRef.PrevIndex = 73; - - uut.Equals(newCoinRef).Should().BeFalse(); - } - - [TestMethod] - public void Class_GetHashCode() - { - UInt256 prevHashVal; - ushort prevIndexVal; - setupCoinReferenceWithVals(uut, out prevHashVal, out prevIndexVal); - - uut.GetHashCode().Should().Be(538976344); - } - - [TestMethod] - public void ToJson() - { - UInt256 prevHashVal; - ushort prevIndexVal; - setupCoinReferenceWithVals(uut, out prevHashVal, out prevIndexVal); - - JObject jObj = uut.ToJson(); - jObj.Should().NotBeNull(); - jObj["txid"].AsString().Should().Be("0x2020202020202020202020202020202020202020202020202020202020202042"); - jObj["vout"].AsNumber().Should().Be(prevIndexVal); - } - - } -} diff --git a/neo.UnitTests/UT_Culture.cs b/neo.UnitTests/UT_Culture.cs deleted file mode 100644 index f1b2fcb57b..0000000000 --- a/neo.UnitTests/UT_Culture.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_Culture - { - // This test runs all the other unit tests in the project, with a variety of cultures - // This test will fail when any other test in the project fails. Fix the other failing test(s) and this test should pass again. - [TestMethod] - [NotReRunnable] - public void All_Tests_Cultures() - { - // get all tests in the unit test project assembly - var testClasses = (from t in typeof(NotReRunnableAttribute).GetTypeInfo().Assembly.DefinedTypes - where t.GetCustomAttribute() != null - select new - { - Constructor = t.GetConstructor(new Type[] { }), - ClassInit = t.GetMethods().Where( - m => m.GetCustomAttribute() != null).SingleOrDefault(), - TestInit = t.GetMethods().Where( - m => m.GetCustomAttribute() != null).SingleOrDefault(), - TestCleanup = t.GetMethods().Where( - m => m.GetCustomAttribute() != null).SingleOrDefault(), - ClassCleanup = t.GetMethods().Where( - m => m.GetCustomAttribute() != null).SingleOrDefault(), - TestMethods = t.GetMethods().Where( - m => m.GetCustomAttribute() != null - && m.GetCustomAttribute() == null).ToList() - }).ToList(); - - var cultures = new string[] { "en-US", "zh-CN", "de-DE", "ko-KR", "ja-JP" }; - var originalUICulture = CultureInfo.CurrentCulture; - var emtpyObjArray = new object[] { }; - - // run all the tests, varying the culture each time. - try - { - foreach (var culture in cultures) - { - CultureInfo.CurrentCulture = new CultureInfo(culture); - - foreach (var c in testClasses) - { - var instance = c.Constructor.Invoke(emtpyObjArray); - if (c.ClassInit != null) - { - c.ClassInit.Invoke(instance, emtpyObjArray); - } - foreach (var m in c.TestMethods) - { - if (c.TestInit != null) - { - c.TestInit.Invoke(instance, emtpyObjArray); - } - m.Invoke(instance, emtpyObjArray); - if (c.TestCleanup != null) - { - c.TestCleanup.Invoke(instance, emtpyObjArray); - } - } - if (c.ClassCleanup != null) - { - c.ClassCleanup.Invoke(instance, emtpyObjArray); - } - } - } - } - finally - { - CultureInfo.CurrentCulture = originalUICulture; - } - - } - } - - public class NotReRunnableAttribute : Attribute - { - - } -} diff --git a/neo.UnitTests/UT_Fixed8.cs b/neo.UnitTests/UT_Fixed8.cs deleted file mode 100644 index 139bcd158b..0000000000 --- a/neo.UnitTests/UT_Fixed8.cs +++ /dev/null @@ -1,25 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_Fixed8 - { - [TestMethod] - public void Basic_equality() - { - var a = Fixed8.FromDecimal(1.23456789m); - var b = Fixed8.Parse("1.23456789"); - a.Should().Be(b); - } - - [TestMethod] - public void Can_parse_exponent_notation() - { - Fixed8 expected = Fixed8.FromDecimal(1.23m); - Fixed8 actual = Fixed8.Parse("1.23E-0"); - actual.Should().Be(expected); - } - } -} \ No newline at end of file diff --git a/neo.UnitTests/UT_Header.cs b/neo.UnitTests/UT_Header.cs deleted file mode 100644 index a0a508f9d5..0000000000 --- a/neo.UnitTests/UT_Header.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_Header - { - Header uut; - - [TestInitialize] - public void TestSetup() - { - uut = new Header(); - } - - [TestMethod] - public void Size_Get() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - TestUtils.SetupHeaderWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal); - // blockbase 4 + 32 + 32 + 4 + 4 + 8 + 20 + 1 + 3 - // header 1 - uut.Size.Should().Be(109); - } - - [TestMethod] - public void Deserialize() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - TestUtils.SetupHeaderWithValues(new Header(), val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal); - - uut.MerkleRoot = merkRoot; // need to set for deserialise to be valid - - byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 128, 171, 4, 253, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 81, 0 }; - int index = 0; - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - { - using (BinaryReader reader = new BinaryReader(ms)) - { - uut.Deserialize(reader); - } - } - - assertStandardHeaderTestVals(val256, merkRoot, val160, timestampVal, indexVal, consensusDataVal, scriptVal); - } - - private void assertStandardHeaderTestVals(UInt256 val256, UInt256 merkRoot, UInt160 val160, uint timestampVal, uint indexVal, ulong consensusDataVal, Witness scriptVal) - { - uut.PrevHash.Should().Be(val256); - uut.MerkleRoot.Should().Be(merkRoot); - uut.Timestamp.Should().Be(timestampVal); - uut.Index.Should().Be(indexVal); - uut.ConsensusData.Should().Be(consensusDataVal); - uut.NextConsensus.Should().Be(val160); - uut.Script.InvocationScript.Length.Should().Be(0); - uut.Script.Size.Should().Be(scriptVal.Size); - uut.Script.VerificationScript[0].Should().Be(scriptVal.VerificationScript[0]); - } - - [TestMethod] - public void Equals_Null() - { - uut.Equals(null).Should().BeFalse(); - } - - - [TestMethod] - public void Equals_SameHeader() - { - uut.Equals(uut).Should().BeTrue(); - } - - [TestMethod] - public void Equals_SameHash() - { - Header newHeader = new Header(); - UInt256 prevHash = new UInt256(TestUtils.GetByteArray(32, 0x42)); - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - TestUtils.SetupHeaderWithValues(newHeader, prevHash, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal); - TestUtils.SetupHeaderWithValues(uut, prevHash, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal); - - uut.Equals(newHeader).Should().BeTrue(); - } - - [TestMethod] - public void Equals_SameObject() - { - uut.Equals((object)uut).Should().BeTrue(); - } - - [TestMethod] - public void FromTrimmedData() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRoot; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - TestUtils.SetupHeaderWithValues(new Header(), val256, out merkRoot, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal); - - byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 128, 171, 4, 253, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 81, 0 }; - - uut = Header.FromTrimmedData(data, 0); - - assertStandardHeaderTestVals(val256, merkRoot, val160, timestampVal, indexVal, consensusDataVal, scriptVal); - } - - [TestMethod] - public void Serialize() - { - UInt256 val256 = UInt256.Zero; - UInt256 merkRootVal; - UInt160 val160; - uint timestampVal, indexVal; - ulong consensusDataVal; - Witness scriptVal; - TestUtils.SetupHeaderWithValues(uut, val256, out merkRootVal, out val160, out timestampVal, out indexVal, out consensusDataVal, out scriptVal); - - byte[] data; - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - uut.Serialize(writer); - data = stream.ToArray(); - } - } - - byte[] requiredData = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 87, 42, 69, 155, 149, 217, 19, 107, 122, 113, 60, 84, 133, 202, 112, 159, 158, 250, 79, 8, 241, 194, 93, 215, 146, 103, 45, 43, 215, 91, 251, 128, 171, 4, 253, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 81, 0 }; - - data.Length.Should().Be(109); - for (int i = 0; i < 109; i++) - { - data[i].Should().Be(requiredData[i]); - } - } - } -} diff --git a/neo.UnitTests/UT_Helper.cs b/neo.UnitTests/UT_Helper.cs deleted file mode 100644 index fddcf06318..0000000000 --- a/neo.UnitTests/UT_Helper.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using FluentAssertions; -using Neo.Core; -using Neo.Wallets; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_Helper - { - [TestMethod] - public void GetHashData() - { - TestVerifiable verifiable = new TestVerifiable(); - byte[] res = verifiable.GetHashData(); - res.Length.Should().Be(8); - byte[] requiredData = new byte[] {7, 116, 101, 115, 116, 83, 116, 114}; - for (int i = 0; i < requiredData.Length; i++) - { - res[i].Should().Be(requiredData[i]); - } - } - - [TestMethod] - public void Sign() - { - TestVerifiable verifiable = new TestVerifiable(); - byte[] res = verifiable.Sign(new KeyPair(TestUtils.GetByteArray(32,0x42))); - res.Length.Should().Be(64); - } - - [TestMethod] - public void ToScriptHash() - { - byte[] testByteArray = TestUtils.GetByteArray(64,0x42); - UInt160 res = testByteArray.ToScriptHash(); - res.Should().Be(UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302")); - } - - } -} diff --git a/neo.UnitTests/UT_InvocationTransaction.cs b/neo.UnitTests/UT_InvocationTransaction.cs deleted file mode 100644 index 77b665f92a..0000000000 --- a/neo.UnitTests/UT_InvocationTransaction.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO.Json; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_InvocationTransaction - { - InvocationTransaction uut; - - [TestInitialize] - public void TestSetup() - { - uut = new InvocationTransaction(); - } - - [TestMethod] - public void Script_Get() - { - uut.Script.Should().BeNull(); - } - - [TestMethod] - public void Script_Set() - { - byte[] val = TestUtils.GetByteArray(32, 0x42); - uut.Script = val; - uut.Script.Length.Should().Be(32); - for (int i = 0; i < val.Length; i++) - { - uut.Script[i].Should().Be(val[i]); - } - } - - [TestMethod] - public void Gas_Get() - { - uut.Gas.Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void Gas_Set() - { - Fixed8 val = Fixed8.FromDecimal(42); - uut.Gas = val; - uut.Gas.Should().Be(val); - } - - [TestMethod] - public void Size_Get() - { - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - byte[] val = TestUtils.GetByteArray(32, 0x42); - uut.Script = val; - uut.Size.Should().Be(39); // 1, 1, 1, 1, 1, 1 + script 33 - } - - [TestMethod] - public void SystemFee_Get() - { - uut.SystemFee.Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void SystemFee_Get_FromGas() - { - Fixed8 val = Fixed8.FromDecimal(42); - uut.Gas = val; - uut.SystemFee.Should().Be(val); - } - - [TestMethod] - public void SystemFee_Set() - { - Fixed8 val = Fixed8.FromDecimal(42); - uut.Gas = val; - uut.SystemFee.Should().Be(val); - } - - [TestMethod] - public void ToJson() - { - byte[] scriptVal = TestUtils.GetByteArray(32, 0x42); - uut.Script = scriptVal; - Fixed8 gasVal = Fixed8.FromDecimal(42); - uut.Gas = gasVal; - - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - JObject jObj = uut.ToJson(); - jObj.Should().NotBeNull(); - jObj["txid"].AsString().Should().Be("0x8258b950487299376f89ad2d09598b7acbc5cde89b161b3dd73c256f9e2a94b1"); - jObj["size"].AsNumber().Should().Be(39); - jObj["type"].AsString().Should().Be("InvocationTransaction"); - jObj["version"].AsNumber().Should().Be(0); - ((JArray)jObj["attributes"]).Count.Should().Be(0); - ((JArray)jObj["vin"]).Count.Should().Be(0); - ((JArray)jObj["vout"]).Count.Should().Be(0); - jObj["sys_fee"].AsString().Should().Be("42"); - jObj["net_fee"].AsString().Should().Be("-42"); - ((JArray)jObj["scripts"]).Count.Should().Be(0); - - jObj["script"].AsString().Should().Be("4220202020202020202020202020202020202020202020202020202020202020"); - jObj["gas"].AsNumber().Should().Be(42); - } - } -} diff --git a/neo.UnitTests/UT_IssueTransaction.cs b/neo.UnitTests/UT_IssueTransaction.cs deleted file mode 100644 index efb6ae8580..0000000000 --- a/neo.UnitTests/UT_IssueTransaction.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO.Json; -using Neo.Wallets; -using Neo.VM; -using Neo.SmartContract; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_IssueTransaction - { - IssueTransaction uut; - - [TestInitialize] - public void TestSetup() - { - uut = new IssueTransaction(); - } - - [TestMethod] - public void SystemFee_Get() - { - uut.Version = 1; - uut.SystemFee.Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void SystemFee_Get_Version_0_Share() - { - uut = TestUtils.GetIssueTransaction(false, 10, Blockchain.GoverningToken.Hash); - uut.Version = 0; - - uut.SystemFee.Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void SystemFee_Get_Version_0_Coin() - { - uut = TestUtils.GetIssueTransaction(false, 10, Blockchain.UtilityToken.Hash); - uut.Version = 0; - - uut.SystemFee.Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void SystemFee_Get_Version_0_OtherAsset() - { - uut = TestUtils.GetIssueTransaction(false, 10, new UInt256(TestUtils.GetByteArray(32,0x42))); - uut.Version = 0; - - uut.SystemFee.Should().Be(Fixed8.FromDecimal(500)); - } - - [TestMethod] - public void GetScriptHashesForVerifying() - { - TestUtils.SetupTestBlockchain(UInt256.Zero); - uut = TestUtils.GetIssueTransaction(false, 10, Blockchain.UtilityToken.Hash); - UInt160[] res = uut.GetScriptHashesForVerifying(); - res.Length.Should().Be(1); - res[0].Should().Be(new UInt160(TestUtils.GetByteArray(20, 0xe7))); - } - - [TestMethod] - public void GetScriptHashesForVerifying_ThrowsException_NullAsset() - { - TestUtils.SetupTestBlockchain(UInt256.Zero); - uut = TestUtils.GetIssueTransaction(false, 10, UInt256.Zero); - Action test = () => uut.GetScriptHashesForVerifying(); - test.ShouldThrow(); - } - - [TestMethod] - public void GetScriptHashesForVerifying_Ordered() - { - TestUtils.SetupTestBlockchain(UInt256.Zero); - uut = new IssueTransaction - { - Attributes = new TransactionAttribute[0], - Inputs = new CoinReference[0], - Outputs = new[] - { - new TransactionOutput - { - AssetId = Blockchain.UtilityToken.Hash, - Value = Fixed8.FromDecimal(10), - ScriptHash = Contract.CreateMultiSigRedeemScript(1, TestUtils.StandbyValidators).ToScriptHash() - }, - new TransactionOutput - { - AssetId = Blockchain.GoverningToken.Hash, - Value = Fixed8.FromDecimal(10), - ScriptHash = Contract.CreateMultiSigRedeemScript(1, TestUtils.StandbyValidators).ToScriptHash() - }, - }, - Scripts = new[] - { - new Witness - { - InvocationScript = new byte[0], - VerificationScript = new[] { (byte)OpCode.PUSHT } - } - } - }; - UInt160[] res = uut.GetScriptHashesForVerifying(); - res.Length.Should().Be(2); - res[0].Should().Be(new UInt160(TestUtils.GetByteArray(20, 0x9b))); - res[1].Should().Be(new UInt160(TestUtils.GetByteArray(20, 0xe7))); - } - - } -} diff --git a/neo.UnitTests/UT_MinerTransaction.cs b/neo.UnitTests/UT_MinerTransaction.cs deleted file mode 100644 index 9c6110e6ad..0000000000 --- a/neo.UnitTests/UT_MinerTransaction.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO.Json; -using Neo.Wallets; -using Neo.VM; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_MinerTransaction - { - MinerTransaction uut; - - [TestInitialize] - public void TestSetup() - { - uut = new MinerTransaction(); - } - - [TestMethod] - public void Nonce_Get() - { - uut.Nonce.Should().Be(0u); - } - - [TestMethod] - public void Nonce_Set() - { - uint val = 42; - uut.Nonce = val; - uut.Nonce.Should().Be(val); - } - - [TestMethod] - public void NetworkFee_Get() - { - uut.NetworkFee.Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void Size_Get() - { - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - - uut.Size.Should().Be(10); // 1, 1, 1, 1, 1, 1 + 4 - } - - [TestMethod] - public void ToJson() - { - uut.Attributes = new TransactionAttribute[0]; - uut.Inputs = new CoinReference[0]; - uut.Outputs = new TransactionOutput[0]; - uut.Scripts = new Witness[0]; - uut.Nonce = 42; - - JObject jObj = uut.ToJson(); - jObj.Should().NotBeNull(); - jObj["txid"].AsString().Should().Be("0xe42ca5744eda6de2e1a2bdc2ed98fa7b967b13cd3aa2605c95fff37261f07ef6"); - jObj["size"].AsNumber().Should().Be(10); - jObj["type"].AsString().Should().Be("MinerTransaction"); - jObj["version"].AsNumber().Should().Be(0); - ((JArray)jObj["attributes"]).Count.Should().Be(0); - ((JArray)jObj["vin"]).Count.Should().Be(0); - ((JArray)jObj["vout"]).Count.Should().Be(0); - jObj["sys_fee"].AsNumber().Should().Be(0); - jObj["net_fee"].AsNumber().Should().Be(0); - ((JArray)jObj["scripts"]).Count.Should().Be(0); - - jObj["nonce"].AsNumber().Should().Be(42); - } - - } -} diff --git a/neo.UnitTests/UT_SpentCoint.cs b/neo.UnitTests/UT_SpentCoint.cs deleted file mode 100644 index 117ceb6d08..0000000000 --- a/neo.UnitTests/UT_SpentCoint.cs +++ /dev/null @@ -1,72 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_SpentCoint - { - SpentCoin uut; - - [TestInitialize] - public void TestSetup() - { - uut = new SpentCoin(); - } - - [TestMethod] - public void Output_Get() - { - uut.Output.Should().BeNull(); - } - - [TestMethod] - public void Output_Set() - { - TransactionOutput val = new TransactionOutput(); - uut.Output = val; - uut.Output.Should().Be(val); - } - - [TestMethod] - public void StartHeight_Get() - { - uut.StartHeight.Should().Be(0u); - } - - [TestMethod] - public void StartHeight_Set() - { - uint val = 42; - uut.StartHeight = val; - uut.StartHeight.Should().Be(val); - } - - [TestMethod] - public void EndHeight_Get() - { - uut.EndHeight.Should().Be(0u); - } - - [TestMethod] - public void EndHeight_Set() - { - uint val = 42; - uut.EndHeight = val; - uut.EndHeight.Should().Be(val); - } - - [TestMethod] - public void Value_Get() - { - TransactionOutput val = new TransactionOutput(); - val.Value = Fixed8.FromDecimal(42); - uut.Output = val; - uut.Value.Should().Be(val.Value); - } - } -} diff --git a/neo.UnitTests/UT_SpentCointState.cs b/neo.UnitTests/UT_SpentCointState.cs deleted file mode 100644 index c728c4bcf7..0000000000 --- a/neo.UnitTests/UT_SpentCointState.cs +++ /dev/null @@ -1,140 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_SpentCointState - { - SpentCoinState uut; - - [TestInitialize] - public void TestSetup() - { - uut = new SpentCoinState(); - } - - [TestMethod] - public void TransactionHash_Get() - { - uut.TransactionHash.Should().BeNull(); - } - - [TestMethod] - public void TransactionHash_Set() - { - UInt256 val = new UInt256(); - uut.TransactionHash = val; - uut.TransactionHash.Should().Be(val); - } - - [TestMethod] - public void TransactionHeight_Get() - { - uut.TransactionHeight.Should().Be(0u); - } - - [TestMethod] - public void TransactionHeight_Set() - { - uint val = 4294967295; - uut.TransactionHeight = val; - uut.TransactionHeight.Should().Be(val); - } - - [TestMethod] - public void Items_Get() - { - uut.Items.Should().BeNull(); - } - - [TestMethod] - public void Items_Set() - { - ushort key = new ushort(); - uint val = new uint(); - Dictionary dict = new Dictionary(); - dict.Add(key, val); - uut.Items = dict; - uut.Items[key].Should().Be(val); - } - - [TestMethod] - public void Size_Get_With_No_Items() - { - UInt256 val = new UInt256(); - Dictionary dict = new Dictionary(); - uut.TransactionHash = val; - uut.Items = dict; - uut.Size.Should().Be(38); // 1 + 32 + 4 + 1 + 0 * (2 + 4) - } - - [TestMethod] - public void Size_Get_With_Items() - { - UInt256 val = new UInt256(); - Dictionary dict = new Dictionary(); - uut.TransactionHash = val; - dict.Add(42, 100); - uut.Items = dict; - uut.Size.Should().Be(44); // 1 + 32 + 4 + 1 + 1 * (2 + 4) - } - - private void setupSpentCoinStateWithValues(SpentCoinState spentCoinState, out UInt256 transactionHash, out uint transactionHeight) - { - transactionHash = new UInt256(TestUtils.GetByteArray(32, 0x20)); - spentCoinState.TransactionHash = transactionHash; - transactionHeight = 757859114; - spentCoinState.TransactionHeight = transactionHeight; - Dictionary dict = new Dictionary(); - dict.Add(42, 100); - spentCoinState.Items = dict; - } - - [TestMethod] - public void DeserializeSCS() - { - byte[] dataArray = new byte[] { 0, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 42, 3, 44, 45, 1, 42, 0, 0, 0, 0, 0 }; - - using (Stream stream = new MemoryStream(dataArray)) - { - using (BinaryReader reader = new BinaryReader(stream)) - { - uut.Deserialize(reader); - } - } - uut.TransactionHash.Should().Be(new UInt256(TestUtils.GetByteArray(32, 0x20))); - uut.TransactionHeight.Should().Be(757859114); - uut.Items.Should().ContainKey(42); - uut.Items[42].Should().Be(0); - } - - [TestMethod] - public void SerializeSCS() - { - UInt256 transactionHash; - uint transactionHeight; - setupSpentCoinStateWithValues(uut, out transactionHash, out transactionHeight); - - byte[] dataArray; - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - uut.Serialize(writer); - dataArray = stream.ToArray(); - } - } - byte[] requiredData = new byte[] { 0, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 42, 3, 44, 45, 1, 42, 0, 100, 0, 0, 0 }; - dataArray.Length.Should().Be(44); - for (int i = 0; i < 44; i++) - { - dataArray[i].Should().Be(requiredData[i]); - } - } - } -} diff --git a/neo.UnitTests/UT_StorageItem.cs b/neo.UnitTests/UT_StorageItem.cs deleted file mode 100644 index ec36b90762..0000000000 --- a/neo.UnitTests/UT_StorageItem.cs +++ /dev/null @@ -1,110 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_StorageItem - { - StorageItem uut; - - [TestInitialize] - public void TestSetup() - { - uut = new StorageItem(); - } - - [TestMethod] - public void Value_Get() - { - uut.Value.Should().BeNull(); - } - - [TestMethod] - public void Value_Set() - { - byte[] val = new byte[] { 0x42, 0x32}; - uut.Value = val; - uut.Value.Length.Should().Be(2); - uut.Value[0].Should().Be(val[0]); - uut.Value[1].Should().Be(val[1]); - } - - [TestMethod] - public void Size_Get() - { - uut.Value = TestUtils.GetByteArray(10, 0x42); - uut.Size.Should().Be(12); // 2 + 10 - } - - [TestMethod] - public void Size_Get_Larger() - { - uut.Value = TestUtils.GetByteArray(88, 0x42); - uut.Size.Should().Be(90); // 2 + 88 - } - - [TestMethod] - public void Clone() - { - uut.Value = TestUtils.GetByteArray(10, 0x42); - - StorageItem newSi = ((ICloneable)uut).Clone(); - newSi.Value.Length.Should().Be(10); - newSi.Value[0].Should().Be(0x42); - for (int i=1; i<10; i++) - { - newSi.Value[i].Should().Be(0x20); - } - } - - [TestMethod] - public void Deserialize() - { - byte[] data = new byte[] { 0, 10, 66, 32, 32, 32, 32, 32, 32, 32, 32, 32 }; - int index = 0; - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - { - using (BinaryReader reader = new BinaryReader(ms)) - { - uut.Deserialize(reader); - } - } - uut.Value.Length.Should().Be(10); - uut.Value[0].Should().Be(0x42); - for (int i = 1; i < 10; i++) - { - uut.Value[i].Should().Be(0x20); - } - } - - [TestMethod] - public void Serialize() - { - uut.Value = TestUtils.GetByteArray(10, 0x42); - - byte[] data; - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - uut.Serialize(writer); - data = stream.ToArray(); - } - } - - byte[] requiredData = new byte[] { 0, 10, 66, 32, 32, 32, 32, 32, 32, 32, 32, 32 }; - - data.Length.Should().Be(12); - for (int i = 0; i < 12; i++) - { - data[i].Should().Be(requiredData[i]); - } - } - - } -} diff --git a/neo.UnitTests/UT_StorageKey.cs b/neo.UnitTests/UT_StorageKey.cs deleted file mode 100644 index 037e0c3309..0000000000 --- a/neo.UnitTests/UT_StorageKey.cs +++ /dev/null @@ -1,115 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_StorageKey - { - StorageKey uut; - - [TestInitialize] - public void TestSetup() - { - uut = new StorageKey(); - } - - [TestMethod] - public void ScriptHash_Get() - { - uut.ScriptHash.Should().BeNull(); - } - - [TestMethod] - public void ScriptHash_Set() - { - UInt160 val = new UInt160(TestUtils.GetByteArray(20, 0x42)); - uut.ScriptHash = val; - uut.ScriptHash.Should().Be(val); - } - - [TestMethod] - public void Key_Get() - { - uut.Key.Should().BeNull(); - } - - [TestMethod] - public void Key_Set() - { - byte[] val = new byte[] { 0x42, 0x32 }; - uut.Key = val; - uut.Key.Length.Should().Be(2); - uut.Key[0].Should().Be(val[0]); - uut.Key[1].Should().Be(val[1]); - } - - [TestMethod] - public void Equals_SameObj() - { - uut.Equals(uut).Should().BeTrue(); - } - - [TestMethod] - public void Equals_Null() - { - uut.Equals(null).Should().BeFalse(); - } - - [TestMethod] - public void Equals_SameHash_SameKey() - { - UInt160 val = new UInt160(TestUtils.GetByteArray(20, 0x42)); - byte[] keyVal = TestUtils.GetByteArray(10, 0x42); - StorageKey newSk = new StorageKey(); - newSk.ScriptHash = val; - newSk.Key = keyVal; - uut.ScriptHash = val; - uut.Key = keyVal; - - uut.Equals(newSk).Should().BeTrue(); - } - - [TestMethod] - public void Equals_DiffHash_SameKey() - { - UInt160 val = new UInt160(TestUtils.GetByteArray(20, 0x42)); - byte[] keyVal = TestUtils.GetByteArray(10, 0x42); - StorageKey newSk = new StorageKey(); - newSk.ScriptHash = val; - newSk.Key = keyVal; - uut.ScriptHash = new UInt160(TestUtils.GetByteArray(20, 0x88)); - uut.Key = keyVal; - - uut.Equals(newSk).Should().BeFalse(); - } - - - [TestMethod] - public void Equals_SameHash_DiffKey() - { - UInt160 val = new UInt160(TestUtils.GetByteArray(20, 0x42)); - byte[] keyVal = TestUtils.GetByteArray(10, 0x42); - StorageKey newSk = new StorageKey(); - newSk.ScriptHash = val; - newSk.Key = keyVal; - uut.ScriptHash = val; - uut.Key = TestUtils.GetByteArray(10, 0x88); - - uut.Equals(newSk).Should().BeFalse(); - } - - [TestMethod] - public void GetHashCode_Get() - { - uut.ScriptHash = new UInt160(TestUtils.GetByteArray(20, 0x42)); - uut.Key = TestUtils.GetByteArray(10, 0x42); - uut.GetHashCode().Should().Be(806209853); - } - - } -} diff --git a/neo.UnitTests/UT_TransactionAttribute.cs b/neo.UnitTests/UT_TransactionAttribute.cs deleted file mode 100644 index c5ff05c7b4..0000000000 --- a/neo.UnitTests/UT_TransactionAttribute.cs +++ /dev/null @@ -1,124 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO.Json; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_TransactionAttribute - { - TransactionAttribute uut; - - [TestInitialize] - public void TestSetup() - { - uut = new TransactionAttribute(); - } - - [TestMethod] - public void Usage_Get() - { - uut.Usage.Should().Be(TransactionAttributeUsage.ContractHash); - } - - [TestMethod] - public void Usage_Set() - { - uut.Usage = TransactionAttributeUsage.ECDH02; - uut.Usage.Should().Be(TransactionAttributeUsage.ECDH02); - } - - [TestMethod] - public void Data_Get() - { - uut.Data.Should().BeNull(); - } - - [TestMethod] - public void Data_Set() - { - byte[] val = new byte[] { 0x42, 0x32 }; - uut.Data = val; - uut.Data.Length.Should().Be(2); - uut.Data[0].Should().Be(val[0]); - uut.Data[1].Should().Be(val[1]); - } - - [TestMethod] - public void Size_Get_ContractHash() - { - uut.Usage = TransactionAttributeUsage.ContractHash; - uut.Size.Should().Be(33); // 1 + 32 - } - - [TestMethod] - public void Size_Get_ECDH02() - { - uut.Usage = TransactionAttributeUsage.ECDH02; - uut.Size.Should().Be(33); // 1 + 32 - } - - [TestMethod] - public void Size_Get_ECDH03() - { - uut.Usage = TransactionAttributeUsage.ECDH03; - uut.Size.Should().Be(33); // 1 + 32 - } - - [TestMethod] - public void Size_Get_Vote() - { - uut.Usage = TransactionAttributeUsage.Vote; - uut.Size.Should().Be(33); // 1 + 32 - } - - [TestMethod] - public void Size_Get_Hash() - { - for (TransactionAttributeUsage i = TransactionAttributeUsage.Hash1; i <= TransactionAttributeUsage.Hash15; i++) - { - uut.Usage = i; - uut.Size.Should().Be(33); // 1 + 32 - } - } - - [TestMethod] - public void Size_Get_Script() - { - uut.Usage = TransactionAttributeUsage.Script; - uut.Size.Should().Be(21); // 1 + 20 - } - - [TestMethod] - public void Size_Get_DescriptionUrl() - { - uut.Usage = TransactionAttributeUsage.DescriptionUrl; - uut.Data = TestUtils.GetByteArray(10, 0x42); - uut.Size.Should().Be(12); // 1 + 1 + 10 - } - - [TestMethod] - public void Size_Get_OtherAttribute() - { - uut.Usage = TransactionAttributeUsage.Remark; - uut.Data = TestUtils.GetByteArray(10, 0x42); - uut.Size.Should().Be(12); // 1 + 11 - } - - [TestMethod] - public void ToJson() - { - uut.Usage = TransactionAttributeUsage.ECDH02; - uut.Data = TestUtils.GetByteArray(10, 0x42); - - JObject jObj = uut.ToJson(); - jObj.Should().NotBeNull(); - jObj["usage"].AsString().Should().Be("ECDH02"); - jObj["data"].AsString().Should().Be("42202020202020202020"); - } - } -} diff --git a/neo.UnitTests/UT_TransactionOutput.cs b/neo.UnitTests/UT_TransactionOutput.cs deleted file mode 100644 index 20862110e7..0000000000 --- a/neo.UnitTests/UT_TransactionOutput.cs +++ /dev/null @@ -1,90 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO.Json; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_TransactionOutput - { - TransactionOutput uut; - - [TestInitialize] - public void TestSetup() - { - uut = new TransactionOutput(); - } - - [TestMethod] - public void AssetId_Get() - { - uut.AssetId.Should().BeNull(); - } - - [TestMethod] - public void AssetId_Set() - { - UInt256 val = new UInt256(TestUtils.GetByteArray(32, 0x42)); - uut.AssetId = val; - uut.AssetId.Should().Be(val); - } - - [TestMethod] - public void Value_Get() - { - uut.Value.Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void Value_Set() - { - Fixed8 val = Fixed8.FromDecimal(42); - uut.Value = val; - uut.Value.Should().Be(val); - } - - [TestMethod] - public void ScriptHash_Get() - { - uut.ScriptHash.Should().BeNull(); - } - - [TestMethod] - public void ScriptHash_Set() - { - UInt160 val = new UInt160(TestUtils.GetByteArray(20, 0x42)); - uut.ScriptHash = val; - uut.ScriptHash.Should().Be(val); - } - - [TestMethod] - public void Size_Get() - { - uut.AssetId = new UInt256(TestUtils.GetByteArray(32, 0x42)); - uut.Value = Fixed8.FromDecimal(42); - uut.ScriptHash = new UInt160(TestUtils.GetByteArray(20, 0x42)); - - uut.Size.Should().Be(60); // 32 + 8 + 20 - } - - [TestMethod] - public void ToJson() - { - uut.AssetId = new UInt256(TestUtils.GetByteArray(32, 0x42)); - uut.Value = Fixed8.FromDecimal(42); - uut.ScriptHash = new UInt160(TestUtils.GetByteArray(20, 0x42)); - - JObject jObj = uut.ToJson(36); - jObj.Should().NotBeNull(); - jObj["n"].AsNumber().Should().Be(36); - jObj["asset"].AsString().Should().Be("0x2020202020202020202020202020202020202020202020202020202020202042"); - jObj["value"].AsString().Should().Be("42"); - jObj["address"].AsString().Should().Be("AMoWjH3BDwMY7j8FEAovPJdq8XEuyJynwN"); - } - - } -} diff --git a/neo.UnitTests/UT_TransactionResult.cs b/neo.UnitTests/UT_TransactionResult.cs deleted file mode 100644 index da6165807e..0000000000 --- a/neo.UnitTests/UT_TransactionResult.cs +++ /dev/null @@ -1,50 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO.Json; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_TransactionResult - { - TransactionResult uut; - - [TestInitialize] - public void TestSetup() - { - uut = new TransactionResult(); - } - - [TestMethod] - public void AssetId_Get() - { - uut.AssetId.Should().BeNull(); - } - - [TestMethod] - public void AssetId_Set() - { - UInt256 val = new UInt256(TestUtils.GetByteArray(32, 0x42)); - uut.AssetId = val; - uut.AssetId.Should().Be(val); - } - - [TestMethod] - public void Amount_Get() - { - uut.Amount.Should().Be(Fixed8.Zero); - } - - [TestMethod] - public void Amount_Set() - { - Fixed8 val = Fixed8.FromDecimal(42); - uut.Amount = val; - uut.Amount.Should().Be(val); - } - } -} diff --git a/neo.UnitTests/UT_UnspentCoinState.cs b/neo.UnitTests/UT_UnspentCoinState.cs deleted file mode 100644 index e8a723808b..0000000000 --- a/neo.UnitTests/UT_UnspentCoinState.cs +++ /dev/null @@ -1,105 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_UnspentCoinState - { - UnspentCoinState uut; - - [TestInitialize] - public void TestSetup() - { - uut = new UnspentCoinState(); - } - - [TestMethod] - public void Items_Get() - { - uut.Items.Should().BeNull(); - } - - [TestMethod] - public void Items_Set() - { - CoinState item1 = CoinState.Confirmed; - CoinState item2 = CoinState.Spent; - CoinState[] val = new CoinState[] { item1, item2 }; - uut.Items = val; - uut.Items.Length.Should().Be(val.Length); - uut.Items[0].Should().Be(item1); - uut.Items[1].Should().Be(item2); - } - - [TestMethod] - public void Size_Get_1() - { - CoinState item1 = CoinState.Confirmed; - CoinState[] val = new CoinState[] { item1 }; - uut.Items = val; - - uut.Size.Should().Be(3); // 1 + 2 - } - - [TestMethod] - public void Size_Get_3() - { - CoinState item1 = CoinState.Confirmed; - CoinState item2 = CoinState.Frozen; - CoinState item3 = CoinState.Spent; - CoinState[] val = new CoinState[] { item1, item2, item3 }; - uut.Items = val; - - uut.Size.Should().Be(5); // 1 + 4 - } - - [TestMethod] - public void Deserialize() - { - byte[] data = new byte[] { 0, 2, 1, 8 }; - int index = 0; - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - { - using (BinaryReader reader = new BinaryReader(ms)) - { - uut.Deserialize(reader); - } - } - uut.Items.Length.Should().Be(2); - uut.Items[0].Should().Be(CoinState.Confirmed); - uut.Items[1].Should().Be(CoinState.Claimed); - } - - [TestMethod] - public void Serialize() - { - CoinState item1 = CoinState.Confirmed; - CoinState item2 = CoinState.Claimed; - CoinState[] val = new CoinState[] { item1, item2 }; - uut.Items = val; - - byte[] data; - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - uut.Serialize(writer); - data = stream.ToArray(); - } - } - - byte[] requiredData = new byte[] { 0, 2, 1, 8 }; - - data.Length.Should().Be(requiredData.Length); - for (int i = 0; i < requiredData.Length; i++) - { - data[i].Should().Be(requiredData[i]); - } - } - - } -} diff --git a/neo.UnitTests/UT_ValidatorState.cs b/neo.UnitTests/UT_ValidatorState.cs deleted file mode 100644 index 1c393bc756..0000000000 --- a/neo.UnitTests/UT_ValidatorState.cs +++ /dev/null @@ -1,108 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.Cryptography.ECC; -using System.IO; -using System.Text; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_ValidatorState - { - ValidatorState uut; - - [TestInitialize] - public void TestSetup() - { - uut = new ValidatorState(); - } - - [TestMethod] - public void PublicKey_Get() - { - uut.PublicKey.Should().BeNull(); - } - - [TestMethod] - public void Items_Set() - { - ECPoint val = new ECPoint(); - uut.PublicKey = val; - uut.PublicKey.Should().Be(val); - } - - [TestMethod] - public void Size_Get() - { - ECPoint val = ECPoint.DecodePoint("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c".HexToBytes(), ECCurve.Secp256r1); - uut.PublicKey = val; - - uut.Size.Should().Be(43); // 1 + 33 + 1 + 8 - } - - [TestMethod] - public void Deserialize() - { - byte[] data = new byte[] { 0, 3, 178, 9, 253, 79, 83, 167, 23, 14, 164, 68, 78, 12, 176, 166, 187, 106, 83, 194, 189, 1, 105, 38, 152, 156, 248, 95, 155, 15, 186, 23, 167, 12, 1, 0, 0, 0, 0, 0, 0, 0, 0 }; - int index = 0; - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - { - using (BinaryReader reader = new BinaryReader(ms)) - { - uut.Deserialize(reader); - } - } - uut.PublicKey.ToString().Should().Be("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c"); - } - - [TestMethod] - public void Equals_SameObj() - { - uut.Equals(uut).Should().BeTrue(); - } - - [TestMethod] - public void Equals_DifferentKey() - { - ValidatorState newVs = new ValidatorState(); - newVs.PublicKey = ECPoint.DecodePoint("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70b".HexToBytes(), ECCurve.Secp256r1); - uut.PublicKey = ECPoint.DecodePoint("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c".HexToBytes(), ECCurve.Secp256r1); - uut.Equals(newVs).Should().BeFalse(); - } - - [TestMethod] - public void Equals_Null() - { - uut.Equals(null).Should().BeFalse(); - } - - [TestMethod] - public void Serialize() - { - ECPoint val = ECPoint.DecodePoint("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c".HexToBytes(), ECCurve.Secp256r1); - uut.PublicKey = val; - uut.Registered = true; - uut.Votes = Fixed8.Zero; - - byte[] data; - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - uut.Serialize(writer); - data = stream.ToArray(); - } - } - - byte[] requiredData = new byte[] { 0, 3, 178, 9, 253, 79, 83, 167, 23, 14, 164, 68, 78, 12, 176, 166, 187, 106, 83, 194, 189, 1, 105, 38, 152, 156, 248, 95, 155, 15, 186, 23, 167, 12, 1, 0, 0, 0, 0, 0, 0, 0, 0 }; - - data.Length.Should().Be(requiredData.Length); - for (int i = 0; i < requiredData.Length; i++) - { - data[i].Should().Be(requiredData[i]); - } - } - - } -} diff --git a/neo.UnitTests/UT_Witness.cs b/neo.UnitTests/UT_Witness.cs deleted file mode 100644 index 48708da55d..0000000000 --- a/neo.UnitTests/UT_Witness.cs +++ /dev/null @@ -1,77 +0,0 @@ -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Neo.Core; -using Neo.IO.Json; - -namespace Neo.UnitTests -{ - [TestClass] - public class UT_Witness - { - Witness uut; - - [TestInitialize] - public void TestSetup() - { - uut = new Witness(); - } - - [TestMethod] - public void InvocationScript_Get() - { - uut.InvocationScript.Should().BeNull(); - } - - [TestMethod] - public void InvocationScript_Set() - { - byte[] dataArray = new byte[] { 0, 32, 32, 20, 32, 32 }; - uut.InvocationScript = dataArray; - uut.InvocationScript.Length.Should().Be(6); - Assert.AreEqual(uut.InvocationScript.ToHexString(), "002020142020"); - } - - private void setupWitnessWithValues(Witness uut, int lenghtInvocation, int lengthVerification, out byte[] invocationScript, out byte[] verificationScript) - { - invocationScript = TestUtils.GetByteArray(lenghtInvocation, 0x20); - verificationScript = TestUtils.GetByteArray(lengthVerification, 0x20); - uut.InvocationScript = invocationScript; - uut.VerificationScript = verificationScript; - } - - [TestMethod] - public void SizeWitness_Small_Arrary() - { - byte[] invocationScript; - byte[] verificationScript; - setupWitnessWithValues(uut, 252, 253, out invocationScript, out verificationScript); - - uut.Size.Should().Be(509); // (1 + 252*1) + (1 + 2 + 253*1) - } - - [TestMethod] - public void SizeWitness_Large_Arrary() - { - byte[] invocationScript; - byte[] verificationScript; - setupWitnessWithValues(uut, 65535, 65536, out invocationScript, out verificationScript); - - uut.Size.Should().Be(131079); // (1 + 2 + 65535*1) + (1 + 4 + 65536*1) - } - - [TestMethod] - public void ToJson() - { - byte[] invocationScript; - byte[] verificationScript; - setupWitnessWithValues(uut, 2, 3, out invocationScript, out verificationScript); - - JObject json = uut.ToJson(); - Assert.IsTrue(json.ContainsProperty("invocation")); - Assert.IsTrue(json.ContainsProperty("verification")); - Assert.AreEqual(json["invocation"].AsString(), "2020"); - Assert.AreEqual(json["verification"].AsString(), "202020"); - - } - } -} \ No newline at end of file diff --git a/neo.UnitTests/neo.UnitTests.csproj b/neo.UnitTests/neo.UnitTests.csproj deleted file mode 100644 index 87e51b9ce8..0000000000 --- a/neo.UnitTests/neo.UnitTests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - Exe - netcoreapp2.0 - Neo.UnitTests - Neo.UnitTests - - - - - - - - - - - - - - - - - - diff --git a/neo.png b/neo.png new file mode 100644 index 0000000000..1a71de07eb Binary files /dev/null and b/neo.png differ diff --git a/neo.sln b/neo.sln index 9d4c436c0e..b4d7ecd8d0 100644 --- a/neo.sln +++ b/neo.sln @@ -1,11 +1,33 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26430.15 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11201.2 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "neo", "neo\neo.csproj", "{36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo", "src\Neo\Neo.csproj", "{36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "neo.UnitTests", "neo.UnitTests\neo.UnitTests.csproj", "{5B783B30-B422-4C2F-AC22-187A8D1993F4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Json", "src\Neo.Json\Neo.Json.csproj", "{6B709ED6-64C0-451D-B07F-8F49185AE191}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.UnitTests", "tests\Neo.UnitTests\Neo.UnitTests.csproj", "{5B783B30-B422-4C2F-AC22-187A8D1993F4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Json.UnitTests", "tests\Neo.Json.UnitTests\Neo.Json.UnitTests.csproj", "{AE6C32EE-8447-4E01-8187-2AE02BB64251}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Benchmarks", "benchmarks\Neo.Benchmarks\Neo.Benchmarks.csproj", "{BCD03521-5F8F-4775-9ADF-FA361480804F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B5339DF7-5D1D-43BA-B332-74B825E1770E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{EDE05FA8-8E73-4924-BC63-DD117127EEE1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.IO", "src\Neo.IO\Neo.IO.csproj", "{4CDAC1AA-45C6-4157-8D8E-199050433048}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Extensions", "src\Neo.Extensions\Neo.Extensions.csproj", "{9C5213D6-3833-4570-8AE2-47E9F9017A8F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions.Tests", "tests\Neo.Extensions.Tests\Neo.Extensions.Tests.csproj", "{77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions.Benchmarks", "benchmarks\Neo.Extensions.Benchmarks\Neo.Extensions.Benchmarks.csproj", "{B6CB2559-10F9-41AC-8D58-364BFEF9688B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Json.Benchmarks", "benchmarks\Neo.Json.Benchmarks\Neo.Json.Benchmarks.csproj", "{5F984D2B-793F-4683-B53A-80050E6E0286}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,12 +39,59 @@ Global {36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU {36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}.Release|Any CPU.Build.0 = Release|Any CPU + {6B709ED6-64C0-451D-B07F-8F49185AE191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B709ED6-64C0-451D-B07F-8F49185AE191}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B709ED6-64C0-451D-B07F-8F49185AE191}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B709ED6-64C0-451D-B07F-8F49185AE191}.Release|Any CPU.Build.0 = Release|Any CPU {5B783B30-B422-4C2F-AC22-187A8D1993F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5B783B30-B422-4C2F-AC22-187A8D1993F4}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B783B30-B422-4C2F-AC22-187A8D1993F4}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B783B30-B422-4C2F-AC22-187A8D1993F4}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6C32EE-8447-4E01-8187-2AE02BB64251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6C32EE-8447-4E01-8187-2AE02BB64251}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6C32EE-8447-4E01-8187-2AE02BB64251}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6C32EE-8447-4E01-8187-2AE02BB64251}.Release|Any CPU.Build.0 = Release|Any CPU + {BCD03521-5F8F-4775-9ADF-FA361480804F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCD03521-5F8F-4775-9ADF-FA361480804F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCD03521-5F8F-4775-9ADF-FA361480804F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCD03521-5F8F-4775-9ADF-FA361480804F}.Release|Any CPU.Build.0 = Release|Any CPU + {4CDAC1AA-45C6-4157-8D8E-199050433048}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CDAC1AA-45C6-4157-8D8E-199050433048}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CDAC1AA-45C6-4157-8D8E-199050433048}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CDAC1AA-45C6-4157-8D8E-199050433048}.Release|Any CPU.Build.0 = Release|Any CPU + {9C5213D6-3833-4570-8AE2-47E9F9017A8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + {77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Release|Any CPU.Build.0 = Release|Any CPU + {B6CB2559-10F9-41AC-8D58-364BFEF9688B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6CB2559-10F9-41AC-8D58-364BFEF9688B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6CB2559-10F9-41AC-8D58-364BFEF9688B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6CB2559-10F9-41AC-8D58-364BFEF9688B}.Release|Any CPU.Build.0 = Release|Any CPU + {5F984D2B-793F-4683-B53A-80050E6E0286}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F984D2B-793F-4683-B53A-80050E6E0286}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F984D2B-793F-4683-B53A-80050E6E0286}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F984D2B-793F-4683-B53A-80050E6E0286}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {36447A9B-0311-4D4D-A3D5-AECBE9C15BBC} = {B5339DF7-5D1D-43BA-B332-74B825E1770E} + {6B709ED6-64C0-451D-B07F-8F49185AE191} = {B5339DF7-5D1D-43BA-B332-74B825E1770E} + {5B783B30-B422-4C2F-AC22-187A8D1993F4} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1} + {AE6C32EE-8447-4E01-8187-2AE02BB64251} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1} + {BCD03521-5F8F-4775-9ADF-FA361480804F} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC} + {4CDAC1AA-45C6-4157-8D8E-199050433048} = {B5339DF7-5D1D-43BA-B332-74B825E1770E} + {9C5213D6-3833-4570-8AE2-47E9F9017A8F} = {B5339DF7-5D1D-43BA-B332-74B825E1770E} + {77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1} + {B6CB2559-10F9-41AC-8D58-364BFEF9688B} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC} + {5F984D2B-793F-4683-B53A-80050E6E0286} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BCBA19D9-F868-4C6D-8061-A2B91E06E3EC} + EndGlobalSection EndGlobal diff --git a/neo/BigDecimal.cs b/neo/BigDecimal.cs deleted file mode 100644 index aefe996425..0000000000 --- a/neo/BigDecimal.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Numerics; - -namespace Neo -{ - public struct BigDecimal - { - private readonly BigInteger value; - private readonly byte decimals; - - public BigInteger Value => value; - public byte Decimals => decimals; - public int Sign => value.Sign; - - public BigDecimal(BigInteger value, byte decimals) - { - this.value = value; - this.decimals = decimals; - } - - public BigDecimal ChangeDecimals(byte decimals) - { - if (this.decimals == decimals) return this; - BigInteger value; - if (this.decimals < decimals) - { - value = this.value * BigInteger.Pow(10, decimals - this.decimals); - } - else - { - BigInteger divisor = BigInteger.Pow(10, this.decimals - decimals); - value = BigInteger.DivRem(this.value, divisor, out BigInteger remainder); - if (remainder > BigInteger.Zero) - throw new ArgumentOutOfRangeException(); - } - return new BigDecimal(value, decimals); - } - - public static BigDecimal Parse(string s, byte decimals) - { - if (!TryParse(s, decimals, out BigDecimal result)) - throw new FormatException(); - return result; - } - - public Fixed8 ToFixed8() - { - try - { - return new Fixed8((long)ChangeDecimals(8).value); - } - catch (Exception ex) - { - throw new InvalidCastException(ex.Message, ex); - } - } - - public override string ToString() - { - BigInteger divisor = BigInteger.Pow(10, decimals); - BigInteger result = BigInteger.DivRem(value, divisor, out BigInteger remainder); - if (remainder == 0) return result.ToString(); - return $"{result}.{remainder.ToString("d" + decimals)}".TrimEnd('0'); - } - - public static bool TryParse(string s, byte decimals, out BigDecimal result) - { - int e = 0; - int index = s.IndexOfAny(new[] { 'e', 'E' }); - if (index >= 0) - { - if (!sbyte.TryParse(s.Substring(index + 1), out sbyte e_temp)) - { - result = default(BigDecimal); - return false; - } - e = e_temp; - s = s.Substring(0, index); - } - index = s.IndexOf('.'); - if (index >= 0) - { - s = s.TrimEnd('0'); - e -= s.Length - index - 1; - s = s.Remove(index, 1); - } - int ds = e + decimals; - if (ds < 0) - { - result = default(BigDecimal); - return false; - } - if (ds > 0) - s += new string('0', ds); - if (!BigInteger.TryParse(s, out BigInteger value)) - { - result = default(BigDecimal); - return false; - } - result = new BigDecimal(value, decimals); - return true; - } - } -} diff --git a/neo/Consensus/ChangeView.cs b/neo/Consensus/ChangeView.cs deleted file mode 100644 index a76979f1ba..0000000000 --- a/neo/Consensus/ChangeView.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.IO; - -namespace Neo.Consensus -{ - internal class ChangeView : ConsensusMessage - { - public byte NewViewNumber; - - public ChangeView() - : base(ConsensusMessageType.ChangeView) - { - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - NewViewNumber = reader.ReadByte(); - if (NewViewNumber == 0) throw new FormatException(); - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write(NewViewNumber); - } - } -} diff --git a/neo/Consensus/ConsensusContext.cs b/neo/Consensus/ConsensusContext.cs deleted file mode 100644 index 417f3aa54e..0000000000 --- a/neo/Consensus/ConsensusContext.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Neo.Core; -using Neo.Cryptography; -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.Network.Payloads; -using Neo.Wallets; -using System.Collections.Generic; -using System.Linq; - -namespace Neo.Consensus -{ - internal class ConsensusContext - { - public const uint Version = 0; - public ConsensusState State; - public UInt256 PrevHash; - public uint BlockIndex; - public byte ViewNumber; - public ECPoint[] Validators; - public int MyIndex; - public uint PrimaryIndex; - public uint Timestamp; - public ulong Nonce; - public UInt160 NextConsensus; - public UInt256[] TransactionHashes; - public Dictionary Transactions; - public byte[][] Signatures; - public byte[] ExpectedView; - public KeyPair KeyPair; - - public int M => Validators.Length - (Validators.Length - 1) / 3; - - public void ChangeView(byte view_number) - { - int p = ((int)BlockIndex - view_number) % Validators.Length; - State &= ConsensusState.SignatureSent; - ViewNumber = view_number; - PrimaryIndex = p >= 0 ? (uint)p : (uint)(p + Validators.Length); - if (State == ConsensusState.Initial) - { - TransactionHashes = null; - Signatures = new byte[Validators.Length][]; - } - _header = null; - } - - public ConsensusPayload MakeChangeView() - { - return MakePayload(new ChangeView - { - NewViewNumber = ExpectedView[MyIndex] - }); - } - - private Block _header = null; - public Block MakeHeader() - { - if (TransactionHashes == null) return null; - if (_header == null) - { - _header = new Block - { - Version = Version, - PrevHash = PrevHash, - MerkleRoot = MerkleTree.ComputeRoot(TransactionHashes), - Timestamp = Timestamp, - Index = BlockIndex, - ConsensusData = Nonce, - NextConsensus = NextConsensus, - Transactions = new Transaction[0] - }; - } - return _header; - } - - private ConsensusPayload MakePayload(ConsensusMessage message) - { - message.ViewNumber = ViewNumber; - return new ConsensusPayload - { - Version = Version, - PrevHash = PrevHash, - BlockIndex = BlockIndex, - ValidatorIndex = (ushort)MyIndex, - Timestamp = Timestamp, - Data = message.ToArray() - }; - } - - public ConsensusPayload MakePrepareRequest() - { - return MakePayload(new PrepareRequest - { - Nonce = Nonce, - NextConsensus = NextConsensus, - TransactionHashes = TransactionHashes, - MinerTransaction = (MinerTransaction)Transactions[TransactionHashes[0]], - Signature = Signatures[MyIndex] - }); - } - - public ConsensusPayload MakePrepareResponse(byte[] signature) - { - return MakePayload(new PrepareResponse - { - Signature = signature - }); - } - - public void Reset(Wallet wallet) - { - State = ConsensusState.Initial; - PrevHash = Blockchain.Default.CurrentBlockHash; - BlockIndex = Blockchain.Default.Height + 1; - ViewNumber = 0; - Validators = Blockchain.Default.GetValidators(); - MyIndex = -1; - PrimaryIndex = BlockIndex % (uint)Validators.Length; - TransactionHashes = null; - Signatures = new byte[Validators.Length][]; - ExpectedView = new byte[Validators.Length]; - KeyPair = null; - for (int i = 0; i < Validators.Length; i++) - { - WalletAccount account = wallet.GetAccount(Validators[i]); - if (account?.HasKey == true) - { - MyIndex = i; - KeyPair = account.GetKey(); - break; - } - } - _header = null; - } - } -} diff --git a/neo/Consensus/ConsensusMessage.cs b/neo/Consensus/ConsensusMessage.cs deleted file mode 100644 index bf07dd2fbf..0000000000 --- a/neo/Consensus/ConsensusMessage.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Neo.IO; -using Neo.IO.Caching; -using System; -using System.IO; - -namespace Neo.Consensus -{ - internal abstract class ConsensusMessage : ISerializable - { - /// - /// Reflection cache for ConsensusMessageType - /// - private static ReflectionCache ReflectionCache = ReflectionCache.CreateFromEnum(); - - public readonly ConsensusMessageType Type; - public byte ViewNumber; - - public int Size => sizeof(ConsensusMessageType) + sizeof(byte); - - protected ConsensusMessage(ConsensusMessageType type) - { - this.Type = type; - } - - public virtual void Deserialize(BinaryReader reader) - { - if (Type != (ConsensusMessageType)reader.ReadByte()) - throw new FormatException(); - ViewNumber = reader.ReadByte(); - } - - public static ConsensusMessage DeserializeFrom(byte[] data) - { - ConsensusMessage message = ReflectionCache.CreateInstance(data[0]); - if (message == null) throw new FormatException(); - - using (MemoryStream ms = new MemoryStream(data, false)) - using (BinaryReader r = new BinaryReader(ms)) - { - message.Deserialize(r); - } - return message; - } - - public virtual void Serialize(BinaryWriter writer) - { - writer.Write((byte)Type); - writer.Write(ViewNumber); - } - } -} diff --git a/neo/Consensus/ConsensusMessageType.cs b/neo/Consensus/ConsensusMessageType.cs deleted file mode 100644 index b57dbc3214..0000000000 --- a/neo/Consensus/ConsensusMessageType.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Neo.IO.Caching; - -namespace Neo.Consensus -{ - internal enum ConsensusMessageType : byte - { - [ReflectionCache(typeof(ChangeView))] - ChangeView = 0x00, - [ReflectionCache(typeof(PrepareRequest))] - PrepareRequest = 0x20, - [ReflectionCache(typeof(PrepareResponse))] - PrepareResponse = 0x21, - } -} diff --git a/neo/Consensus/ConsensusService.cs b/neo/Consensus/ConsensusService.cs deleted file mode 100644 index 532d0d1722..0000000000 --- a/neo/Consensus/ConsensusService.cs +++ /dev/null @@ -1,372 +0,0 @@ -using Neo.Core; -using Neo.Cryptography; -using Neo.IO; -using Neo.Network; -using Neo.Network.Payloads; -using Neo.SmartContract; -using Neo.Wallets; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace Neo.Consensus -{ - public class ConsensusService : IDisposable - { - private ConsensusContext context = new ConsensusContext(); - private LocalNode localNode; - private Wallet wallet; - private Timer timer; - private uint timer_height; - private byte timer_view; - private DateTime block_received_time; - private bool started = false; - - public ConsensusService(LocalNode localNode, Wallet wallet) - { - this.localNode = localNode; - this.wallet = wallet; - this.timer = new Timer(OnTimeout, null, Timeout.Infinite, Timeout.Infinite); - } - - private bool AddTransaction(Transaction tx, bool verify) - { - if (Blockchain.Default.ContainsTransaction(tx.Hash) || - (verify && !tx.Verify(context.Transactions.Values)) || - !CheckPolicy(tx)) - { - Log($"reject tx: {tx.Hash}{Environment.NewLine}{tx.ToArray().ToHexString()}"); - RequestChangeView(); - return false; - } - context.Transactions[tx.Hash] = tx; - if (context.TransactionHashes.Length == context.Transactions.Count) - { - if (Blockchain.GetConsensusAddress(Blockchain.Default.GetValidators(context.Transactions.Values).ToArray()).Equals(context.NextConsensus)) - { - Log($"send perpare response"); - context.State |= ConsensusState.SignatureSent; - context.Signatures[context.MyIndex] = context.MakeHeader().Sign(context.KeyPair); - SignAndRelay(context.MakePrepareResponse(context.Signatures[context.MyIndex])); - CheckSignatures(); - } - else - { - RequestChangeView(); - return false; - } - } - return true; - } - - private void Blockchain_PersistCompleted(object sender, Block block) - { - Log($"persist block: {block.Hash}"); - block_received_time = DateTime.Now; - InitializeConsensus(0); - } - - private void CheckExpectedView(byte view_number) - { - if (context.ViewNumber == view_number) return; - if (context.ExpectedView.Count(p => p == view_number) >= context.M) - { - InitializeConsensus(view_number); - } - } - - protected virtual bool CheckPolicy(Transaction tx) - { - return true; - } - - private void CheckSignatures() - { - if (context.Signatures.Count(p => p != null) >= context.M && context.TransactionHashes.All(p => context.Transactions.ContainsKey(p))) - { - Contract contract = Contract.CreateMultiSigContract(context.M, context.Validators); - Block block = context.MakeHeader(); - ContractParametersContext sc = new ContractParametersContext(block); - for (int i = 0, j = 0; i < context.Validators.Length && j < context.M; i++) - if (context.Signatures[i] != null) - { - sc.AddSignature(contract, context.Validators[i], context.Signatures[i]); - j++; - } - sc.Verifiable.Scripts = sc.GetScripts(); - block.Transactions = context.TransactionHashes.Select(p => context.Transactions[p]).ToArray(); - Log($"relay block: {block.Hash}"); - if (!localNode.Relay(block)) - Log($"reject block: {block.Hash}"); - context.State |= ConsensusState.BlockSent; - } - } - - private MinerTransaction CreateMinerTransaction(IEnumerable transactions, uint height, ulong nonce) - { - Fixed8 amount_netfee = Block.CalculateNetFee(transactions); - TransactionOutput[] outputs = amount_netfee == Fixed8.Zero ? new TransactionOutput[0] : new[] { new TransactionOutput - { - AssetId = Blockchain.UtilityToken.Hash, - Value = amount_netfee, - ScriptHash = wallet.GetChangeAddress() - } }; - return new MinerTransaction - { - Nonce = (uint)(nonce % (uint.MaxValue + 1ul)), - Attributes = new TransactionAttribute[0], - Inputs = new CoinReference[0], - Outputs = outputs, - Scripts = new Witness[0] - }; - } - - public void Dispose() - { - Log("OnStop"); - if (timer != null) timer.Dispose(); - if (started) - { - Blockchain.PersistCompleted -= Blockchain_PersistCompleted; - LocalNode.InventoryReceiving -= LocalNode_InventoryReceiving; - LocalNode.InventoryReceived -= LocalNode_InventoryReceived; - } - } - - private static ulong GetNonce() - { - byte[] nonce = new byte[sizeof(ulong)]; - Random rand = new Random(); - rand.NextBytes(nonce); - return nonce.ToUInt64(0); - } - - private void InitializeConsensus(byte view_number) - { - lock (context) - { - if (view_number == 0) - context.Reset(wallet); - else - context.ChangeView(view_number); - if (context.MyIndex < 0) return; - Log($"initialize: height={context.BlockIndex} view={view_number} index={context.MyIndex} role={(context.MyIndex == context.PrimaryIndex ? ConsensusState.Primary : ConsensusState.Backup)}"); - if (context.MyIndex == context.PrimaryIndex) - { - context.State |= ConsensusState.Primary; - timer_height = context.BlockIndex; - timer_view = view_number; - TimeSpan span = DateTime.Now - block_received_time; - if (span >= Blockchain.TimePerBlock) - timer.Change(0, Timeout.Infinite); - else - timer.Change(Blockchain.TimePerBlock - span, Timeout.InfiniteTimeSpan); - } - else - { - context.State = ConsensusState.Backup; - timer_height = context.BlockIndex; - timer_view = view_number; - timer.Change(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (view_number + 1)), Timeout.InfiniteTimeSpan); - } - } - } - - private void LocalNode_InventoryReceived(object sender, IInventory inventory) - { - ConsensusPayload payload = inventory as ConsensusPayload; - if (payload != null) - { - lock (context) - { - if (payload.ValidatorIndex == context.MyIndex) return; - - if (payload.Version != ConsensusContext.Version) - return; - if (payload.PrevHash != context.PrevHash || payload.BlockIndex != context.BlockIndex) - { - // Request blocks - - if (Blockchain.Default?.Height + 1 < payload.BlockIndex) - { - Log($"chain sync: expected={payload.BlockIndex} current: {Blockchain.Default?.Height}"); - - localNode.RequestGetBlocks(); - } - - return; - } - - if (payload.ValidatorIndex >= context.Validators.Length) return; - ConsensusMessage message; - try - { - message = ConsensusMessage.DeserializeFrom(payload.Data); - } - catch - { - return; - } - if (message.ViewNumber != context.ViewNumber && message.Type != ConsensusMessageType.ChangeView) - return; - switch (message.Type) - { - case ConsensusMessageType.ChangeView: - OnChangeViewReceived(payload, (ChangeView)message); - break; - case ConsensusMessageType.PrepareRequest: - OnPrepareRequestReceived(payload, (PrepareRequest)message); - break; - case ConsensusMessageType.PrepareResponse: - OnPrepareResponseReceived(payload, (PrepareResponse)message); - break; - } - } - } - } - - private void LocalNode_InventoryReceiving(object sender, InventoryReceivingEventArgs e) - { - Transaction tx = e.Inventory as Transaction; - if (tx != null) - { - lock (context) - { - if (!context.State.HasFlag(ConsensusState.Backup) || !context.State.HasFlag(ConsensusState.RequestReceived) || context.State.HasFlag(ConsensusState.SignatureSent) || context.State.HasFlag(ConsensusState.ViewChanging)) - return; - if (context.Transactions.ContainsKey(tx.Hash)) return; - if (!context.TransactionHashes.Contains(tx.Hash)) return; - AddTransaction(tx, true); - e.Cancel = true; - } - } - } - - protected virtual void Log(string message) - { - } - - private void OnChangeViewReceived(ConsensusPayload payload, ChangeView message) - { - Log($"{nameof(OnChangeViewReceived)}: height={payload.BlockIndex} view={message.ViewNumber} index={payload.ValidatorIndex} nv={message.NewViewNumber}"); - if (message.NewViewNumber <= context.ExpectedView[payload.ValidatorIndex]) - return; - context.ExpectedView[payload.ValidatorIndex] = message.NewViewNumber; - CheckExpectedView(message.NewViewNumber); - } - - private void OnPrepareRequestReceived(ConsensusPayload payload, PrepareRequest message) - { - Log($"{nameof(OnPrepareRequestReceived)}: height={payload.BlockIndex} view={message.ViewNumber} index={payload.ValidatorIndex} tx={message.TransactionHashes.Length}"); - if (!context.State.HasFlag(ConsensusState.Backup) || context.State.HasFlag(ConsensusState.RequestReceived)) - return; - if (payload.ValidatorIndex != context.PrimaryIndex) return; - if (payload.Timestamp <= Blockchain.Default.GetHeader(context.PrevHash).Timestamp || payload.Timestamp > DateTime.Now.AddMinutes(10).ToTimestamp()) - { - Log($"Timestamp incorrect: {payload.Timestamp}"); - return; - } - context.State |= ConsensusState.RequestReceived; - context.Timestamp = payload.Timestamp; - context.Nonce = message.Nonce; - context.NextConsensus = message.NextConsensus; - context.TransactionHashes = message.TransactionHashes; - context.Transactions = new Dictionary(); - if (!Crypto.Default.VerifySignature(context.MakeHeader().GetHashData(), message.Signature, context.Validators[payload.ValidatorIndex].EncodePoint(false))) return; - context.Signatures = new byte[context.Validators.Length][]; - context.Signatures[payload.ValidatorIndex] = message.Signature; - Dictionary mempool = LocalNode.GetMemoryPool().ToDictionary(p => p.Hash); - foreach (UInt256 hash in context.TransactionHashes.Skip(1)) - { - if (mempool.TryGetValue(hash, out Transaction tx)) - if (!AddTransaction(tx, false)) - return; - } - if (!AddTransaction(message.MinerTransaction, true)) return; - LocalNode.AllowHashes(context.TransactionHashes.Except(context.Transactions.Keys)); - if (context.Transactions.Count < context.TransactionHashes.Length) - localNode.SynchronizeMemoryPool(); - } - - private void OnPrepareResponseReceived(ConsensusPayload payload, PrepareResponse message) - { - Log($"{nameof(OnPrepareResponseReceived)}: height={payload.BlockIndex} view={message.ViewNumber} index={payload.ValidatorIndex}"); - if (context.State.HasFlag(ConsensusState.BlockSent)) return; - if (context.Signatures[payload.ValidatorIndex] != null) return; - Block header = context.MakeHeader(); - if (header == null || !Crypto.Default.VerifySignature(header.GetHashData(), message.Signature, context.Validators[payload.ValidatorIndex].EncodePoint(false))) return; - context.Signatures[payload.ValidatorIndex] = message.Signature; - CheckSignatures(); - } - - private void OnTimeout(object state) - { - lock (context) - { - if (timer_height != context.BlockIndex || timer_view != context.ViewNumber) return; - Log($"timeout: height={timer_height} view={timer_view} state={context.State}"); - if (context.State.HasFlag(ConsensusState.Primary) && !context.State.HasFlag(ConsensusState.RequestSent)) - { - Log($"send perpare request: height={timer_height} view={timer_view}"); - context.State |= ConsensusState.RequestSent; - if (!context.State.HasFlag(ConsensusState.SignatureSent)) - { - context.Timestamp = Math.Max(DateTime.Now.ToTimestamp(), Blockchain.Default.GetHeader(context.PrevHash).Timestamp + 1); - context.Nonce = GetNonce(); - List transactions = LocalNode.GetMemoryPool().Where(p => CheckPolicy(p)).ToList(); - if (transactions.Count >= Settings.Default.MaxTransactionsPerBlock) - transactions = transactions.OrderByDescending(p => p.NetworkFee / p.Size).Take(Settings.Default.MaxTransactionsPerBlock - 1).ToList(); - transactions.Insert(0, CreateMinerTransaction(transactions, context.BlockIndex, context.Nonce)); - context.TransactionHashes = transactions.Select(p => p.Hash).ToArray(); - context.Transactions = transactions.ToDictionary(p => p.Hash); - context.NextConsensus = Blockchain.GetConsensusAddress(Blockchain.Default.GetValidators(transactions).ToArray()); - context.Signatures[context.MyIndex] = context.MakeHeader().Sign(context.KeyPair); - } - SignAndRelay(context.MakePrepareRequest()); - timer.Change(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (timer_view + 1)), Timeout.InfiniteTimeSpan); - } - else if ((context.State.HasFlag(ConsensusState.Primary) && context.State.HasFlag(ConsensusState.RequestSent)) || context.State.HasFlag(ConsensusState.Backup)) - { - RequestChangeView(); - } - } - } - - private void RequestChangeView() - { - context.State |= ConsensusState.ViewChanging; - context.ExpectedView[context.MyIndex]++; - Log($"request change view: height={context.BlockIndex} view={context.ViewNumber} nv={context.ExpectedView[context.MyIndex]} state={context.State}"); - timer.Change(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (context.ExpectedView[context.MyIndex] + 1)), Timeout.InfiniteTimeSpan); - SignAndRelay(context.MakeChangeView()); - CheckExpectedView(context.ExpectedView[context.MyIndex]); - } - - private void SignAndRelay(ConsensusPayload payload) - { - ContractParametersContext sc; - try - { - sc = new ContractParametersContext(payload); - wallet.Sign(sc); - } - catch (InvalidOperationException) - { - return; - } - sc.Verifiable.Scripts = sc.GetScripts(); - localNode.RelayDirectly(payload); - } - - public void Start() - { - Log("OnStart"); - started = true; - Blockchain.PersistCompleted += Blockchain_PersistCompleted; - LocalNode.InventoryReceiving += LocalNode_InventoryReceiving; - LocalNode.InventoryReceived += LocalNode_InventoryReceived; - InitializeConsensus(0); - } - } -} diff --git a/neo/Consensus/ConsensusState.cs b/neo/Consensus/ConsensusState.cs deleted file mode 100644 index 15b570d08b..0000000000 --- a/neo/Consensus/ConsensusState.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Neo.Consensus -{ - [Flags] - internal enum ConsensusState : byte - { - Initial = 0x00, - Primary = 0x01, - Backup = 0x02, - RequestSent = 0x04, - RequestReceived = 0x08, - SignatureSent = 0x10, - BlockSent = 0x20, - ViewChanging = 0x40, - } -} diff --git a/neo/Consensus/PrepareRequest.cs b/neo/Consensus/PrepareRequest.cs deleted file mode 100644 index a9e9032a54..0000000000 --- a/neo/Consensus/PrepareRequest.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Neo.Core; -using Neo.IO; -using System; -using System.IO; -using System.Linq; - -namespace Neo.Consensus -{ - internal class PrepareRequest : ConsensusMessage - { - public ulong Nonce; - public UInt160 NextConsensus; - public UInt256[] TransactionHashes; - public MinerTransaction MinerTransaction; - public byte[] Signature; - - public PrepareRequest() - : base(ConsensusMessageType.PrepareRequest) - { - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - Nonce = reader.ReadUInt64(); - NextConsensus = reader.ReadSerializable(); - TransactionHashes = reader.ReadSerializableArray(); - if (TransactionHashes.Distinct().Count() != TransactionHashes.Length) - throw new FormatException(); - MinerTransaction = reader.ReadSerializable(); - if (MinerTransaction.Hash != TransactionHashes[0]) - throw new FormatException(); - Signature = reader.ReadBytes(64); - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write(Nonce); - writer.Write(NextConsensus); - writer.Write(TransactionHashes); - writer.Write(MinerTransaction); - writer.Write(Signature); - } - } -} diff --git a/neo/Consensus/PrepareResponse.cs b/neo/Consensus/PrepareResponse.cs deleted file mode 100644 index d7c95c917a..0000000000 --- a/neo/Consensus/PrepareResponse.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.IO; - -namespace Neo.Consensus -{ - internal class PrepareResponse : ConsensusMessage - { - public byte[] Signature; - - public PrepareResponse() - : base(ConsensusMessageType.PrepareResponse) - { - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - Signature = reader.ReadBytes(64); - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write(Signature); - } - } -} diff --git a/neo/Core/AccountState.cs b/neo/Core/AccountState.cs deleted file mode 100644 index 75e50b54ad..0000000000 --- a/neo/Core/AccountState.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.IO.Json; -using Neo.VM; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - public class AccountState : StateBase, ICloneable - { - public UInt160 ScriptHash; - public bool IsFrozen; - public ECPoint[] Votes; - public Dictionary Balances; - - public override int Size => base.Size + ScriptHash.Size + sizeof(bool) + Votes.GetVarSize() - + IO.Helper.GetVarSize(Balances.Count) + Balances.Count * (32 + 8); - - public AccountState() { } - - public AccountState(UInt160 hash) - { - this.ScriptHash = hash; - this.IsFrozen = false; - this.Votes = new ECPoint[0]; - this.Balances = new Dictionary(); - } - - AccountState ICloneable.Clone() - { - return new AccountState - { - ScriptHash = ScriptHash, - IsFrozen = IsFrozen, - Votes = Votes, - Balances = Balances.ToDictionary(p => p.Key, p => p.Value) - }; - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - ScriptHash = reader.ReadSerializable(); - IsFrozen = reader.ReadBoolean(); - Votes = new ECPoint[reader.ReadVarInt()]; - for (int i = 0; i < Votes.Length; i++) - Votes[i] = ECPoint.DeserializeFrom(reader, ECCurve.Secp256r1); - int count = (int)reader.ReadVarInt(); - Balances = new Dictionary(count); - for (int i = 0; i < count; i++) - { - UInt256 assetId = reader.ReadSerializable(); - Fixed8 value = reader.ReadSerializable(); - Balances.Add(assetId, value); - } - } - - void ICloneable.FromReplica(AccountState replica) - { - ScriptHash = replica.ScriptHash; - IsFrozen = replica.IsFrozen; - Votes = replica.Votes; - Balances = replica.Balances; - } - - public Fixed8 GetBalance(UInt256 asset_id) - { - if (!Balances.TryGetValue(asset_id, out Fixed8 value)) - value = Fixed8.Zero; - return value; - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write(ScriptHash); - writer.Write(IsFrozen); - writer.Write(Votes); - var balances = Balances.Where(p => p.Value > Fixed8.Zero).ToArray(); - writer.WriteVarInt(balances.Length); - foreach (var pair in balances) - { - writer.Write(pair.Key); - writer.Write(pair.Value); - } - } - - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["script_hash"] = ScriptHash.ToString(); - json["frozen"] = IsFrozen; - json["votes"] = new JArray(Votes.Select(p => (JObject)p.ToString())); - json["balances"] = new JArray(Balances.Select(p => - { - JObject balance = new JObject(); - balance["asset"] = p.Key.ToString(); - balance["value"] = p.Value.ToString(); - return balance; - })); - return json; - } - } -} diff --git a/neo/Core/ApplicationExecutedEventArgs.cs b/neo/Core/ApplicationExecutedEventArgs.cs deleted file mode 100644 index aca7ddb75b..0000000000 --- a/neo/Core/ApplicationExecutedEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Neo.SmartContract; -using Neo.VM; -using System; -using System.Linq; - -namespace Neo.Core -{ - public class ApplicationExecutedEventArgs : EventArgs - { - public InvocationTransaction Transaction { get; } - public VMState VMState { get; } - public Fixed8 GasConsumed { get; } - public StackItem[] Stack { get; } - public NotifyEventArgs[] Notifications { get; } - - public ApplicationExecutedEventArgs(InvocationTransaction tx, NotifyEventArgs[] notifications, ApplicationEngine engine) - { - this.Transaction = tx; - this.VMState = engine.State; - this.GasConsumed = engine.GasConsumed; - this.Stack = engine.EvaluationStack.ToArray(); - this.Notifications = notifications; - } - } -} diff --git a/neo/Core/AssetState.cs b/neo/Core/AssetState.cs deleted file mode 100644 index f18529fdbc..0000000000 --- a/neo/Core/AssetState.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.IO.Json; -using Neo.VM; -using Neo.Wallets; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - public class AssetState : StateBase, ICloneable - { - public UInt256 AssetId; - public AssetType AssetType; - public string Name; - public Fixed8 Amount; - public Fixed8 Available; - public byte Precision; - public const byte FeeMode = 0; - public Fixed8 Fee; - public UInt160 FeeAddress; - public ECPoint Owner; - public UInt160 Admin; - public UInt160 Issuer; - public uint Expiration; - public bool IsFrozen; - - public override int Size => base.Size + AssetId.Size + sizeof(AssetType) + Name.GetVarSize() + Amount.Size + Available.Size + sizeof(byte) + sizeof(byte) + Fee.Size + FeeAddress.Size + Owner.Size + Admin.Size + Issuer.Size + sizeof(uint) + sizeof(bool); - - AssetState ICloneable.Clone() - { - return new AssetState - { - AssetId = AssetId, - AssetType = AssetType, - Name = Name, - Amount = Amount, - Available = Available, - Precision = Precision, - //FeeMode = FeeMode, - Fee = Fee, - FeeAddress = FeeAddress, - Owner = Owner, - Admin = Admin, - Issuer = Issuer, - Expiration = Expiration, - IsFrozen = IsFrozen, - _names = _names - }; - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - AssetId = reader.ReadSerializable(); - AssetType = (AssetType)reader.ReadByte(); - Name = reader.ReadVarString(); - Amount = reader.ReadSerializable(); - Available = reader.ReadSerializable(); - Precision = reader.ReadByte(); - reader.ReadByte(); //FeeMode - Fee = reader.ReadSerializable(); //Fee - FeeAddress = reader.ReadSerializable(); - Owner = ECPoint.DeserializeFrom(reader, ECCurve.Secp256r1); - Admin = reader.ReadSerializable(); - Issuer = reader.ReadSerializable(); - Expiration = reader.ReadUInt32(); - IsFrozen = reader.ReadBoolean(); - } - - void ICloneable.FromReplica(AssetState replica) - { - AssetId = replica.AssetId; - AssetType = replica.AssetType; - Name = replica.Name; - Amount = replica.Amount; - Available = replica.Available; - Precision = replica.Precision; - //FeeMode = replica.FeeMode; - Fee = replica.Fee; - FeeAddress = replica.FeeAddress; - Owner = replica.Owner; - Admin = replica.Admin; - Issuer = replica.Issuer; - Expiration = replica.Expiration; - IsFrozen = replica.IsFrozen; - _names = replica._names; - } - - private Dictionary _names; - public string GetName(CultureInfo culture = null) - { - if (AssetType == AssetType.GoverningToken) return "NEO"; - if (AssetType == AssetType.UtilityToken) return "NeoGas"; - if (_names == null) - { - JObject name_obj; - try - { - name_obj = JObject.Parse(Name); - } - catch (FormatException) - { - name_obj = Name; - } - if (name_obj is JString) - _names = new Dictionary { { new CultureInfo("en"), name_obj.AsString() } }; - else - _names = ((JArray)name_obj).Where(p => p.ContainsProperty("lang") && p.ContainsProperty("name")).ToDictionary(p => new CultureInfo(p["lang"].AsString()), p => p["name"].AsString()); - } - if (culture == null) culture = CultureInfo.CurrentCulture; - if (_names.TryGetValue(culture, out string name)) - { - return name; - } - else if (_names.TryGetValue(en, out name)) - { - return name; - } - else - { - return _names.Values.First(); - } - } - - private static readonly CultureInfo en = new CultureInfo("en"); - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write(AssetId); - writer.Write((byte)AssetType); - writer.WriteVarString(Name); - writer.Write(Amount); - writer.Write(Available); - writer.Write(Precision); - writer.Write(FeeMode); - writer.Write(Fee); - writer.Write(FeeAddress); - writer.Write(Owner); - writer.Write(Admin); - writer.Write(Issuer); - writer.Write(Expiration); - writer.Write(IsFrozen); - } - - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["id"] = AssetId.ToString(); - json["type"] = AssetType; - try - { - json["name"] = Name == "" ? null : JObject.Parse(Name); - } - catch (FormatException) - { - json["name"] = Name; - } - json["amount"] = Amount.ToString(); - json["available"] = Available.ToString(); - json["precision"] = Precision; - json["owner"] = Owner.ToString(); - json["admin"] = Wallet.ToAddress(Admin); - json["issuer"] = Wallet.ToAddress(Issuer); - json["expiration"] = Expiration; - json["frozen"] = IsFrozen; - return json; - } - - public override string ToString() - { - return GetName(); - } - } -} diff --git a/neo/Core/AssetType.cs b/neo/Core/AssetType.cs deleted file mode 100644 index b70f517d26..0000000000 --- a/neo/Core/AssetType.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Neo.Core -{ - /// - /// 资产类别 - /// - public enum AssetType : byte - { - CreditFlag = 0x40, - DutyFlag = 0x80, - - GoverningToken = 0x00, - UtilityToken = 0x01, - Currency = 0x08, - Share = DutyFlag | 0x10, - Invoice = DutyFlag | 0x18, - Token = CreditFlag | 0x20, - } -} diff --git a/neo/Core/Block.cs b/neo/Core/Block.cs deleted file mode 100644 index ead1e9e462..0000000000 --- a/neo/Core/Block.cs +++ /dev/null @@ -1,196 +0,0 @@ -using Neo.Cryptography; -using Neo.IO; -using Neo.IO.Json; -using Neo.Network; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - /// - /// 区块或区块头 - /// - public class Block : BlockBase, IInventory, IEquatable - { - /// - /// 交易列表 - /// - public Transaction[] Transactions; - - private Header _header = null; - /// - /// 该区块的区块头 - /// - public Header Header - { - get - { - if (_header == null) - { - _header = new Header - { - PrevHash = PrevHash, - MerkleRoot = MerkleRoot, - Timestamp = Timestamp, - Index = Index, - ConsensusData = ConsensusData, - NextConsensus = NextConsensus, - Script = Script - }; - } - return _header; - } - } - - /// - /// 资产清单的类型 - /// - InventoryType IInventory.InventoryType => InventoryType.Block; - - public override int Size => base.Size + Transactions.GetVarSize(); - - public static Fixed8 CalculateNetFee(IEnumerable transactions) - { - Transaction[] ts = transactions.Where(p => p.Type != TransactionType.MinerTransaction && p.Type != TransactionType.ClaimTransaction).ToArray(); - Fixed8 amount_in = ts.SelectMany(p => p.References.Values.Where(o => o.AssetId == Blockchain.UtilityToken.Hash)).Sum(p => p.Value); - Fixed8 amount_out = ts.SelectMany(p => p.Outputs.Where(o => o.AssetId == Blockchain.UtilityToken.Hash)).Sum(p => p.Value); - Fixed8 amount_sysfee = ts.Sum(p => p.SystemFee); - return amount_in - amount_out - amount_sysfee; - } - - /// - /// 反序列化 - /// - /// 数据来源 - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - Transactions = new Transaction[reader.ReadVarInt(0x10000)]; - if (Transactions.Length == 0) throw new FormatException(); - for (int i = 0; i < Transactions.Length; i++) - { - Transactions[i] = Transaction.DeserializeFrom(reader); - } - if (MerkleTree.ComputeRoot(Transactions.Select(p => p.Hash).ToArray()) != MerkleRoot) - throw new FormatException(); - } - - /// - /// 比较当前区块与指定区块是否相等 - /// - /// 要比较的区块 - /// 返回对象是否相等 - public bool Equals(Block other) - { - if (ReferenceEquals(this, other)) return true; - if (ReferenceEquals(null, other)) return false; - return Hash.Equals(other.Hash); - } - - /// - /// 比较当前区块与指定区块是否相等 - /// - /// 要比较的区块 - /// 返回对象是否相等 - public override bool Equals(object obj) - { - return Equals(obj as Block); - } - - public static Block FromTrimmedData(byte[] data, int index, Func txSelector) - { - Block block = new Block(); - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - using (BinaryReader reader = new BinaryReader(ms)) - { - ((IVerifiable)block).DeserializeUnsigned(reader); - reader.ReadByte(); block.Script = reader.ReadSerializable(); - block.Transactions = new Transaction[reader.ReadVarInt(0x10000000)]; - for (int i = 0; i < block.Transactions.Length; i++) - { - block.Transactions[i] = txSelector(reader.ReadSerializable()); - } - } - return block; - } - - /// - /// 获得区块的HashCode - /// - /// 返回区块的HashCode - public override int GetHashCode() - { - return Hash.GetHashCode(); - } - - /// - /// 根据区块中所有交易的Hash生成MerkleRoot - /// - public void RebuildMerkleRoot() - { - MerkleRoot = MerkleTree.ComputeRoot(Transactions.Select(p => p.Hash).ToArray()); - } - - /// - /// 序列化 - /// - /// 存放序列化后的数据 - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write(Transactions); - } - - /// - /// 变成json对象 - /// - /// 返回json对象 - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["tx"] = Transactions.Select(p => p.ToJson()).ToArray(); - return json; - } - - /// - /// 把区块对象变为只包含区块头和交易Hash的字节数组,去除交易数据 - /// - /// 返回只包含区块头和交易Hash的字节数组 - public byte[] Trim() - { - using (MemoryStream ms = new MemoryStream()) - using (BinaryWriter writer = new BinaryWriter(ms)) - { - ((IVerifiable)this).SerializeUnsigned(writer); - writer.Write((byte)1); writer.Write(Script); - writer.Write(Transactions.Select(p => p.Hash).ToArray()); - writer.Flush(); - return ms.ToArray(); - } - } - - /// - /// 验证该区块是否合法 - /// - /// 是否同时验证区块中的每一笔交易 - /// 返回该区块的合法性,返回true即为合法,否则,非法。 - public bool Verify(bool completely) - { - if (!Verify()) return false; - if (Transactions[0].Type != TransactionType.MinerTransaction || Transactions.Skip(1).Any(p => p.Type == TransactionType.MinerTransaction)) - return false; - if (completely) - { - if (NextConsensus != Blockchain.GetConsensusAddress(Blockchain.Default.GetValidators(Transactions).ToArray())) - return false; - foreach (Transaction tx in Transactions) - if (!tx.Verify(Transactions.Where(p => !p.Hash.Equals(tx.Hash)))) return false; - Transaction tx_gen = Transactions.FirstOrDefault(p => p.Type == TransactionType.MinerTransaction); - if (tx_gen?.Outputs.Sum(p => p.Value) != CalculateNetFee(Transactions)) return false; - } - return true; - } - } -} diff --git a/neo/Core/BlockBase.cs b/neo/Core/BlockBase.cs deleted file mode 100644 index 9b64c43a10..0000000000 --- a/neo/Core/BlockBase.cs +++ /dev/null @@ -1,148 +0,0 @@ -using Neo.Cryptography; -using Neo.IO; -using Neo.IO.Json; -using Neo.VM; -using Neo.Wallets; -using System; -using System.IO; - -namespace Neo.Core -{ - public abstract class BlockBase : IVerifiable - { - /// - /// 区块版本 - /// - public uint Version; - /// - /// 前一个区块的散列值 - /// - public UInt256 PrevHash; - /// - /// 该区块中所有交易的Merkle树的根 - /// - public UInt256 MerkleRoot; - /// - /// 时间戳 - /// - public uint Timestamp; - /// - /// 区块高度 - /// - public uint Index; - public ulong ConsensusData; - /// - /// 下一个区块的记账合约的散列值 - /// - public UInt160 NextConsensus; - /// - /// 用于验证该区块的脚本 - /// - public Witness Script; - - private UInt256 _hash = null; - public UInt256 Hash - { - get - { - if (_hash == null) - { - _hash = new UInt256(Crypto.Default.Hash256(this.GetHashData())); - } - return _hash; - } - } - - Witness[] IVerifiable.Scripts - { - get - { - return new[] { Script }; - } - set - { - if (value.Length != 1) throw new ArgumentException(); - Script = value[0]; - } - } - - public virtual int Size => sizeof(uint) + PrevHash.Size + MerkleRoot.Size + sizeof(uint) + sizeof(uint) + sizeof(ulong) + NextConsensus.Size + 1 + Script.Size; - - public virtual void Deserialize(BinaryReader reader) - { - ((IVerifiable)this).DeserializeUnsigned(reader); - if (reader.ReadByte() != 1) throw new FormatException(); - Script = reader.ReadSerializable(); - } - - void IVerifiable.DeserializeUnsigned(BinaryReader reader) - { - Version = reader.ReadUInt32(); - PrevHash = reader.ReadSerializable(); - MerkleRoot = reader.ReadSerializable(); - Timestamp = reader.ReadUInt32(); - Index = reader.ReadUInt32(); - ConsensusData = reader.ReadUInt64(); - NextConsensus = reader.ReadSerializable(); - } - - byte[] IScriptContainer.GetMessage() - { - return this.GetHashData(); - } - - UInt160[] IVerifiable.GetScriptHashesForVerifying() - { - if (PrevHash == UInt256.Zero) - return new[] { Script.ScriptHash }; - Header prev_header = Blockchain.Default.GetHeader(PrevHash); - if (prev_header == null) throw new InvalidOperationException(); - return new UInt160[] { prev_header.NextConsensus }; - } - - public virtual void Serialize(BinaryWriter writer) - { - ((IVerifiable)this).SerializeUnsigned(writer); - writer.Write((byte)1); writer.Write(Script); - } - - void IVerifiable.SerializeUnsigned(BinaryWriter writer) - { - writer.Write(Version); - writer.Write(PrevHash); - writer.Write(MerkleRoot); - writer.Write(Timestamp); - writer.Write(Index); - writer.Write(ConsensusData); - writer.Write(NextConsensus); - } - - public virtual JObject ToJson() - { - JObject json = new JObject(); - json["hash"] = Hash.ToString(); - json["size"] = Size; - json["version"] = Version; - json["previousblockhash"] = PrevHash.ToString(); - json["merkleroot"] = MerkleRoot.ToString(); - json["time"] = Timestamp; - json["index"] = Index; - json["nonce"] = ConsensusData.ToString("x16"); - json["nextconsensus"] = Wallet.ToAddress(NextConsensus); - json["script"] = Script.ToJson(); - return json; - } - - public bool Verify() - { - if (Hash == Blockchain.GenesisBlock.Hash) return true; - if (Blockchain.Default.ContainsBlock(Hash)) return true; - Header prev_header = Blockchain.Default.GetHeader(PrevHash); - if (prev_header == null) return false; - if (prev_header.Index + 1 != Index) return false; - if (prev_header.Timestamp >= Timestamp) return false; - if (!this.VerifyScripts()) return false; - return true; - } - } -} diff --git a/neo/Core/Blockchain.cs b/neo/Core/Blockchain.cs deleted file mode 100644 index 0d752220db..0000000000 --- a/neo/Core/Blockchain.cs +++ /dev/null @@ -1,585 +0,0 @@ -using Neo.Cryptography; -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.IO.Caching; -using Neo.SmartContract; -using Neo.VM; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Neo.Core -{ - /// - /// 实现区块链功能的基类 - /// - public abstract class Blockchain : IDisposable, IScriptTable - { - public static event EventHandler PersistCompleted; - - /// - /// 产生每个区块的时间间隔,已秒为单位 - /// - public const uint SecondsPerBlock = 15; - public const uint DecrementInterval = 2000000; - public const uint MaxValidators = 1024; - public static readonly uint[] GenerationAmount = { 8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; - /// - /// 产生每个区块的时间间隔 - /// - public static readonly TimeSpan TimePerBlock = TimeSpan.FromSeconds(SecondsPerBlock); - /// - /// 后备记账人列表 - /// - public static readonly ECPoint[] StandbyValidators = Settings.Default.StandbyValidators.OfType().Select(p => ECPoint.DecodePoint(p.HexToBytes(), ECCurve.Secp256r1)).ToArray(); - - /// - /// Return true if haven't got valid handle - /// - public abstract bool IsDisposed { get; } - -#pragma warning disable CS0612 - public static readonly RegisterTransaction GoverningToken = new RegisterTransaction - { - AssetType = AssetType.GoverningToken, - Name = "[{\"lang\":\"zh-CN\",\"name\":\"小蚁股\"},{\"lang\":\"en\",\"name\":\"AntShare\"}]", - Amount = Fixed8.FromDecimal(100000000), - Precision = 0, - Owner = ECCurve.Secp256r1.Infinity, - Admin = (new[] { (byte)OpCode.PUSHT }).ToScriptHash(), - Attributes = new TransactionAttribute[0], - Inputs = new CoinReference[0], - Outputs = new TransactionOutput[0], - Scripts = new Witness[0] - }; - - public static readonly RegisterTransaction UtilityToken = new RegisterTransaction - { - AssetType = AssetType.UtilityToken, - Name = "[{\"lang\":\"zh-CN\",\"name\":\"小蚁币\"},{\"lang\":\"en\",\"name\":\"AntCoin\"}]", - Amount = Fixed8.FromDecimal(GenerationAmount.Sum(p => p) * DecrementInterval), - Precision = 8, - Owner = ECCurve.Secp256r1.Infinity, - Admin = (new[] { (byte)OpCode.PUSHF }).ToScriptHash(), - Attributes = new TransactionAttribute[0], - Inputs = new CoinReference[0], - Outputs = new TransactionOutput[0], - Scripts = new Witness[0] - }; -#pragma warning restore CS0612 - - /// - /// 创世区块 - /// - public static readonly Block GenesisBlock = new Block - { - PrevHash = UInt256.Zero, - Timestamp = (new DateTime(2016, 7, 15, 15, 8, 21, DateTimeKind.Utc)).ToTimestamp(), - Index = 0, - ConsensusData = 2083236893, //向比特币致敬 - NextConsensus = GetConsensusAddress(StandbyValidators), - Script = new Witness - { - InvocationScript = new byte[0], - VerificationScript = new[] { (byte)OpCode.PUSHT } - }, - Transactions = new Transaction[] - { - new MinerTransaction - { - Nonce = 2083236893, - Attributes = new TransactionAttribute[0], - Inputs = new CoinReference[0], - Outputs = new TransactionOutput[0], - Scripts = new Witness[0] - }, - GoverningToken, - UtilityToken, - new IssueTransaction - { - Attributes = new TransactionAttribute[0], - Inputs = new CoinReference[0], - Outputs = new[] - { - new TransactionOutput - { - AssetId = GoverningToken.Hash, - Value = GoverningToken.Amount, - ScriptHash = Contract.CreateMultiSigRedeemScript(StandbyValidators.Length / 2 + 1, StandbyValidators).ToScriptHash() - } - }, - Scripts = new[] - { - new Witness - { - InvocationScript = new byte[0], - VerificationScript = new[] { (byte)OpCode.PUSHT } - } - } - } - } - }; - - /// - /// 当前最新区块散列值 - /// - public abstract UInt256 CurrentBlockHash { get; } - /// - /// 当前最新区块头的散列值 - /// - public abstract UInt256 CurrentHeaderHash { get; } - /// - /// 默认的区块链实例 - /// - public static Blockchain Default { get; private set; } = null; - /// - /// 区块头高度 - /// - public abstract uint HeaderHeight { get; } - /// - /// 区块高度 - /// - public abstract uint Height { get; } - - static Blockchain() - { - GenesisBlock.RebuildMerkleRoot(); - } - - /// - /// 将指定的区块添加到区块链中 - /// - /// 要添加的区块 - /// 返回是否添加成功 - public abstract bool AddBlock(Block block); - - /// - /// 将指定的区块头添加到区块头链中 - /// - /// 要添加的区块头列表 - protected internal abstract void AddHeaders(IEnumerable
headers); - - public static Fixed8 CalculateBonus(IEnumerable inputs, bool ignoreClaimed = true) - { - List unclaimed = new List(); - foreach (var group in inputs.GroupBy(p => p.PrevHash)) - { - Dictionary claimable = Default.GetUnclaimed(group.Key); - if (claimable == null || claimable.Count == 0) - if (ignoreClaimed) - continue; - else - throw new ArgumentException(); - foreach (CoinReference claim in group) - { - if (!claimable.TryGetValue(claim.PrevIndex, out SpentCoin claimed)) - if (ignoreClaimed) - continue; - else - throw new ArgumentException(); - unclaimed.Add(claimed); - } - } - return CalculateBonusInternal(unclaimed); - } - - public static Fixed8 CalculateBonus(IEnumerable inputs, uint height_end) - { - List unclaimed = new List(); - foreach (var group in inputs.GroupBy(p => p.PrevHash)) - { - Transaction tx = Default.GetTransaction(group.Key, out int height_start); - if (tx == null) throw new ArgumentException(); - if (height_start == height_end) continue; - foreach (CoinReference claim in group) - { - if (claim.PrevIndex >= tx.Outputs.Length || !tx.Outputs[claim.PrevIndex].AssetId.Equals(GoverningToken.Hash)) - throw new ArgumentException(); - unclaimed.Add(new SpentCoin - { - Output = tx.Outputs[claim.PrevIndex], - StartHeight = (uint)height_start, - EndHeight = height_end - }); - } - } - return CalculateBonusInternal(unclaimed); - } - - private static Fixed8 CalculateBonusInternal(IEnumerable unclaimed) - { - Fixed8 amount_claimed = Fixed8.Zero; - foreach (var group in unclaimed.GroupBy(p => new { p.StartHeight, p.EndHeight })) - { - uint amount = 0; - uint ustart = group.Key.StartHeight / DecrementInterval; - if (ustart < GenerationAmount.Length) - { - uint istart = group.Key.StartHeight % DecrementInterval; - uint uend = group.Key.EndHeight / DecrementInterval; - uint iend = group.Key.EndHeight % DecrementInterval; - if (uend >= GenerationAmount.Length) - { - uend = (uint)GenerationAmount.Length; - iend = 0; - } - if (iend == 0) - { - uend--; - iend = DecrementInterval; - } - while (ustart < uend) - { - amount += (DecrementInterval - istart) * GenerationAmount[ustart]; - ustart++; - istart = 0; - } - amount += (iend - istart) * GenerationAmount[ustart]; - } - amount += (uint)(Default.GetSysFeeAmount(group.Key.EndHeight - 1) - (group.Key.StartHeight == 0 ? 0 : Default.GetSysFeeAmount(group.Key.StartHeight - 1))); - amount_claimed += group.Sum(p => p.Value) / 100000000 * amount; - } - return amount_claimed; - } - - /// - /// 判断区块链中是否包含指定的区块 - /// - /// 区块编号 - /// 如果包含指定区块则返回true - public abstract bool ContainsBlock(UInt256 hash); - - /// - /// 判断区块链中是否包含指定的交易 - /// - /// 交易编号 - /// 如果包含指定交易则返回true - public abstract bool ContainsTransaction(UInt256 hash); - - public bool ContainsUnspent(CoinReference input) - { - return ContainsUnspent(input.PrevHash, input.PrevIndex); - } - - public abstract bool ContainsUnspent(UInt256 hash, ushort index); - - public abstract MetaDataCache GetMetaData() where T : class, ISerializable, new(); - - public abstract DataCache GetStates() - where TKey : IEquatable, ISerializable, new() - where TValue : StateBase, ICloneable, new(); - - public abstract void Dispose(); - - public abstract AccountState GetAccountState(UInt160 script_hash); - - public abstract AssetState GetAssetState(UInt256 asset_id); - - /// - /// 根据指定的高度,返回对应的区块信息 - /// - /// 区块高度 - /// 返回对应的区块信息 - public Block GetBlock(uint height) - { - UInt256 hash = GetBlockHash(height); - if (hash == null) return null; - return GetBlock(hash); - } - - /// - /// 根据指定的散列值,返回对应的区块信息 - /// - /// 散列值 - /// 返回对应的区块信息 - public abstract Block GetBlock(UInt256 hash); - - /// - /// 根据指定的高度,返回对应区块的散列值 - /// - /// 区块高度 - /// 返回对应区块的散列值 - public abstract UInt256 GetBlockHash(uint height); - - public abstract ContractState GetContract(UInt160 hash); - - public abstract IEnumerable GetEnrollments(); - - /// - /// 根据指定的高度,返回对应的区块头信息 - /// - /// 区块高度 - /// 返回对应的区块头信息 - public abstract Header GetHeader(uint height); - - /// - /// 根据指定的散列值,返回对应的区块头信息 - /// - /// 散列值 - /// 返回对应的区块头信息 - public abstract Header GetHeader(UInt256 hash); - - /// - /// 获取记账人的合约地址 - /// - /// 记账人的公钥列表 - /// 返回记账人的合约地址 - public static UInt160 GetConsensusAddress(ECPoint[] validators) - { - return Contract.CreateMultiSigRedeemScript(validators.Length - (validators.Length - 1) / 3, validators).ToScriptHash(); - } - - private List _validators = new List(); - /// - /// 获取下一个区块的记账人列表 - /// - /// 返回一组公钥,表示下一个区块的记账人列表 - public ECPoint[] GetValidators() - { - lock (_validators) - { - if (_validators.Count == 0) - { - _validators.AddRange(GetValidators(Enumerable.Empty())); - } - return _validators.ToArray(); - } - } - - public virtual IEnumerable GetValidators(IEnumerable others) - { - DataCache accounts = GetStates(); - DataCache validators = GetStates(); - MetaDataCache validators_count = GetMetaData(); - foreach (Transaction tx in others) - { - foreach (TransactionOutput output in tx.Outputs) - { - AccountState account = accounts.GetAndChange(output.ScriptHash, () => new AccountState(output.ScriptHash)); - if (account.Balances.ContainsKey(output.AssetId)) - account.Balances[output.AssetId] += output.Value; - else - account.Balances[output.AssetId] = output.Value; - if (output.AssetId.Equals(GoverningToken.Hash) && account.Votes.Length > 0) - { - foreach (ECPoint pubkey in account.Votes) - validators.GetAndChange(pubkey, () => new ValidatorState(pubkey)).Votes += output.Value; - validators_count.GetAndChange().Votes[account.Votes.Length - 1] += output.Value; - } - } - foreach (var group in tx.Inputs.GroupBy(p => p.PrevHash)) - { - Transaction tx_prev = GetTransaction(group.Key, out int height); - foreach (CoinReference input in group) - { - TransactionOutput out_prev = tx_prev.Outputs[input.PrevIndex]; - AccountState account = accounts.GetAndChange(out_prev.ScriptHash); - if (out_prev.AssetId.Equals(GoverningToken.Hash)) - { - if (account.Votes.Length > 0) - { - foreach (ECPoint pubkey in account.Votes) - { - ValidatorState validator = validators.GetAndChange(pubkey); - validator.Votes -= out_prev.Value; - if (!validator.Registered && validator.Votes.Equals(Fixed8.Zero)) - validators.Delete(pubkey); - } - validators_count.GetAndChange().Votes[account.Votes.Length - 1] -= out_prev.Value; - } - } - account.Balances[out_prev.AssetId] -= out_prev.Value; - } - } - switch (tx) - { -#pragma warning disable CS0612 - case EnrollmentTransaction tx_enrollment: - validators.GetAndChange(tx_enrollment.PublicKey, () => new ValidatorState(tx_enrollment.PublicKey)).Registered = true; - break; -#pragma warning restore CS0612 - case StateTransaction tx_state: - foreach (StateDescriptor descriptor in tx_state.Descriptors) - switch (descriptor.Type) - { - case StateType.Account: - ProcessAccountStateDescriptor(descriptor, accounts, validators, validators_count); - break; - case StateType.Validator: - ProcessValidatorStateDescriptor(descriptor, validators); - break; - } - break; - } - } - int count = (int)validators_count.Get().Votes.Select((p, i) => new - { - Count = i, - Votes = p - }).Where(p => p.Votes > Fixed8.Zero).ToArray().WeightedFilter(0.25, 0.75, p => p.Votes.GetData(), (p, w) => new - { - p.Count, - Weight = w - }).WeightedAverage(p => p.Count, p => p.Weight); - count = Math.Max(count, StandbyValidators.Length); - HashSet sv = new HashSet(StandbyValidators); - ECPoint[] pubkeys = validators.Find().Select(p => p.Value).Where(p => (p.Registered && p.Votes > Fixed8.Zero) || sv.Contains(p.PublicKey)).OrderByDescending(p => p.Votes).ThenBy(p => p.PublicKey).Select(p => p.PublicKey).Take(count).ToArray(); - IEnumerable result; - if (pubkeys.Length == count) - { - result = pubkeys; - } - else - { - HashSet hashSet = new HashSet(pubkeys); - for (int i = 0; i < StandbyValidators.Length && hashSet.Count < count; i++) - hashSet.Add(StandbyValidators[i]); - result = hashSet; - } - return result.OrderBy(p => p); - } - - /// - /// 根据指定的散列值,返回下一个区块的信息 - /// - /// 散列值 - /// 返回下一个区块的信息> - public abstract Block GetNextBlock(UInt256 hash); - - /// - /// 根据指定的散列值,返回下一个区块的散列值 - /// - /// 散列值 - /// 返回下一个区块的散列值 - public abstract UInt256 GetNextBlockHash(UInt256 hash); - - byte[] IScriptTable.GetScript(byte[] script_hash) - { - return GetContract(new UInt160(script_hash)).Script; - } - - public abstract StorageItem GetStorageItem(StorageKey key); - - /// - /// 根据指定的区块高度,返回对应区块及之前所有区块中包含的系统费用的总量 - /// - /// 区块高度 - /// 返回对应的系统费用的总量 - public virtual long GetSysFeeAmount(uint height) - { - return GetSysFeeAmount(GetBlockHash(height)); - } - - /// - /// 根据指定的区块散列值,返回对应区块及之前所有区块中包含的系统费用的总量 - /// - /// 散列值 - /// 返回系统费用的总量 - public abstract long GetSysFeeAmount(UInt256 hash); - - /// - /// 根据指定的散列值,返回对应的交易信息 - /// - /// 散列值 - /// 返回对应的交易信息 - public Transaction GetTransaction(UInt256 hash) - { - return GetTransaction(hash, out _); - } - - /// - /// 根据指定的散列值,返回对应的交易信息与该交易所在区块的高度 - /// - /// 交易散列值 - /// 返回该交易所在区块的高度 - /// 返回对应的交易信息 - public abstract Transaction GetTransaction(UInt256 hash, out int height); - - public abstract Dictionary GetUnclaimed(UInt256 hash); - - /// - /// 根据指定的散列值和索引,获取对应的未花费的资产 - /// - /// 交易散列值 - /// 输出的索引 - /// 返回一个交易输出,表示一个未花费的资产 - public abstract TransactionOutput GetUnspent(UInt256 hash, ushort index); - - public abstract IEnumerable GetUnspent(UInt256 hash); - - /// - /// 判断交易是否双花 - /// - /// 交易 - /// 返回交易是否双花 - public abstract bool IsDoubleSpend(Transaction tx); - - /// - /// 当区块被写入到硬盘后调用 - /// - /// 区块 - protected void OnPersistCompleted(Block block) - { - lock (_validators) - { - _validators.Clear(); - } - PersistCompleted?.Invoke(this, block); - } - - protected void ProcessAccountStateDescriptor(StateDescriptor descriptor, DataCache accounts, DataCache validators, MetaDataCache validators_count) - { - UInt160 hash = new UInt160(descriptor.Key); - AccountState account = accounts.GetAndChange(hash, () => new AccountState(hash)); - switch (descriptor.Field) - { - case "Votes": - Fixed8 balance = account.GetBalance(GoverningToken.Hash); - foreach (ECPoint pubkey in account.Votes) - { - ValidatorState validator = validators.GetAndChange(pubkey); - validator.Votes -= balance; - if (!validator.Registered && validator.Votes.Equals(Fixed8.Zero)) - validators.Delete(pubkey); - } - ECPoint[] votes = descriptor.Value.AsSerializableArray().Distinct().ToArray(); - if (votes.Length != account.Votes.Length) - { - ValidatorsCountState count_state = validators_count.GetAndChange(); - if (account.Votes.Length > 0) - count_state.Votes[account.Votes.Length - 1] -= balance; - if (votes.Length > 0) - count_state.Votes[votes.Length - 1] += balance; - } - account.Votes = votes; - foreach (ECPoint pubkey in account.Votes) - validators.GetAndChange(pubkey, () => new ValidatorState(pubkey)).Votes += balance; - break; - } - } - - protected void ProcessValidatorStateDescriptor(StateDescriptor descriptor, DataCache validators) - { - ECPoint pubkey = ECPoint.DecodePoint(descriptor.Key, ECCurve.Secp256r1); - ValidatorState validator = validators.GetAndChange(pubkey, () => new ValidatorState(pubkey)); - switch (descriptor.Field) - { - case "Registered": - validator.Registered = BitConverter.ToBoolean(descriptor.Value, 0); - break; - } - } - - /// - /// 注册默认的区块链实例 - /// - /// 区块链实例 - /// 返回注册后的区块链实例 - public static Blockchain RegisterBlockchain(Blockchain blockchain) - { - if (Default != null) Default.Dispose(); - Default = blockchain ?? throw new ArgumentNullException(); - return blockchain; - } - } -} diff --git a/neo/Core/ClaimTransaction.cs b/neo/Core/ClaimTransaction.cs deleted file mode 100644 index d544c88d95..0000000000 --- a/neo/Core/ClaimTransaction.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Neo.IO; -using Neo.IO.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - public class ClaimTransaction : Transaction - { - public CoinReference[] Claims; - - public override Fixed8 NetworkFee => Fixed8.Zero; - - public override int Size => base.Size + Claims.GetVarSize(); - - public ClaimTransaction() - : base(TransactionType.ClaimTransaction) - { - } - - /// - /// 反序列化交易中的额外数据 - /// - /// 数据来源 - protected override void DeserializeExclusiveData(BinaryReader reader) - { - if (Version != 0) throw new FormatException(); - Claims = reader.ReadSerializableArray(); - if (Claims.Length == 0) throw new FormatException(); - } - - /// - /// 获得需要校验的脚本Hash - /// - /// 返回需要校验的脚本Hash - public override UInt160[] GetScriptHashesForVerifying() - { - HashSet hashes = new HashSet(base.GetScriptHashesForVerifying()); - foreach (var group in Claims.GroupBy(p => p.PrevHash)) - { - Transaction tx = Blockchain.Default.GetTransaction(group.Key); - if (tx == null) throw new InvalidOperationException(); - foreach (CoinReference claim in group) - { - if (tx.Outputs.Length <= claim.PrevIndex) throw new InvalidOperationException(); - hashes.Add(tx.Outputs[claim.PrevIndex].ScriptHash); - } - } - return hashes.OrderBy(p => p).ToArray(); - } - - /// - /// 序列化交易中的额外数据 - /// - /// 存放序列化后的结果 - protected override void SerializeExclusiveData(BinaryWriter writer) - { - writer.Write(Claims); - } - - /// - /// 变成json对象 - /// - /// 返回json对象 - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["claims"] = new JArray(Claims.Select(p => p.ToJson()).ToArray()); - return json; - } - - /// - /// 验证交易 - /// - /// 返回验证结果 - public override bool Verify(IEnumerable mempool) - { - if (!base.Verify(mempool)) return false; - if (Claims.Length != Claims.Distinct().Count()) - return false; - if (mempool.OfType().Where(p => p != this).SelectMany(p => p.Claims).Intersect(Claims).Count() > 0) - return false; - TransactionResult result = GetTransactionResults().FirstOrDefault(p => p.AssetId == Blockchain.UtilityToken.Hash); - if (result == null || result.Amount > Fixed8.Zero) return false; - try - { - return Blockchain.CalculateBonus(Claims, false) == -result.Amount; - } - catch (ArgumentException) - { - return false; - } - catch (NotSupportedException) - { - return false; - } - } - } -} diff --git a/neo/Core/CoinReference.cs b/neo/Core/CoinReference.cs deleted file mode 100644 index 4145fbe20e..0000000000 --- a/neo/Core/CoinReference.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Neo.IO; -using Neo.IO.Json; -using Neo.VM; -using System; -using System.IO; - -namespace Neo.Core -{ - /// - /// 交易输入 - /// - public class CoinReference : IEquatable, IInteropInterface, ISerializable - { - /// - /// 引用交易的散列值 - /// - public UInt256 PrevHash; - /// - /// 引用交易输出的索引 - /// - public ushort PrevIndex; - - public int Size => PrevHash.Size + sizeof(ushort); - - void ISerializable.Deserialize(BinaryReader reader) - { - PrevHash = reader.ReadSerializable(); - PrevIndex = reader.ReadUInt16(); - } - - /// - /// 比较当前对象与指定对象是否相等 - /// - /// 要比较的对象 - /// 返回对象是否相等 - public bool Equals(CoinReference other) - { - if (ReferenceEquals(this, other)) return true; - if (ReferenceEquals(null, other)) return false; - return PrevHash.Equals(other.PrevHash) && PrevIndex.Equals(other.PrevIndex); - } - - /// - /// 比较当前对象与指定对象是否相等 - /// - /// 要比较的对象 - /// 返回对象是否相等 - public override bool Equals(object obj) - { - if (ReferenceEquals(this, obj)) return true; - if (ReferenceEquals(null, obj)) return false; - if (!(obj is CoinReference)) return false; - return Equals((CoinReference)obj); - } - - /// - /// 获得对象的HashCode - /// - /// 返回对象的HashCode - public override int GetHashCode() - { - return PrevHash.GetHashCode() + PrevIndex.GetHashCode(); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(PrevHash); - writer.Write(PrevIndex); - } - - /// - /// 将交易输入转变为json对象 - /// - /// 返回json对象 - public JObject ToJson() - { - JObject json = new JObject(); - json["txid"] = PrevHash.ToString(); - json["vout"] = PrevIndex; - return json; - } - } -} diff --git a/neo/Core/CoinState.cs b/neo/Core/CoinState.cs deleted file mode 100644 index 85300e5763..0000000000 --- a/neo/Core/CoinState.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Neo.Core -{ - [Flags] - public enum CoinState : byte - { - Unconfirmed = 0, - - Confirmed = 1 << 0, - Spent = 1 << 1, - //Vote = 1 << 2, - Claimed = 1 << 3, - //Locked = 1 << 4, - Frozen = 1 << 5, - //WatchOnly = 1 << 6, - } -} diff --git a/neo/Core/ContractPropertyState.cs b/neo/Core/ContractPropertyState.cs deleted file mode 100644 index 25a675a279..0000000000 --- a/neo/Core/ContractPropertyState.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Neo.Core -{ - [Flags] - public enum ContractPropertyState : byte - { - NoProperty = 0, - - HasStorage = 1 << 0, - HasDynamicInvoke = 1 << 1, - } -} diff --git a/neo/Core/ContractState.cs b/neo/Core/ContractState.cs deleted file mode 100644 index f2586edae1..0000000000 --- a/neo/Core/ContractState.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Neo.IO; -using Neo.IO.Json; -using Neo.SmartContract; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - public class ContractState : StateBase, ICloneable - { - public byte[] Script; - public ContractParameterType[] ParameterList; - public ContractParameterType ReturnType; - public ContractPropertyState ContractProperties; - public string Name; - public string CodeVersion; - public string Author; - public string Email; - public string Description; - - - public bool HasStorage => ContractProperties.HasFlag(ContractPropertyState.HasStorage); - - public bool HasDynamicInvoke => ContractProperties.HasFlag(ContractPropertyState.HasDynamicInvoke); - - private UInt160 _scriptHash; - public UInt160 ScriptHash - { - get - { - if (_scriptHash == null) - { - _scriptHash = Script.ToScriptHash(); - } - return _scriptHash; - } - } - - public override int Size => base.Size + Script.GetVarSize() + ParameterList.GetVarSize() + sizeof(ContractParameterType) + sizeof(bool) + Name.GetVarSize() + CodeVersion.GetVarSize() + Author.GetVarSize() + Email.GetVarSize() + Description.GetVarSize(); - - ContractState ICloneable.Clone() - { - return new ContractState - { - Script = Script, - ParameterList = ParameterList, - ReturnType = ReturnType, - ContractProperties = ContractProperties, - Name = Name, - CodeVersion = CodeVersion, - Author = Author, - Email = Email, - Description = Description - }; - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - Script = reader.ReadVarBytes(); - ParameterList = reader.ReadVarBytes().Select(p => (ContractParameterType)p).ToArray(); - ReturnType = (ContractParameterType)reader.ReadByte(); - ContractProperties = (ContractPropertyState)reader.ReadByte(); - Name = reader.ReadVarString(); - CodeVersion = reader.ReadVarString(); - Author = reader.ReadVarString(); - Email = reader.ReadVarString(); - Description = reader.ReadVarString(); - } - - void ICloneable.FromReplica(ContractState replica) - { - Script = replica.Script; - ParameterList = replica.ParameterList; - ReturnType = replica.ReturnType; - ContractProperties = replica.ContractProperties; - Name = replica.Name; - CodeVersion = replica.CodeVersion; - Author = replica.Author; - Email = replica.Email; - Description = replica.Description; - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.WriteVarBytes(Script); - writer.WriteVarBytes(ParameterList.Cast().ToArray()); - writer.Write((byte)ReturnType); - writer.Write((byte)ContractProperties); - writer.WriteVarString(Name); - writer.WriteVarString(CodeVersion); - writer.WriteVarString(Author); - writer.WriteVarString(Email); - writer.WriteVarString(Description); - } - - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["hash"] = ScriptHash.ToString(); - json["script"] = Script.ToHexString(); - json["parameters"] = new JArray(ParameterList.Select(p => (JObject)p)); - json["returntype"] = ReturnType; - json["name"] = Name; - json["code_version"] = CodeVersion; - json["author"] = Author; - json["email"] = Email; - json["description"] = Description; - json["properties"] = new JObject(); - json["properties"]["storage"] = HasStorage; - json["properties"]["dynamic_invoke"] = HasDynamicInvoke; - return json; - } - } -} diff --git a/neo/Core/ContractTransaction.cs b/neo/Core/ContractTransaction.cs deleted file mode 100644 index 252976fd69..0000000000 --- a/neo/Core/ContractTransaction.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.IO; - -namespace Neo.Core -{ - /// - /// 合约交易,这是最常用的一种交易 - /// - public class ContractTransaction : Transaction - { - public ContractTransaction() - : base(TransactionType.ContractTransaction) - { - } - - protected override void DeserializeExclusiveData(BinaryReader reader) - { - if (Version != 0) throw new FormatException(); - } - } -} diff --git a/neo/Core/EnrollmentTransaction.cs b/neo/Core/EnrollmentTransaction.cs deleted file mode 100644 index 89af83b426..0000000000 --- a/neo/Core/EnrollmentTransaction.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.IO.Json; -using Neo.SmartContract; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - [Obsolete] - public class EnrollmentTransaction : Transaction - { - /// - /// 记账人的公钥 - /// - public ECPoint PublicKey; - - private UInt160 _script_hash = null; - private UInt160 ScriptHash - { - get - { - if (_script_hash == null) - { - _script_hash = Contract.CreateSignatureRedeemScript(PublicKey).ToScriptHash(); - } - return _script_hash; - } - } - - public override int Size => base.Size + PublicKey.Size; - - public EnrollmentTransaction() - : base(TransactionType.EnrollmentTransaction) - { - } - - /// - /// 序列化交易中的额外数据 - /// - /// 数据来源 - protected override void DeserializeExclusiveData(BinaryReader reader) - { - if (Version != 0) throw new FormatException(); - PublicKey = ECPoint.DeserializeFrom(reader, ECCurve.Secp256r1); - } - - /// - /// 获取需要校验的脚本Hash - /// - /// 返回需要校验的脚本Hash - public override UInt160[] GetScriptHashesForVerifying() - { - return base.GetScriptHashesForVerifying().Union(new UInt160[] { ScriptHash }).OrderBy(p => p).ToArray(); - } - - /// - /// 序列化交易中的额外数据 - /// - /// 存放序列化后的结果 - protected override void SerializeExclusiveData(BinaryWriter writer) - { - writer.Write(PublicKey); - } - - /// - /// 变成json对象 - /// - /// 返回json对象 - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["pubkey"] = PublicKey.ToString(); - return json; - } - - public override bool Verify(IEnumerable mempool) - { - return false; - } - } -} diff --git a/neo/Core/Header.cs b/neo/Core/Header.cs deleted file mode 100644 index 096707fd82..0000000000 --- a/neo/Core/Header.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Neo.IO; -using Neo.VM; -using System; -using System.IO; - -namespace Neo.Core -{ - public class Header : BlockBase, IEquatable
- { - public override int Size => base.Size + 1; - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - if (reader.ReadByte() != 0) throw new FormatException(); - } - - public bool Equals(Header other) - { - if (ReferenceEquals(other, null)) return false; - if (ReferenceEquals(other, this)) return true; - return Hash.Equals(other.Hash); - } - - public override bool Equals(object obj) - { - return Equals(obj as Header); - } - - public static Header FromTrimmedData(byte[] data, int index) - { - Header header = new Header(); - using (MemoryStream ms = new MemoryStream(data, index, data.Length - index, false)) - using (BinaryReader reader = new BinaryReader(ms)) - { - ((IVerifiable)header).DeserializeUnsigned(reader); - reader.ReadByte(); header.Script = reader.ReadSerializable(); - } - return header; - } - - public override int GetHashCode() - { - return Hash.GetHashCode(); - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write((byte)0); - } - } -} diff --git a/neo/Core/Helper.cs b/neo/Core/Helper.cs deleted file mode 100644 index 1c35f838fc..0000000000 --- a/neo/Core/Helper.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Neo.Cryptography; -using Neo.SmartContract; -using Neo.VM; -using Neo.Wallets; -using System; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - /// - /// 包含一系列签名与验证的扩展方法 - /// - public static class Helper - { - public static byte[] GetHashData(this IVerifiable verifiable) - { - using (MemoryStream ms = new MemoryStream()) - using (BinaryWriter writer = new BinaryWriter(ms)) - { - verifiable.SerializeUnsigned(writer); - writer.Flush(); - return ms.ToArray(); - } - } - - /// - /// 根据传入的账户信息,对可签名的对象进行签名 - /// - /// 要签名的数据 - /// 用于签名的账户 - /// 返回签名后的结果 - public static byte[] Sign(this IVerifiable verifiable, KeyPair key) - { - using (key.Decrypt()) - { - return Crypto.Default.Sign(verifiable.GetHashData(), key.PrivateKey, key.PublicKey.EncodePoint(false).Skip(1).ToArray()); - } - } - - public static UInt160 ToScriptHash(this byte[] script) - { - return new UInt160(Crypto.Default.Hash160(script)); - } - - internal static bool VerifyScripts(this IVerifiable verifiable) - { - UInt160[] hashes; - try - { - hashes = verifiable.GetScriptHashesForVerifying(); - } - catch (InvalidOperationException) - { - return false; - } - if (hashes.Length != verifiable.Scripts.Length) return false; - for (int i = 0; i < hashes.Length; i++) - { - byte[] verification = verifiable.Scripts[i].VerificationScript; - if (verification.Length == 0) - { - using (ScriptBuilder sb = new ScriptBuilder()) - { - sb.EmitAppCall(hashes[i].ToArray()); - verification = sb.ToArray(); - } - } - else - { - if (hashes[i] != verifiable.Scripts[i].ScriptHash) return false; - } - using (StateReader service = new StateReader()) - { - ApplicationEngine engine = new ApplicationEngine(TriggerType.Verification, verifiable, Blockchain.Default, service, Fixed8.Zero); - engine.LoadScript(verification, false); - engine.LoadScript(verifiable.Scripts[i].InvocationScript, true); - if (!engine.Execute()) return false; - if (engine.EvaluationStack.Count != 1 || !engine.EvaluationStack.Pop().GetBoolean()) return false; - } - } - return true; - } - } -} diff --git a/neo/Core/IVerifiable.cs b/neo/Core/IVerifiable.cs deleted file mode 100644 index dcf9fc72b4..0000000000 --- a/neo/Core/IVerifiable.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Neo.IO; -using Neo.VM; -using System.IO; - -namespace Neo.Core -{ - /// - /// 为需要签名的数据提供一个接口 - /// - public interface IVerifiable : ISerializable, IScriptContainer - { - /// - /// 用于验证该对象的脚本列表 - /// - Witness[] Scripts { get; set; } - - /// - /// 反序列化未签名的数据 - /// - /// 数据来源 - void DeserializeUnsigned(BinaryReader reader); - - /// - /// 获得需要校验的脚本Hash值 - /// - /// 返回需要校验的脚本Hash值 - UInt160[] GetScriptHashesForVerifying(); - - /// - /// 序列化未签名的数据 - /// - /// 存放序列化后的结果 - void SerializeUnsigned(BinaryWriter writer); - } -} diff --git a/neo/Core/InvocationTransaction.cs b/neo/Core/InvocationTransaction.cs deleted file mode 100644 index be1d63c27f..0000000000 --- a/neo/Core/InvocationTransaction.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Neo.IO; -using Neo.IO.Json; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Neo.Core -{ - public class InvocationTransaction : Transaction - { - public byte[] Script; - public Fixed8 Gas; - - public override int Size => base.Size + Script.GetVarSize(); - - public override Fixed8 SystemFee => Gas; - - public InvocationTransaction() - : base(TransactionType.InvocationTransaction) - { - } - - protected override void DeserializeExclusiveData(BinaryReader reader) - { - if (Version > 1) throw new FormatException(); - Script = reader.ReadVarBytes(65536); - if (Script.Length == 0) throw new FormatException(); - if (Version >= 1) - { - Gas = reader.ReadSerializable(); - if (Gas < Fixed8.Zero) throw new FormatException(); - } - else - { - Gas = Fixed8.Zero; - } - } - - public static Fixed8 GetGas(Fixed8 consumed) - { - Fixed8 gas = consumed - Fixed8.FromDecimal(10); - if (gas <= Fixed8.Zero) return Fixed8.Zero; - return gas.Ceiling(); - } - - protected override void SerializeExclusiveData(BinaryWriter writer) - { - writer.WriteVarBytes(Script); - if (Version >= 1) - writer.Write(Gas); - } - - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["script"] = Script.ToHexString(); - json["gas"] = Gas.ToString(); - return json; - } - - public override bool Verify(IEnumerable mempool) - { - if (Gas.GetData() % 100000000 != 0) return false; - return base.Verify(mempool); - } - } -} diff --git a/neo/Core/IssueTransaction.cs b/neo/Core/IssueTransaction.cs deleted file mode 100644 index 60e53c8f2e..0000000000 --- a/neo/Core/IssueTransaction.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - /// - /// 用于分发资产的特殊交易 - /// - public class IssueTransaction : Transaction - { - /// - /// 系统费用 - /// - public override Fixed8 SystemFee - { - get - { - if (Version >= 1) return Fixed8.Zero; - if (Outputs.All(p => p.AssetId == Blockchain.GoverningToken.Hash || p.AssetId == Blockchain.UtilityToken.Hash)) - return Fixed8.Zero; - return base.SystemFee; - } - } - - public IssueTransaction() - : base(TransactionType.IssueTransaction) - { - } - - protected override void DeserializeExclusiveData(BinaryReader reader) - { - if (Version > 1) throw new FormatException(); - } - - /// - /// 获取需要校验的脚本散列值 - /// - /// 返回需要校验的脚本散列值 - public override UInt160[] GetScriptHashesForVerifying() - { - HashSet hashes = new HashSet(base.GetScriptHashesForVerifying()); - foreach (TransactionResult result in GetTransactionResults().Where(p => p.Amount < Fixed8.Zero)) - { - AssetState asset = Blockchain.Default.GetAssetState(result.AssetId); - if (asset == null) throw new InvalidOperationException(); - hashes.Add(asset.Issuer); - } - return hashes.OrderBy(p => p).ToArray(); - } - - /// - /// 验证交易 - /// - /// 返回验证后的结果 - public override bool Verify(IEnumerable mempool) - { - if (!base.Verify(mempool)) return false; - TransactionResult[] results = GetTransactionResults()?.Where(p => p.Amount < Fixed8.Zero).ToArray(); - if (results == null) return false; - foreach (TransactionResult r in results) - { - AssetState asset = Blockchain.Default.GetAssetState(r.AssetId); - if (asset == null) return false; - if (asset.Amount < Fixed8.Zero) continue; - Fixed8 quantity_issued = asset.Available + mempool.OfType().Where(p => p != this).SelectMany(p => p.Outputs).Where(p => p.AssetId == r.AssetId).Sum(p => p.Value); - if (asset.Amount - quantity_issued < -r.Amount) return false; - } - return true; - } - } -} diff --git a/neo/Core/MinerTransaction.cs b/neo/Core/MinerTransaction.cs deleted file mode 100644 index 78cdd8b4a4..0000000000 --- a/neo/Core/MinerTransaction.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Neo.IO.Json; -using System; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - /// - /// 用于分配字节费的特殊交易 - /// - public class MinerTransaction : Transaction - { - /// - /// 随机数 - /// - public uint Nonce; - - public override Fixed8 NetworkFee => Fixed8.Zero; - - public override int Size => base.Size + sizeof(uint); - - public MinerTransaction() - : base(TransactionType.MinerTransaction) - { - } - - /// - /// 反序列化交易中的额外数据 - /// - /// 数据来源 - protected override void DeserializeExclusiveData(BinaryReader reader) - { - if (Version != 0) throw new FormatException(); - this.Nonce = reader.ReadUInt32(); - } - - /// - /// 反序列化进行完毕时触发 - /// - protected override void OnDeserialized() - { - base.OnDeserialized(); - if (Inputs.Length != 0) - throw new FormatException(); - if (Outputs.Any(p => p.AssetId != Blockchain.UtilityToken.Hash)) - throw new FormatException(); - } - - /// - /// 序列化交易中的额外数据 - /// - /// 存放序列化后的结果 - protected override void SerializeExclusiveData(BinaryWriter writer) - { - writer.Write(Nonce); - } - - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["nonce"] = Nonce; - return json; - } - } -} diff --git a/neo/Core/PublishTransaction.cs b/neo/Core/PublishTransaction.cs deleted file mode 100644 index 3826cd3088..0000000000 --- a/neo/Core/PublishTransaction.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Neo.IO; -using Neo.IO.Json; -using Neo.SmartContract; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - [Obsolete] - public class PublishTransaction : Transaction - { - public byte[] Script; - public ContractParameterType[] ParameterList; - public ContractParameterType ReturnType; - public bool NeedStorage; - public string Name; - public string CodeVersion; - public string Author; - public string Email; - public string Description; - - private UInt160 _scriptHash; - internal UInt160 ScriptHash - { - get - { - if (_scriptHash == null) - { - _scriptHash = Script.ToScriptHash(); - } - return _scriptHash; - } - } - - public override int Size => base.Size + Script.GetVarSize() + ParameterList.GetVarSize() + sizeof(ContractParameterType) + Name.GetVarSize() + CodeVersion.GetVarSize() + Author.GetVarSize() + Email.GetVarSize() + Description.GetVarSize(); - - public PublishTransaction() - : base(TransactionType.PublishTransaction) - { - } - - protected override void DeserializeExclusiveData(BinaryReader reader) - { - if (Version > 1) throw new FormatException(); - Script = reader.ReadVarBytes(); - ParameterList = reader.ReadVarBytes().Select(p => (ContractParameterType)p).ToArray(); - ReturnType = (ContractParameterType)reader.ReadByte(); - if (Version >= 1) - NeedStorage = reader.ReadBoolean(); - else - NeedStorage = false; - Name = reader.ReadVarString(252); - CodeVersion = reader.ReadVarString(252); - Author = reader.ReadVarString(252); - Email = reader.ReadVarString(252); - Description = reader.ReadVarString(65536); - } - - protected override void SerializeExclusiveData(BinaryWriter writer) - { - writer.WriteVarBytes(Script); - writer.WriteVarBytes(ParameterList.Cast().ToArray()); - writer.Write((byte)ReturnType); - if (Version >= 1) writer.Write(NeedStorage); - writer.WriteVarString(Name); - writer.WriteVarString(CodeVersion); - writer.WriteVarString(Author); - writer.WriteVarString(Email); - writer.WriteVarString(Description); - } - - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["contract"] = new JObject(); - json["contract"]["code"] = new JObject(); - json["contract"]["code"]["hash"] = ScriptHash.ToString(); - json["contract"]["code"]["script"] = Script.ToHexString(); - json["contract"]["code"]["parameters"] = new JArray(ParameterList.Select(p => (JObject)p)); - json["contract"]["code"]["returntype"] = ReturnType; - json["contract"]["needstorage"] = NeedStorage; - json["contract"]["name"] = Name; - json["contract"]["version"] = CodeVersion; - json["contract"]["author"] = Author; - json["contract"]["email"] = Email; - json["contract"]["description"] = Description; - return json; - } - - public override bool Verify(IEnumerable mempool) - { - return false; - } - } -} diff --git a/neo/Core/RegisterTransaction.cs b/neo/Core/RegisterTransaction.cs deleted file mode 100644 index 667df17b1f..0000000000 --- a/neo/Core/RegisterTransaction.cs +++ /dev/null @@ -1,139 +0,0 @@ -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.IO.Json; -using Neo.SmartContract; -using Neo.Wallets; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - [Obsolete] - public class RegisterTransaction : Transaction - { - /// - /// 资产类别 - /// - public AssetType AssetType; - /// - /// 资产名称 - /// - public string Name; - /// - /// 发行总量,共有2种模式: - /// 1. 限量模式:当Amount为正数时,表示当前资产的最大总量为Amount,且不可修改(股权在未来可能会支持扩股或增发,会考虑需要公司签名或一定比例的股东签名认可)。 - /// 2. 不限量模式:当Amount等于-1时,表示当前资产可以由创建者无限量发行。这种模式的自由度最大,但是公信力最低,不建议使用。 - /// - public Fixed8 Amount; - public byte Precision; - /// - /// 发行者的公钥 - /// - public ECPoint Owner; - /// - /// 资产管理员的合约散列值 - /// - public UInt160 Admin; - - public override int Size => base.Size + sizeof(AssetType) + Name.GetVarSize() + Amount.Size + sizeof(byte) + Owner.Size + Admin.Size; - - /// - /// 系统费用 - /// - public override Fixed8 SystemFee - { - get - { - if (AssetType == AssetType.GoverningToken || AssetType == AssetType.UtilityToken) - return Fixed8.Zero; - return base.SystemFee; - } - } - - public RegisterTransaction() - : base(TransactionType.RegisterTransaction) - { - } - - /// - /// 反序列化交易中额外的数据 - /// - /// 数据来源 - protected override void DeserializeExclusiveData(BinaryReader reader) - { - if (Version != 0) throw new FormatException(); - AssetType = (AssetType)reader.ReadByte(); - Name = reader.ReadVarString(1024); - Amount = reader.ReadSerializable(); - Precision = reader.ReadByte(); - Owner = ECPoint.DeserializeFrom(reader, ECCurve.Secp256r1); - if (Owner.IsInfinity && AssetType != AssetType.GoverningToken && AssetType != AssetType.UtilityToken) - throw new FormatException(); - Admin = reader.ReadSerializable(); - } - - /// - /// 获取需要校验的脚本Hash值 - /// - /// 返回需要校验的脚本Hash值 - public override UInt160[] GetScriptHashesForVerifying() - { - UInt160 owner = Contract.CreateSignatureRedeemScript(Owner).ToScriptHash(); - return base.GetScriptHashesForVerifying().Union(new[] { owner }).OrderBy(p => p).ToArray(); - } - - protected override void OnDeserialized() - { - base.OnDeserialized(); - if (AssetType == AssetType.GoverningToken && !Hash.Equals(Blockchain.GoverningToken.Hash)) - throw new FormatException(); - if (AssetType == AssetType.UtilityToken && !Hash.Equals(Blockchain.UtilityToken.Hash)) - throw new FormatException(); - } - - /// - /// 序列化交易中额外的数据 - /// - /// 存放序列化后的结果 - protected override void SerializeExclusiveData(BinaryWriter writer) - { - writer.Write((byte)AssetType); - writer.WriteVarString(Name); - writer.Write(Amount); - writer.Write(Precision); - writer.Write(Owner); - writer.Write(Admin); - } - - /// - /// 变成json对象 - /// - /// 返回json对象 - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["asset"] = new JObject(); - json["asset"]["type"] = AssetType; - try - { - json["asset"]["name"] = Name == "" ? null : JObject.Parse(Name); - } - catch (FormatException) - { - json["asset"]["name"] = Name; - } - json["asset"]["amount"] = Amount.ToString(); - json["asset"]["precision"] = Precision; - json["asset"]["owner"] = Owner.ToString(); - json["asset"]["admin"] = Wallet.ToAddress(Admin); - return json; - } - - public override bool Verify(IEnumerable mempool) - { - return false; - } - } -} diff --git a/neo/Core/SpentCoin.cs b/neo/Core/SpentCoin.cs deleted file mode 100644 index ba6a900aa7..0000000000 --- a/neo/Core/SpentCoin.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Neo.Core -{ - public class SpentCoin - { - public TransactionOutput Output; - public uint StartHeight; - public uint EndHeight; - - public Fixed8 Value => Output.Value; - } -} diff --git a/neo/Core/SpentCoinState.cs b/neo/Core/SpentCoinState.cs deleted file mode 100644 index 9f1e8b4049..0000000000 --- a/neo/Core/SpentCoinState.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Neo.IO; -using System.Collections.Generic; -using System.IO; - -namespace Neo.Core -{ - public class SpentCoinState : StateBase, ICloneable - { - public UInt256 TransactionHash; - public uint TransactionHeight; - public Dictionary Items; - - public override int Size => base.Size + TransactionHash.Size + sizeof(uint) - + IO.Helper.GetVarSize(Items.Count) + Items.Count * (sizeof(ushort) + sizeof(uint)); - - SpentCoinState ICloneable.Clone() - { - return new SpentCoinState - { - TransactionHash = TransactionHash, - TransactionHeight = TransactionHeight, - Items = new Dictionary(Items) - }; - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - TransactionHash = reader.ReadSerializable(); - TransactionHeight = reader.ReadUInt32(); - int count = (int)reader.ReadVarInt(); - Items = new Dictionary(count); - for (int i = 0; i < count; i++) - { - ushort index = reader.ReadUInt16(); - uint height = reader.ReadUInt32(); - Items.Add(index, height); - } - } - - void ICloneable.FromReplica(SpentCoinState replica) - { - TransactionHash = replica.TransactionHash; - TransactionHeight = replica.TransactionHeight; - Items = replica.Items; - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write(TransactionHash); - writer.Write(TransactionHeight); - writer.WriteVarInt(Items.Count); - foreach (var pair in Items) - { - writer.Write(pair.Key); - writer.Write(pair.Value); - } - } - } -} diff --git a/neo/Core/StateBase.cs b/neo/Core/StateBase.cs deleted file mode 100644 index ca1bf5b3f0..0000000000 --- a/neo/Core/StateBase.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Neo.IO; -using Neo.IO.Json; -using Neo.VM; -using System; -using System.IO; - -namespace Neo.Core -{ - public abstract class StateBase : IInteropInterface, ISerializable - { - public const byte StateVersion = 0; - - public virtual int Size => sizeof(byte); - - public virtual void Deserialize(BinaryReader reader) - { - if (reader.ReadByte() != StateVersion) throw new FormatException(); - } - - public virtual void Serialize(BinaryWriter writer) - { - writer.Write(StateVersion); - } - - public virtual JObject ToJson() - { - JObject json = new JObject(); - json["version"] = StateVersion; - return json; - } - } -} diff --git a/neo/Core/StateDescriptor.cs b/neo/Core/StateDescriptor.cs deleted file mode 100644 index 70623423ef..0000000000 --- a/neo/Core/StateDescriptor.cs +++ /dev/null @@ -1,155 +0,0 @@ -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.IO.Caching; -using Neo.IO.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - public class StateDescriptor : ISerializable - { - public StateType Type; - public byte[] Key; - public string Field; - public byte[] Value; - - public int Size => sizeof(StateType) + Key.GetVarSize() + Field.GetVarSize() + Value.GetVarSize(); - - public Fixed8 SystemFee - { - get - { - switch (Type) - { - case StateType.Validator: - return GetSystemFee_Validator(); - default: - return Fixed8.Zero; - } - } - } - - private void CheckAccountState() - { - if (Key.Length != 20) throw new FormatException(); - if (Field != "Votes") throw new FormatException(); - } - - private void CheckValidatorState() - { - if (Key.Length != 33) throw new FormatException(); - if (Field != "Registered") throw new FormatException(); - } - - void ISerializable.Deserialize(BinaryReader reader) - { - Type = (StateType)reader.ReadByte(); - if (!Enum.IsDefined(typeof(StateType), Type)) - throw new FormatException(); - Key = reader.ReadVarBytes(100); - Field = reader.ReadVarString(32); - Value = reader.ReadVarBytes(65535); - switch (Type) - { - case StateType.Account: - CheckAccountState(); - break; - case StateType.Validator: - CheckValidatorState(); - break; - } - } - - private Fixed8 GetSystemFee_Validator() - { - switch (Field) - { - case "Registered": - if (Value.Any(p => p != 0)) - return Fixed8.FromDecimal(1000); - else - return Fixed8.Zero; - default: - throw new InvalidOperationException(); - } - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write((byte)Type); - writer.WriteVarBytes(Key); - writer.WriteVarString(Field); - writer.WriteVarBytes(Value); - } - - public JObject ToJson() - { - JObject json = new JObject(); - json["type"] = Type; - json["key"] = Key.ToHexString(); - json["field"] = Field; - json["value"] = Value.ToHexString(); - return json; - } - - internal bool Verify() - { - switch (Type) - { - case StateType.Account: - return VerifyAccountState(); - case StateType.Validator: - return VerifyValidatorState(); - default: - return false; - } - } - - private bool VerifyAccountState() - { - switch (Field) - { - case "Votes": - if (Blockchain.Default == null) return false; - ECPoint[] pubkeys; - try - { - pubkeys = Value.AsSerializableArray((int)Blockchain.MaxValidators); - } - catch (FormatException) - { - return false; - } - UInt160 hash = new UInt160(Key); - AccountState account = Blockchain.Default.GetAccountState(hash); - if (account?.IsFrozen != false) return false; - if (pubkeys.Length > 0) - { - if (account.GetBalance(Blockchain.GoverningToken.Hash).Equals(Fixed8.Zero)) return false; - HashSet sv = new HashSet(Blockchain.StandbyValidators); - DataCache validators = Blockchain.Default.GetStates(); - foreach (ECPoint pubkey in pubkeys) - if (!sv.Contains(pubkey) && validators.TryGet(pubkey)?.Registered != true) - return false; - } - return true; - default: - return false; - } - } - - private bool VerifyValidatorState() - { - switch (Field) - { - case "Registered": - return true; - default: - return false; - } - } - } -} diff --git a/neo/Core/StateTransaction.cs b/neo/Core/StateTransaction.cs deleted file mode 100644 index 1fc996d351..0000000000 --- a/neo/Core/StateTransaction.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.IO.Json; -using Neo.SmartContract; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - public class StateTransaction : Transaction - { - public StateDescriptor[] Descriptors; - - public override int Size => base.Size + Descriptors.GetVarSize(); - public override Fixed8 SystemFee => Descriptors.Sum(p => p.SystemFee); - - public StateTransaction() - : base(TransactionType.StateTransaction) - { - } - - protected override void DeserializeExclusiveData(BinaryReader reader) - { - Descriptors = reader.ReadSerializableArray(16); - } - - public override UInt160[] GetScriptHashesForVerifying() - { - HashSet hashes = new HashSet(base.GetScriptHashesForVerifying()); - foreach (StateDescriptor descriptor in Descriptors) - { - switch (descriptor.Type) - { - case StateType.Account: - hashes.UnionWith(GetScriptHashesForVerifying_Account(descriptor)); - break; - case StateType.Validator: - hashes.UnionWith(GetScriptHashesForVerifying_Validator(descriptor)); - break; - default: - throw new InvalidOperationException(); - } - } - return hashes.OrderBy(p => p).ToArray(); - } - - private IEnumerable GetScriptHashesForVerifying_Account(StateDescriptor descriptor) - { - switch (descriptor.Field) - { - case "Votes": - yield return new UInt160(descriptor.Key); - break; - default: - throw new InvalidOperationException(); - } - } - - private IEnumerable GetScriptHashesForVerifying_Validator(StateDescriptor descriptor) - { - switch (descriptor.Field) - { - case "Registered": - yield return Contract.CreateSignatureRedeemScript(ECPoint.DecodePoint(descriptor.Key, ECCurve.Secp256r1)).ToScriptHash(); - break; - default: - throw new InvalidOperationException(); - } - } - - protected override void SerializeExclusiveData(BinaryWriter writer) - { - writer.Write(Descriptors); - } - - public override JObject ToJson() - { - JObject json = base.ToJson(); - json["descriptors"] = new JArray(Descriptors.Select(p => p.ToJson())); - return json; - } - - public override bool Verify(IEnumerable mempool) - { - foreach (StateDescriptor descriptor in Descriptors) - if (!descriptor.Verify()) - return false; - return base.Verify(mempool); - } - } -} diff --git a/neo/Core/StateType.cs b/neo/Core/StateType.cs deleted file mode 100644 index e49cad623a..0000000000 --- a/neo/Core/StateType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Neo.Core -{ - public enum StateType : byte - { - Account = 0x40, - Validator = 0x48 - } -} diff --git a/neo/Core/StorageItem.cs b/neo/Core/StorageItem.cs deleted file mode 100644 index 257dbf936b..0000000000 --- a/neo/Core/StorageItem.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Neo.IO; -using System.IO; - -namespace Neo.Core -{ - public class StorageItem : StateBase, ICloneable - { - public byte[] Value; - - public override int Size => base.Size + Value.GetVarSize(); - - StorageItem ICloneable.Clone() - { - return new StorageItem - { - Value = Value - }; - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - Value = reader.ReadVarBytes(); - } - - void ICloneable.FromReplica(StorageItem replica) - { - Value = replica.Value; - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.WriteVarBytes(Value); - } - } -} diff --git a/neo/Core/StorageKey.cs b/neo/Core/StorageKey.cs deleted file mode 100644 index 3babef23c3..0000000000 --- a/neo/Core/StorageKey.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Neo.Cryptography; -using Neo.IO; -using System; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - public class StorageKey : IEquatable, ISerializable - { - public UInt160 ScriptHash; - public byte[] Key; - - int ISerializable.Size => ScriptHash.Size + Key.GetVarSize(); - - void ISerializable.Deserialize(BinaryReader reader) - { - ScriptHash = reader.ReadSerializable(); - Key = reader.ReadVarBytes(); - } - - public bool Equals(StorageKey other) - { - if (ReferenceEquals(other, null)) - return false; - if (ReferenceEquals(this, other)) - return true; - return ScriptHash.Equals(other.ScriptHash) && Key.SequenceEqual(other.Key); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(obj, null)) return false; - if (!(obj is StorageKey)) return false; - return Equals((StorageKey)obj); - } - - public override int GetHashCode() - { - return ScriptHash.GetHashCode() + (int)Key.Murmur32(0); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(ScriptHash); - writer.WriteVarBytes(Key); - } - } -} diff --git a/neo/Core/Transaction.cs b/neo/Core/Transaction.cs deleted file mode 100644 index 5fce0a3144..0000000000 --- a/neo/Core/Transaction.cs +++ /dev/null @@ -1,380 +0,0 @@ -using Neo.Cryptography; -using Neo.IO; -using Neo.IO.Caching; -using Neo.IO.Json; -using Neo.Network; -using Neo.VM; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace Neo.Core -{ - /// - /// 一切交易的基类 - /// - public abstract class Transaction : IEquatable, IInventory - { - /// - /// Maximum number of attributes that can be contained within a transaction - /// - private const int MaxTransactionAttributes = 16; - - /// - /// Reflection cache for TransactionType - /// - private static ReflectionCache ReflectionCache = ReflectionCache.CreateFromEnum(); - - /// - /// 交易类型 - /// - public readonly TransactionType Type; - /// - /// 版本 - /// - public byte Version; - /// - /// 该交易所具备的额外特性 - /// - public TransactionAttribute[] Attributes; - /// - /// 输入列表 - /// - public CoinReference[] Inputs; - /// - /// 输出列表 - /// - public TransactionOutput[] Outputs; - /// - /// 用于验证该交易的脚本列表 - /// - public Witness[] Scripts { get; set; } - - private UInt256 _hash = null; - public UInt256 Hash - { - get - { - if (_hash == null) - { - _hash = new UInt256(Crypto.Default.Hash256(this.GetHashData())); - } - return _hash; - } - } - - /// - /// 清单类型 - /// - InventoryType IInventory.InventoryType => InventoryType.TX; - - private Fixed8 _network_fee = -Fixed8.Satoshi; - public virtual Fixed8 NetworkFee - { - get - { - if (_network_fee == -Fixed8.Satoshi) - { - Fixed8 input = References.Values.Where(p => p.AssetId.Equals(Blockchain.UtilityToken.Hash)).Sum(p => p.Value); - Fixed8 output = Outputs.Where(p => p.AssetId.Equals(Blockchain.UtilityToken.Hash)).Sum(p => p.Value); - _network_fee = input - output - SystemFee; - } - return _network_fee; - } - } - - private IReadOnlyDictionary _references; - /// - /// 每一个交易输入所引用的交易输出 - /// - public IReadOnlyDictionary References - { - get - { - if (_references == null) - { - Dictionary dictionary = new Dictionary(); - foreach (var group in Inputs.GroupBy(p => p.PrevHash)) - { - Transaction tx = Blockchain.Default.GetTransaction(group.Key); - if (tx == null) return null; - foreach (var reference in group.Select(p => new - { - Input = p, - Output = tx.Outputs[p.PrevIndex] - })) - { - dictionary.Add(reference.Input, reference.Output); - } - } - _references = dictionary; - } - return _references; - } - } - - public virtual int Size => sizeof(TransactionType) + sizeof(byte) + Attributes.GetVarSize() + Inputs.GetVarSize() + Outputs.GetVarSize() + Scripts.GetVarSize(); - - /// - /// 系统费用 - /// - public virtual Fixed8 SystemFee => Settings.Default.SystemFee.TryGetValue(Type, out Fixed8 fee) ? fee : Fixed8.Zero; - - /// - /// 用指定的类型初始化Transaction对象 - /// - /// 交易类型 - protected Transaction(TransactionType type) - { - this.Type = type; - } - - /// - /// 反序列化 - /// - /// 数据来源 - void ISerializable.Deserialize(BinaryReader reader) - { - ((IVerifiable)this).DeserializeUnsigned(reader); - Scripts = reader.ReadSerializableArray(); - OnDeserialized(); - } - - /// - /// 反序列化交易中的额外数据 - /// - /// 数据来源 - protected virtual void DeserializeExclusiveData(BinaryReader reader) - { - } - - /// - /// 从指定的字节数组反序列化一笔交易 - /// - /// 字节数组 - /// 偏移量,反序列化从该偏移量处开始 - /// 返回反序列化后的结果 - public static Transaction DeserializeFrom(byte[] value, int offset = 0) - { - using (MemoryStream ms = new MemoryStream(value, offset, value.Length - offset, false)) - using (BinaryReader reader = new BinaryReader(ms, Encoding.UTF8)) - { - return DeserializeFrom(reader); - } - } - - /// - /// 反序列化 - /// - /// 数据来源 - /// 返回反序列化后的结果 - internal static Transaction DeserializeFrom(BinaryReader reader) - { - // Looking for type in reflection cache - Transaction transaction = ReflectionCache.CreateInstance(reader.ReadByte()); - if (transaction == null) throw new FormatException(); - - transaction.DeserializeUnsignedWithoutType(reader); - transaction.Scripts = reader.ReadSerializableArray(); - transaction.OnDeserialized(); - return transaction; - } - - void IVerifiable.DeserializeUnsigned(BinaryReader reader) - { - if ((TransactionType)reader.ReadByte() != Type) - throw new FormatException(); - DeserializeUnsignedWithoutType(reader); - } - - private void DeserializeUnsignedWithoutType(BinaryReader reader) - { - Version = reader.ReadByte(); - DeserializeExclusiveData(reader); - Attributes = reader.ReadSerializableArray(MaxTransactionAttributes); - Inputs = reader.ReadSerializableArray(); - Outputs = reader.ReadSerializableArray(ushort.MaxValue + 1); - } - - public bool Equals(Transaction other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Hash.Equals(other.Hash); - } - - public override bool Equals(object obj) - { - return Equals(obj as Transaction); - } - - public override int GetHashCode() - { - return Hash.GetHashCode(); - } - - byte[] IScriptContainer.GetMessage() - { - return this.GetHashData(); - } - - /// - /// 获取需要校验的脚本散列值 - /// - /// 返回需要校验的脚本散列值 - public virtual UInt160[] GetScriptHashesForVerifying() - { - if (References == null) throw new InvalidOperationException(); - HashSet hashes = new HashSet(Inputs.Select(p => References[p].ScriptHash)); - hashes.UnionWith(Attributes.Where(p => p.Usage == TransactionAttributeUsage.Script).Select(p => new UInt160(p.Data))); - foreach (var group in Outputs.GroupBy(p => p.AssetId)) - { - AssetState asset = Blockchain.Default.GetAssetState(group.Key); - if (asset == null) throw new InvalidOperationException(); - if (asset.AssetType.HasFlag(AssetType.DutyFlag)) - { - hashes.UnionWith(group.Select(p => p.ScriptHash)); - } - } - return hashes.OrderBy(p => p).ToArray(); - } - - /// - /// 获取交易后各资产的变化量 - /// - /// 返回交易后各资产的变化量 - public IEnumerable GetTransactionResults() - { - if (References == null) return null; - return References.Values.Select(p => new - { - AssetId = p.AssetId, - Value = p.Value - }).Concat(Outputs.Select(p => new - { - AssetId = p.AssetId, - Value = -p.Value - })).GroupBy(p => p.AssetId, (k, g) => new TransactionResult - { - AssetId = k, - Amount = g.Sum(p => p.Value) - }).Where(p => p.Amount != Fixed8.Zero); - } - - /// - /// 通知子类反序列化完毕 - /// - protected virtual void OnDeserialized() - { - } - - /// - /// 序列化 - /// - /// 存放序列化后的结果 - void ISerializable.Serialize(BinaryWriter writer) - { - ((IVerifiable)this).SerializeUnsigned(writer); - writer.Write(Scripts); - } - - /// - /// 序列化交易中的额外数据 - /// - /// 存放序列化后的结果 - protected virtual void SerializeExclusiveData(BinaryWriter writer) - { - } - - void IVerifiable.SerializeUnsigned(BinaryWriter writer) - { - writer.Write((byte)Type); - writer.Write(Version); - SerializeExclusiveData(writer); - writer.Write(Attributes); - writer.Write(Inputs); - writer.Write(Outputs); - } - - /// - /// 变成json对象 - /// - /// 返回json对象 - public virtual JObject ToJson() - { - JObject json = new JObject(); - json["txid"] = Hash.ToString(); - json["size"] = Size; - json["type"] = Type; - json["version"] = Version; - json["attributes"] = Attributes.Select(p => p.ToJson()).ToArray(); - json["vin"] = Inputs.Select(p => p.ToJson()).ToArray(); - json["vout"] = Outputs.Select((p, i) => p.ToJson((ushort)i)).ToArray(); - json["sys_fee"] = SystemFee.ToString(); - json["net_fee"] = NetworkFee.ToString(); - json["scripts"] = Scripts.Select(p => p.ToJson()).ToArray(); - return json; - } - - bool IInventory.Verify() - { - return Verify(Enumerable.Empty()); - } - - /// - /// 验证交易 - /// - /// 返回验证的结果 - public virtual bool Verify(IEnumerable mempool) - { - for (int i = 1; i < Inputs.Length; i++) - for (int j = 0; j < i; j++) - if (Inputs[i].PrevHash == Inputs[j].PrevHash && Inputs[i].PrevIndex == Inputs[j].PrevIndex) - return false; - if (mempool.Where(p => p != this).SelectMany(p => p.Inputs).Intersect(Inputs).Count() > 0) - return false; - if (Blockchain.Default.IsDoubleSpend(this)) - return false; - foreach (var group in Outputs.GroupBy(p => p.AssetId)) - { - AssetState asset = Blockchain.Default.GetAssetState(group.Key); - if (asset == null) return false; - if (asset.Expiration <= Blockchain.Default.Height + 1 && asset.AssetType != AssetType.GoverningToken && asset.AssetType != AssetType.UtilityToken) - return false; - foreach (TransactionOutput output in group) - if (output.Value.GetData() % (long)Math.Pow(10, 8 - asset.Precision) != 0) - return false; - } - TransactionResult[] results = GetTransactionResults()?.ToArray(); - if (results == null) return false; - TransactionResult[] results_destroy = results.Where(p => p.Amount > Fixed8.Zero).ToArray(); - if (results_destroy.Length > 1) return false; - if (results_destroy.Length == 1 && results_destroy[0].AssetId != Blockchain.UtilityToken.Hash) - return false; - if (SystemFee > Fixed8.Zero && (results_destroy.Length == 0 || results_destroy[0].Amount < SystemFee)) - return false; - TransactionResult[] results_issue = results.Where(p => p.Amount < Fixed8.Zero).ToArray(); - switch (Type) - { - case TransactionType.MinerTransaction: - case TransactionType.ClaimTransaction: - if (results_issue.Any(p => p.AssetId != Blockchain.UtilityToken.Hash)) - return false; - break; - case TransactionType.IssueTransaction: - if (results_issue.Any(p => p.AssetId == Blockchain.UtilityToken.Hash)) - return false; - break; - default: - if (results_issue.Length > 0) - return false; - break; - } - if (Attributes.Count(p => p.Usage == TransactionAttributeUsage.ECDH02 || p.Usage == TransactionAttributeUsage.ECDH03) > 1) - return false; - return this.VerifyScripts(); - } - } -} diff --git a/neo/Core/TransactionAttribute.cs b/neo/Core/TransactionAttribute.cs deleted file mode 100644 index 2cc04194d4..0000000000 --- a/neo/Core/TransactionAttribute.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Neo.IO; -using Neo.IO.Json; -using Neo.VM; -using System; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - /// - /// 交易特性 - /// - public class TransactionAttribute : IInteropInterface, ISerializable - { - /// - /// 用途 - /// - public TransactionAttributeUsage Usage; - /// - /// 特定于用途的外部数据 - /// - public byte[] Data; - - public int Size - { - get - { - if (Usage == TransactionAttributeUsage.ContractHash || Usage == TransactionAttributeUsage.ECDH02 || Usage == TransactionAttributeUsage.ECDH03 || Usage == TransactionAttributeUsage.Vote || (Usage >= TransactionAttributeUsage.Hash1 && Usage <= TransactionAttributeUsage.Hash15)) - return sizeof(TransactionAttributeUsage) + 32; - else if (Usage == TransactionAttributeUsage.Script) - return sizeof(TransactionAttributeUsage) + 20; - else if (Usage == TransactionAttributeUsage.DescriptionUrl) - return sizeof(TransactionAttributeUsage) + sizeof(byte) + Data.Length; - else - return sizeof(TransactionAttributeUsage) + Data.GetVarSize(); - } - } - - void ISerializable.Deserialize(BinaryReader reader) - { - Usage = (TransactionAttributeUsage)reader.ReadByte(); - if (Usage == TransactionAttributeUsage.ContractHash || Usage == TransactionAttributeUsage.Vote || (Usage >= TransactionAttributeUsage.Hash1 && Usage <= TransactionAttributeUsage.Hash15)) - Data = reader.ReadBytes(32); - else if (Usage == TransactionAttributeUsage.ECDH02 || Usage == TransactionAttributeUsage.ECDH03) - Data = new[] { (byte)Usage }.Concat(reader.ReadBytes(32)).ToArray(); - else if (Usage == TransactionAttributeUsage.Script) - Data = reader.ReadBytes(20); - else if (Usage == TransactionAttributeUsage.DescriptionUrl) - Data = reader.ReadBytes(reader.ReadByte()); - else if (Usage == TransactionAttributeUsage.Description || Usage >= TransactionAttributeUsage.Remark) - Data = reader.ReadVarBytes(ushort.MaxValue); - else - throw new FormatException(); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write((byte)Usage); - if (Usage == TransactionAttributeUsage.DescriptionUrl) - writer.Write((byte)Data.Length); - else if (Usage == TransactionAttributeUsage.Description || Usage >= TransactionAttributeUsage.Remark) - writer.WriteVarInt(Data.Length); - if (Usage == TransactionAttributeUsage.ECDH02 || Usage == TransactionAttributeUsage.ECDH03) - writer.Write(Data, 1, 32); - else - writer.Write(Data); - } - - /// - /// 变成json对象 - /// - /// 返回json对象 - public JObject ToJson() - { - JObject json = new JObject(); - json["usage"] = Usage; - json["data"] = Data.ToHexString(); - return json; - } - } -} diff --git a/neo/Core/TransactionAttributeUsage.cs b/neo/Core/TransactionAttributeUsage.cs deleted file mode 100644 index 74079f295d..0000000000 --- a/neo/Core/TransactionAttributeUsage.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace Neo.Core -{ - /// - /// 表示交易特性的用途 - /// - public enum TransactionAttributeUsage : byte - { - /// - /// 外部合同的散列值 - /// - ContractHash = 0x00, - - /// - /// 用于ECDH密钥交换的公钥,该公钥的第一个字节为0x02 - /// - ECDH02 = 0x02, - /// - /// 用于ECDH密钥交换的公钥,该公钥的第一个字节为0x03 - /// - ECDH03 = 0x03, - - /// - /// 用于对交易进行额外的验证 - /// - Script = 0x20, - - Vote = 0x30, - - DescriptionUrl = 0x81, - Description = 0x90, - - Hash1 = 0xa1, - Hash2 = 0xa2, - Hash3 = 0xa3, - Hash4 = 0xa4, - Hash5 = 0xa5, - Hash6 = 0xa6, - Hash7 = 0xa7, - Hash8 = 0xa8, - Hash9 = 0xa9, - Hash10 = 0xaa, - Hash11 = 0xab, - Hash12 = 0xac, - Hash13 = 0xad, - Hash14 = 0xae, - Hash15 = 0xaf, - - /// - /// 备注 - /// - Remark = 0xf0, - Remark1 = 0xf1, - Remark2 = 0xf2, - Remark3 = 0xf3, - Remark4 = 0xf4, - Remark5 = 0xf5, - Remark6 = 0xf6, - Remark7 = 0xf7, - Remark8 = 0xf8, - Remark9 = 0xf9, - Remark10 = 0xfa, - Remark11 = 0xfb, - Remark12 = 0xfc, - Remark13 = 0xfd, - Remark14 = 0xfe, - Remark15 = 0xff - } -} diff --git a/neo/Core/TransactionOutput.cs b/neo/Core/TransactionOutput.cs deleted file mode 100644 index ad69315fd9..0000000000 --- a/neo/Core/TransactionOutput.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Neo.IO; -using Neo.IO.Json; -using Neo.VM; -using Neo.Wallets; -using System; -using System.IO; - -namespace Neo.Core -{ - /// - /// 交易输出 - /// - public class TransactionOutput : IInteropInterface, ISerializable - { - /// - /// 资产编号 - /// - public UInt256 AssetId; - /// - /// 金额 - /// - public Fixed8 Value; - /// - /// 收款地址 - /// - public UInt160 ScriptHash; - - public int Size => AssetId.Size + Value.Size + ScriptHash.Size; - - void ISerializable.Deserialize(BinaryReader reader) - { - this.AssetId = reader.ReadSerializable(); - this.Value = reader.ReadSerializable(); - if (Value <= Fixed8.Zero) throw new FormatException(); - this.ScriptHash = reader.ReadSerializable(); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(AssetId); - writer.Write(Value); - writer.Write(ScriptHash); - } - - /// - /// 将交易输出转变为json对象 - /// - /// 该交易输出在交易中的索引 - /// 返回json对象 - public JObject ToJson(ushort index) - { - JObject json = new JObject(); - json["n"] = index; - json["asset"] = AssetId.ToString(); - json["value"] = Value.ToString(); - json["address"] = Wallet.ToAddress(ScriptHash); - return json; - } - } -} diff --git a/neo/Core/TransactionResult.cs b/neo/Core/TransactionResult.cs deleted file mode 100644 index 92448610fa..0000000000 --- a/neo/Core/TransactionResult.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Neo.Core -{ - /// - /// 交易结果,表示交易中资产的变化量 - /// - public class TransactionResult - { - /// - /// 资产编号 - /// - public UInt256 AssetId; - /// - /// 该资产的变化量 - /// - public Fixed8 Amount; - } -} diff --git a/neo/Core/TransactionType.cs b/neo/Core/TransactionType.cs deleted file mode 100644 index ea60017ef8..0000000000 --- a/neo/Core/TransactionType.cs +++ /dev/null @@ -1,49 +0,0 @@ -#pragma warning disable CS0612 - -using Neo.IO.Caching; - -namespace Neo.Core -{ - /// - /// 交易类型 - /// - public enum TransactionType : byte - { - /// - /// 用于分配字节费的特殊交易 - /// - [ReflectionCache(typeof(MinerTransaction))] - MinerTransaction = 0x00, - /// - /// 用于分发资产的特殊交易 - /// - [ReflectionCache(typeof(IssueTransaction))] - IssueTransaction = 0x01, - [ReflectionCache(typeof(ClaimTransaction))] - ClaimTransaction = 0x02, - /// - /// 用于报名成为记账候选人的特殊交易 - /// - [ReflectionCache(typeof(EnrollmentTransaction))] - EnrollmentTransaction = 0x20, - /// - /// 用于资产登记的特殊交易 - /// - [ReflectionCache(typeof(RegisterTransaction))] - RegisterTransaction = 0x40, - /// - /// 合约交易,这是最常用的一种交易 - /// - [ReflectionCache(typeof(ContractTransaction))] - ContractTransaction = 0x80, - [ReflectionCache(typeof(StateTransaction))] - StateTransaction = 0x90, - /// - /// Publish scripts to the blockchain for being invoked later. - /// - [ReflectionCache(typeof(PublishTransaction))] - PublishTransaction = 0xd0, - [ReflectionCache(typeof(InvocationTransaction))] - InvocationTransaction = 0xd1 - } -} diff --git a/neo/Core/UnspentCoinState.cs b/neo/Core/UnspentCoinState.cs deleted file mode 100644 index 76caa7d447..0000000000 --- a/neo/Core/UnspentCoinState.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Neo.IO; -using System.IO; -using System.Linq; - -namespace Neo.Core -{ - public class UnspentCoinState : StateBase, ICloneable - { - public CoinState[] Items; - - public override int Size => base.Size + Items.GetVarSize(); - - UnspentCoinState ICloneable.Clone() - { - return new UnspentCoinState - { - Items = (CoinState[])Items.Clone() - }; - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - Items = reader.ReadVarBytes().Select(p => (CoinState)p).ToArray(); - } - - void ICloneable.FromReplica(UnspentCoinState replica) - { - Items = replica.Items; - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.WriteVarBytes(Items.Cast().ToArray()); - } - } -} diff --git a/neo/Core/ValidatorState.cs b/neo/Core/ValidatorState.cs deleted file mode 100644 index 1292cbbcdb..0000000000 --- a/neo/Core/ValidatorState.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Neo.Cryptography.ECC; -using Neo.IO; -using System.IO; - -namespace Neo.Core -{ - public class ValidatorState : StateBase, ICloneable - { - public ECPoint PublicKey; - public bool Registered; - public Fixed8 Votes; - - public override int Size => base.Size + PublicKey.Size + sizeof(bool) + Votes.Size; - - public ValidatorState() { } - - public ValidatorState(ECPoint pubkey) - { - this.PublicKey = pubkey; - this.Registered = false; - this.Votes = Fixed8.Zero; - } - - ValidatorState ICloneable.Clone() - { - return new ValidatorState - { - PublicKey = PublicKey, - Registered = Registered, - Votes = Votes - }; - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - PublicKey = ECPoint.DeserializeFrom(reader, ECCurve.Secp256r1); - Registered = reader.ReadBoolean(); - Votes = reader.ReadSerializable(); - } - - void ICloneable.FromReplica(ValidatorState replica) - { - PublicKey = replica.PublicKey; - Registered = replica.Registered; - Votes = replica.Votes; - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write(PublicKey); - writer.Write(Registered); - writer.Write(Votes); - } - } -} diff --git a/neo/Core/ValidatorsCountState.cs b/neo/Core/ValidatorsCountState.cs deleted file mode 100644 index 9ad96c330a..0000000000 --- a/neo/Core/ValidatorsCountState.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Neo.IO; -using System.IO; - -namespace Neo.Core -{ - public class ValidatorsCountState : StateBase, ICloneable - { - public Fixed8[] Votes; - - public override int Size => base.Size + Votes.GetVarSize(); - - public ValidatorsCountState() - { - this.Votes = new Fixed8[Blockchain.MaxValidators]; - } - - ValidatorsCountState ICloneable.Clone() - { - return new ValidatorsCountState - { - Votes = (Fixed8[])Votes.Clone() - }; - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - Votes = reader.ReadSerializableArray(); - } - - void ICloneable.FromReplica(ValidatorsCountState replica) - { - Votes = replica.Votes; - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.Write(Votes); - } - } -} diff --git a/neo/Core/Witness.cs b/neo/Core/Witness.cs deleted file mode 100644 index b906fe6257..0000000000 --- a/neo/Core/Witness.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Neo.IO; -using Neo.IO.Json; -using System.IO; - -namespace Neo.Core -{ - public class Witness : ISerializable - { - public byte[] InvocationScript; - public byte[] VerificationScript; - - private UInt160 _scriptHash; - public virtual UInt160 ScriptHash - { - get - { - if (_scriptHash == null) - { - _scriptHash = VerificationScript.ToScriptHash(); - } - return _scriptHash; - } - } - - public int Size => InvocationScript.GetVarSize() + VerificationScript.GetVarSize(); - - void ISerializable.Deserialize(BinaryReader reader) - { - InvocationScript = reader.ReadVarBytes(65536); - VerificationScript = reader.ReadVarBytes(65536); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.WriteVarBytes(InvocationScript); - writer.WriteVarBytes(VerificationScript); - } - - /// - /// 变成json对象 - /// - /// 返回json对象 - public JObject ToJson() - { - JObject json = new JObject(); - json["invocation"] = InvocationScript.ToHexString(); - json["verification"] = VerificationScript.ToHexString(); - return json; - } - } -} diff --git a/neo/Cryptography/Base58.cs b/neo/Cryptography/Base58.cs deleted file mode 100644 index 1d8107c1ff..0000000000 --- a/neo/Cryptography/Base58.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using System.Text; - -namespace Neo.Cryptography -{ - public static class Base58 - { - /// - /// base58编码的字母表 - /// - public const string Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - - /// - /// 解码 - /// - /// 要解码的字符串 - /// 返回解码后的字节数组 - public static byte[] Decode(string input) - { - BigInteger bi = BigInteger.Zero; - for (int i = input.Length - 1; i >= 0; i--) - { - int index = Alphabet.IndexOf(input[i]); - if (index == -1) - throw new FormatException(); - bi += index * BigInteger.Pow(58, input.Length - 1 - i); - } - byte[] bytes = bi.ToByteArray(); - Array.Reverse(bytes); - bool stripSignByte = bytes.Length > 1 && bytes[0] == 0 && bytes[1] >= 0x80; - int leadingZeros = 0; - for (int i = 0; i < input.Length && input[i] == Alphabet[0]; i++) - { - leadingZeros++; - } - byte[] tmp = new byte[bytes.Length - (stripSignByte ? 1 : 0) + leadingZeros]; - Array.Copy(bytes, stripSignByte ? 1 : 0, tmp, leadingZeros, tmp.Length - leadingZeros); - return tmp; - } - - /// - /// 编码 - /// - /// 要编码的字节数组 - /// 返回编码后的字符串 - public static string Encode(byte[] input) - { - BigInteger value = new BigInteger(new byte[1].Concat(input).Reverse().ToArray()); - StringBuilder sb = new StringBuilder(); - while (value >= 58) - { - BigInteger mod = value % 58; - sb.Insert(0, Alphabet[(int)mod]); - value /= 58; - } - sb.Insert(0, Alphabet[(int)value]); - foreach (byte b in input) - { - if (b == 0) - sb.Insert(0, Alphabet[0]); - else - break; - } - return sb.ToString(); - } - } -} diff --git a/neo/Cryptography/BloomFilter.cs b/neo/Cryptography/BloomFilter.cs deleted file mode 100644 index 637cd9fac5..0000000000 --- a/neo/Cryptography/BloomFilter.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections; -using System.Linq; - -namespace Neo.Cryptography -{ - public class BloomFilter - { - private readonly uint[] seeds; - private readonly BitArray bits; - - public int K => seeds.Length; - - public int M => bits.Length; - - public uint Tweak { get; private set; } - - public BloomFilter(int m, int k, uint nTweak, byte[] elements = null) - { - this.seeds = Enumerable.Range(0, k).Select(p => (uint)p * 0xFBA4C795 + nTweak).ToArray(); - this.bits = elements == null ? new BitArray(m) : new BitArray(elements); - this.bits.Length = m; - this.Tweak = nTweak; - } - - public void Add(byte[] element) - { - foreach (uint i in seeds.AsParallel().Select(s => element.Murmur32(s))) - bits.Set((int)(i % (uint)bits.Length), true); - } - - public bool Check(byte[] element) - { - foreach (uint i in seeds.AsParallel().Select(s => element.Murmur32(s))) - if (!bits.Get((int)(i % (uint)bits.Length))) - return false; - return true; - } - - public void GetBits(byte[] newBits) - { - bits.CopyTo(newBits, 0); - } - } -} diff --git a/neo/Cryptography/Crypto.cs b/neo/Cryptography/Crypto.cs deleted file mode 100644 index 38a5eaeb8b..0000000000 --- a/neo/Cryptography/Crypto.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Neo.VM; -using System; -using System.Linq; -using System.Security.Cryptography; - -namespace Neo.Cryptography -{ - public class Crypto : ICrypto - { - public static readonly Crypto Default = new Crypto(); - - public byte[] Hash160(byte[] message) - { - return message.Sha256().RIPEMD160(); - } - - public byte[] Hash256(byte[] message) - { - return message.Sha256().Sha256(); - } - - public byte[] Sign(byte[] message, byte[] prikey, byte[] pubkey) - { - using (var ecdsa = ECDsa.Create(new ECParameters - { - Curve = ECCurve.NamedCurves.nistP256, - D = prikey, - Q = new ECPoint - { - X = pubkey.Take(32).ToArray(), - Y = pubkey.Skip(32).ToArray() - } - })) - { - return ecdsa.SignData(message, HashAlgorithmName.SHA256); - } - } - - public bool VerifySignature(byte[] message, byte[] signature, byte[] pubkey) - { - if (pubkey.Length == 33 && (pubkey[0] == 0x02 || pubkey[0] == 0x03)) - { - try - { - pubkey = Cryptography.ECC.ECPoint.DecodePoint(pubkey, Cryptography.ECC.ECCurve.Secp256r1).EncodePoint(false).Skip(1).ToArray(); - } - catch - { - return false; - } - } - else if (pubkey.Length == 65 && pubkey[0] == 0x04) - { - pubkey = pubkey.Skip(1).ToArray(); - } - else if (pubkey.Length != 64) - { - throw new ArgumentException(); - } - using (var ecdsa = ECDsa.Create(new ECParameters - { - Curve = ECCurve.NamedCurves.nistP256, - Q = new ECPoint - { - X = pubkey.Take(32).ToArray(), - Y = pubkey.Skip(32).ToArray() - } - })) - { - return ecdsa.VerifyData(message, signature, HashAlgorithmName.SHA256); - } - } - } -} diff --git a/neo/Cryptography/ECC/ECCurve.cs b/neo/Cryptography/ECC/ECCurve.cs deleted file mode 100644 index aa4a956f76..0000000000 --- a/neo/Cryptography/ECC/ECCurve.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Globalization; -using System.Numerics; - -namespace Neo.Cryptography.ECC -{ - /// - /// ECC椭圆曲线参数 - /// - public class ECCurve - { - internal readonly BigInteger Q; - internal readonly ECFieldElement A; - internal readonly ECFieldElement B; - internal readonly BigInteger N; - /// - /// 无穷远点 - /// - public readonly ECPoint Infinity; - /// - /// 基点 - /// - public readonly ECPoint G; - - private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G) - { - this.Q = Q; - this.A = new ECFieldElement(A, this); - this.B = new ECFieldElement(B, this); - this.N = N; - this.Infinity = new ECPoint(null, null, this); - this.G = ECPoint.DecodePoint(G, this); - } - - /// - /// 曲线secp256k1 - /// - public static readonly ECCurve Secp256k1 = new ECCurve - ( - BigInteger.Parse("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", NumberStyles.AllowHexSpecifier), - BigInteger.Zero, - 7, - BigInteger.Parse("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", NumberStyles.AllowHexSpecifier), - ("04" + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8").HexToBytes() - ); - - /// - /// 曲线secp256r1 - /// - public static readonly ECCurve Secp256r1 = new ECCurve - ( - BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", NumberStyles.AllowHexSpecifier), - BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", NumberStyles.AllowHexSpecifier), - BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier), - BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier), - ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes() - ); - } -} diff --git a/neo/Cryptography/ECC/ECDsa.cs b/neo/Cryptography/ECC/ECDsa.cs deleted file mode 100644 index 8e0f028e99..0000000000 --- a/neo/Cryptography/ECC/ECDsa.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using System.Security.Cryptography; - -namespace Neo.Cryptography.ECC -{ - /// - /// 提供椭圆曲线数字签名算法(ECDSA)的功能 - /// - public class ECDsa - { - private readonly byte[] privateKey; - private readonly ECPoint publicKey; - private readonly ECCurve curve; - - /// - /// 根据指定的私钥和曲线参数来创建新的ECDsa对象,该对象可用于签名 - /// - /// 私钥 - /// 椭圆曲线参数 - public ECDsa(byte[] privateKey, ECCurve curve) - : this(curve.G * privateKey) - { - this.privateKey = privateKey; - } - - /// - /// 根据指定的公钥来创建新的ECDsa对象,该对象可用于验证签名 - /// - /// 公钥 - public ECDsa(ECPoint publicKey) - { - this.publicKey = publicKey; - this.curve = publicKey.Curve; - } - - private BigInteger CalculateE(BigInteger n, byte[] message) - { - int messageBitLength = message.Length * 8; - BigInteger trunc = new BigInteger(message.Reverse().Concat(new byte[1]).ToArray()); - if (n.GetBitLength() < messageBitLength) - { - trunc >>= messageBitLength - n.GetBitLength(); - } - return trunc; - } - - /// - /// 生成椭圆曲线数字签名 - /// - /// 要签名的消息 - /// 返回签名的数字编码(r,s) - public BigInteger[] GenerateSignature(byte[] message) - { - if (privateKey == null) throw new InvalidOperationException(); - BigInteger e = CalculateE(curve.N, message); - BigInteger d = new BigInteger(privateKey.Reverse().Concat(new byte[1]).ToArray()); - BigInteger r, s; - using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) - { - do - { - BigInteger k; - do - { - do - { - k = rng.NextBigInteger(curve.N.GetBitLength()); - } - while (k.Sign == 0 || k.CompareTo(curve.N) >= 0); - ECPoint p = ECPoint.Multiply(curve.G, k); - BigInteger x = p.X.Value; - r = x.Mod(curve.N); - } - while (r.Sign == 0); - s = (k.ModInverse(curve.N) * (e + d * r)).Mod(curve.N); - if (s > curve.N / 2) - { - s = curve.N - s; - } - } - while (s.Sign == 0); - } - return new BigInteger[] { r, s }; - } - - private static ECPoint SumOfTwoMultiplies(ECPoint P, BigInteger k, ECPoint Q, BigInteger l) - { - int m = Math.Max(k.GetBitLength(), l.GetBitLength()); - ECPoint Z = P + Q; - ECPoint R = P.Curve.Infinity; - for (int i = m - 1; i >= 0; --i) - { - R = R.Twice(); - if (k.TestBit(i)) - { - if (l.TestBit(i)) - R = R + Z; - else - R = R + P; - } - else - { - if (l.TestBit(i)) - R = R + Q; - } - } - return R; - } - - /// - /// 验证签名的合法性 - /// - /// 要验证的消息 - /// 签名的数字编码 - /// 签名的数字编码 - /// 返回验证的结果 - public bool VerifySignature(byte[] message, BigInteger r, BigInteger s) - { - if (r.Sign < 1 || s.Sign < 1 || r.CompareTo(curve.N) >= 0 || s.CompareTo(curve.N) >= 0) - return false; - BigInteger e = CalculateE(curve.N, message); - BigInteger c = s.ModInverse(curve.N); - BigInteger u1 = (e * c).Mod(curve.N); - BigInteger u2 = (r * c).Mod(curve.N); - ECPoint point = SumOfTwoMultiplies(curve.G, u1, publicKey, u2); - BigInteger v = point.X.Value.Mod(curve.N); - return v.Equals(r); - } - } -} diff --git a/neo/Cryptography/ECC/ECFieldElement.cs b/neo/Cryptography/ECC/ECFieldElement.cs deleted file mode 100644 index 4ce303dcc8..0000000000 --- a/neo/Cryptography/ECC/ECFieldElement.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; -using System.Numerics; - -namespace Neo.Cryptography.ECC -{ - internal class ECFieldElement : IComparable, IEquatable - { - internal readonly BigInteger Value; - private readonly ECCurve curve; - - public ECFieldElement(BigInteger value, ECCurve curve) - { - if (value >= curve.Q) - throw new ArgumentException("x value too large in field element"); - this.Value = value; - this.curve = curve; - } - - public int CompareTo(ECFieldElement other) - { - if (ReferenceEquals(this, other)) return 0; - return Value.CompareTo(other.Value); - } - - public override bool Equals(object obj) - { - if (obj == this) - return true; - - ECFieldElement other = obj as ECFieldElement; - - if (other == null) - return false; - - return Equals(other); - } - - public bool Equals(ECFieldElement other) - { - return Value.Equals(other.Value); - } - - private static BigInteger[] FastLucasSequence(BigInteger p, BigInteger P, BigInteger Q, BigInteger k) - { - int n = k.GetBitLength(); - int s = k.GetLowestSetBit(); - - Debug.Assert(k.TestBit(s)); - - BigInteger Uh = 1; - BigInteger Vl = 2; - BigInteger Vh = P; - BigInteger Ql = 1; - BigInteger Qh = 1; - - for (int j = n - 1; j >= s + 1; --j) - { - Ql = (Ql * Qh).Mod(p); - - if (k.TestBit(j)) - { - Qh = (Ql * Q).Mod(p); - Uh = (Uh * Vh).Mod(p); - Vl = (Vh * Vl - P * Ql).Mod(p); - Vh = ((Vh * Vh) - (Qh << 1)).Mod(p); - } - else - { - Qh = Ql; - Uh = (Uh * Vl - Ql).Mod(p); - Vh = (Vh * Vl - P * Ql).Mod(p); - Vl = ((Vl * Vl) - (Ql << 1)).Mod(p); - } - } - - Ql = (Ql * Qh).Mod(p); - Qh = (Ql * Q).Mod(p); - Uh = (Uh * Vl - Ql).Mod(p); - Vl = (Vh * Vl - P * Ql).Mod(p); - Ql = (Ql * Qh).Mod(p); - - for (int j = 1; j <= s; ++j) - { - Uh = Uh * Vl * p; - Vl = ((Vl * Vl) - (Ql << 1)).Mod(p); - Ql = (Ql * Ql).Mod(p); - } - - return new BigInteger[] { Uh, Vl }; - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public ECFieldElement Sqrt() - { - if (curve.Q.TestBit(1)) - { - ECFieldElement z = new ECFieldElement(BigInteger.ModPow(Value, (curve.Q >> 2) + 1, curve.Q), curve); - return z.Square().Equals(this) ? z : null; - } - BigInteger qMinusOne = curve.Q - 1; - BigInteger legendreExponent = qMinusOne >> 1; - if (BigInteger.ModPow(Value, legendreExponent, curve.Q) != 1) - return null; - BigInteger u = qMinusOne >> 2; - BigInteger k = (u << 1) + 1; - BigInteger Q = this.Value; - BigInteger fourQ = (Q << 2).Mod(curve.Q); - BigInteger U, V; - do - { - Random rand = new Random(); - BigInteger P; - do - { - P = rand.NextBigInteger(curve.Q.GetBitLength()); - } - while (P >= curve.Q || BigInteger.ModPow(P * P - fourQ, legendreExponent, curve.Q) != qMinusOne); - BigInteger[] result = FastLucasSequence(curve.Q, P, Q, k); - U = result[0]; - V = result[1]; - if ((V * V).Mod(curve.Q) == fourQ) - { - if (V.TestBit(0)) - { - V += curve.Q; - } - V >>= 1; - Debug.Assert((V * V).Mod(curve.Q) == Value); - return new ECFieldElement(V, curve); - } - } - while (U.Equals(BigInteger.One) || U.Equals(qMinusOne)); - return null; - } - - public ECFieldElement Square() - { - return new ECFieldElement((Value * Value).Mod(curve.Q), curve); - } - - public byte[] ToByteArray() - { - byte[] data = Value.ToByteArray(); - if (data.Length == 32) - return data.Reverse().ToArray(); - if (data.Length > 32) - return data.Take(32).Reverse().ToArray(); - return Enumerable.Repeat(0, 32 - data.Length).Concat(data.Reverse()).ToArray(); - } - - public static ECFieldElement operator -(ECFieldElement x) - { - return new ECFieldElement((-x.Value).Mod(x.curve.Q), x.curve); - } - - public static ECFieldElement operator *(ECFieldElement x, ECFieldElement y) - { - return new ECFieldElement((x.Value * y.Value).Mod(x.curve.Q), x.curve); - } - - public static ECFieldElement operator /(ECFieldElement x, ECFieldElement y) - { - return new ECFieldElement((x.Value * y.Value.ModInverse(x.curve.Q)).Mod(x.curve.Q), x.curve); - } - - public static ECFieldElement operator +(ECFieldElement x, ECFieldElement y) - { - return new ECFieldElement((x.Value + y.Value).Mod(x.curve.Q), x.curve); - } - - public static ECFieldElement operator -(ECFieldElement x, ECFieldElement y) - { - return new ECFieldElement((x.Value - y.Value).Mod(x.curve.Q), x.curve); - } - } -} diff --git a/neo/Cryptography/ECC/ECPoint.cs b/neo/Cryptography/ECC/ECPoint.cs deleted file mode 100644 index ba529f8b02..0000000000 --- a/neo/Cryptography/ECC/ECPoint.cs +++ /dev/null @@ -1,464 +0,0 @@ -using Neo.IO; -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Numerics; - -namespace Neo.Cryptography.ECC -{ - public class ECPoint : IComparable, IEquatable, ISerializable - { - internal ECFieldElement X, Y; - internal readonly ECCurve Curve; - - /// - /// 判断是否为无穷远点 - /// - public bool IsInfinity - { - get { return X == null && Y == null; } - } - - public int Size => IsInfinity ? 1 : 33; - - public ECPoint() - : this(null, null, ECCurve.Secp256r1) - { - } - - internal ECPoint(ECFieldElement x, ECFieldElement y, ECCurve curve) - { - if ((x != null && y == null) || (x == null && y != null)) - throw new ArgumentException("Exactly one of the field elements is null"); - this.X = x; - this.Y = y; - this.Curve = curve; - } - - /// - /// 与另一对象进行比较 - /// - /// 另一对象 - /// 返回比较的结果 - public int CompareTo(ECPoint other) - { - if (ReferenceEquals(this, other)) return 0; - int result = X.CompareTo(other.X); - if (result != 0) return result; - return Y.CompareTo(other.Y); - } - - /// - /// 从字节数组中解码 - /// - /// 要解码的字节数组 - /// 曲线参数 - /// - public static ECPoint DecodePoint(byte[] encoded, ECCurve curve) - { - ECPoint p = null; - int expectedLength = (curve.Q.GetBitLength() + 7) / 8; - switch (encoded[0]) - { - case 0x00: // infinity - { - if (encoded.Length != 1) - throw new FormatException("Incorrect length for infinity encoding"); - p = curve.Infinity; - break; - } - case 0x02: // compressed - case 0x03: // compressed - { - if (encoded.Length != (expectedLength + 1)) - throw new FormatException("Incorrect length for compressed encoding"); - int yTilde = encoded[0] & 1; - BigInteger X1 = new BigInteger(encoded.Skip(1).Reverse().Concat(new byte[1]).ToArray()); - p = DecompressPoint(yTilde, X1, curve); - break; - } - case 0x04: // uncompressed - case 0x06: // hybrid - case 0x07: // hybrid - { - if (encoded.Length != (2 * expectedLength + 1)) - throw new FormatException("Incorrect length for uncompressed/hybrid encoding"); - BigInteger X1 = new BigInteger(encoded.Skip(1).Take(expectedLength).Reverse().Concat(new byte[1]).ToArray()); - BigInteger Y1 = new BigInteger(encoded.Skip(1 + expectedLength).Reverse().Concat(new byte[1]).ToArray()); - p = new ECPoint(new ECFieldElement(X1, curve), new ECFieldElement(Y1, curve), curve); - break; - } - default: - throw new FormatException("Invalid point encoding " + encoded[0]); - } - return p; - } - - private static ECPoint DecompressPoint(int yTilde, BigInteger X1, ECCurve curve) - { - ECFieldElement x = new ECFieldElement(X1, curve); - ECFieldElement alpha = x * (x.Square() + curve.A) + curve.B; - ECFieldElement beta = alpha.Sqrt(); - - // - // if we can't find a sqrt we haven't got a point on the - // curve - run! - // - if (beta == null) - throw new ArithmeticException("Invalid point compression"); - - BigInteger betaValue = beta.Value; - int bit0 = betaValue.IsEven ? 0 : 1; - - if (bit0 != yTilde) - { - // Use the other root - beta = new ECFieldElement(curve.Q - betaValue, curve); - } - - return new ECPoint(x, beta, curve); - } - - void ISerializable.Deserialize(BinaryReader reader) - { - ECPoint p = DeserializeFrom(reader, Curve); - X = p.X; - Y = p.Y; - } - - /// - /// 反序列化 - /// - /// 数据来源 - /// 椭圆曲线参数 - /// - public static ECPoint DeserializeFrom(BinaryReader reader, ECCurve curve) - { - int expectedLength = (curve.Q.GetBitLength() + 7) / 8; - byte[] buffer = new byte[1 + expectedLength * 2]; - buffer[0] = reader.ReadByte(); - switch (buffer[0]) - { - case 0x00: - return curve.Infinity; - case 0x02: - case 0x03: - reader.Read(buffer, 1, expectedLength); - return DecodePoint(buffer.Take(1 + expectedLength).ToArray(), curve); - case 0x04: - case 0x06: - case 0x07: - reader.Read(buffer, 1, expectedLength * 2); - return DecodePoint(buffer, curve); - default: - throw new FormatException("Invalid point encoding " + buffer[0]); - } - } - - /// - /// 将对象编码到字节数组 - /// - /// 是否为压缩格式的编码 - /// 返回编码后的字节数组 - public byte[] EncodePoint(bool commpressed) - { - if (IsInfinity) return new byte[1]; - byte[] data; - if (commpressed) - { - data = new byte[33]; - } - else - { - data = new byte[65]; - byte[] yBytes = Y.Value.ToByteArray().Reverse().ToArray(); - Buffer.BlockCopy(yBytes, 0, data, 65 - yBytes.Length, yBytes.Length); - } - byte[] xBytes = X.Value.ToByteArray().Reverse().ToArray(); - Buffer.BlockCopy(xBytes, 0, data, 33 - xBytes.Length, xBytes.Length); - data[0] = commpressed ? Y.Value.IsEven ? (byte)0x02 : (byte)0x03 : (byte)0x04; - return data; - } - - /// - /// 比较与另一个对象是否相等 - /// - /// 另一个对象 - /// 返回比较的结果 - public bool Equals(ECPoint other) - { - if (ReferenceEquals(this, other)) return true; - if (ReferenceEquals(null, other)) return false; - if (IsInfinity && other.IsInfinity) return true; - if (IsInfinity || other.IsInfinity) return false; - return X.Equals(other.X) && Y.Equals(other.Y); - } - - /// - /// 比较与另一个对象是否相等 - /// - /// 另一个对象 - /// 返回比较的结果 - public override bool Equals(object obj) - { - return Equals(obj as ECPoint); - } - - /// - /// 从指定的字节数组中解析出公钥,这个字节数组可以是任意形式的公钥编码、或者包含私钥的内容 - /// - /// 要解析的字节数组 - /// 椭圆曲线参数 - /// 返回解析出的公钥 - public static ECPoint FromBytes(byte[] pubkey, ECCurve curve) - { - switch (pubkey.Length) - { - case 33: - case 65: - return DecodePoint(pubkey, curve); - case 64: - case 72: - return DecodePoint(new byte[] { 0x04 }.Concat(pubkey.Skip(pubkey.Length - 64)).ToArray(), curve); - case 96: - case 104: - return DecodePoint(new byte[] { 0x04 }.Concat(pubkey.Skip(pubkey.Length - 96).Take(64)).ToArray(), curve); - default: - throw new FormatException(); - } - } - - /// - /// 获取HashCode - /// - /// 返回HashCode - public override int GetHashCode() - { - return X.GetHashCode() + Y.GetHashCode(); - } - - internal static ECPoint Multiply(ECPoint p, BigInteger k) - { - // floor(log2(k)) - int m = k.GetBitLength(); - - // width of the Window NAF - sbyte width; - - // Required length of precomputation array - int reqPreCompLen; - - // Determine optimal width and corresponding length of precomputation - // array based on literature values - if (m < 13) - { - width = 2; - reqPreCompLen = 1; - } - else if (m < 41) - { - width = 3; - reqPreCompLen = 2; - } - else if (m < 121) - { - width = 4; - reqPreCompLen = 4; - } - else if (m < 337) - { - width = 5; - reqPreCompLen = 8; - } - else if (m < 897) - { - width = 6; - reqPreCompLen = 16; - } - else if (m < 2305) - { - width = 7; - reqPreCompLen = 32; - } - else - { - width = 8; - reqPreCompLen = 127; - } - - // The length of the precomputation array - int preCompLen = 1; - - ECPoint[] preComp = preComp = new ECPoint[] { p }; - ECPoint twiceP = p.Twice(); - - if (preCompLen < reqPreCompLen) - { - // Precomputation array must be made bigger, copy existing preComp - // array into the larger new preComp array - ECPoint[] oldPreComp = preComp; - preComp = new ECPoint[reqPreCompLen]; - Array.Copy(oldPreComp, 0, preComp, 0, preCompLen); - - for (int i = preCompLen; i < reqPreCompLen; i++) - { - // Compute the new ECPoints for the precomputation array. - // The values 1, 3, 5, ..., 2^(width-1)-1 times p are - // computed - preComp[i] = twiceP + preComp[i - 1]; - } - } - - // Compute the Window NAF of the desired width - sbyte[] wnaf = WindowNaf(width, k); - int l = wnaf.Length; - - // Apply the Window NAF to p using the precomputed ECPoint values. - ECPoint q = p.Curve.Infinity; - for (int i = l - 1; i >= 0; i--) - { - q = q.Twice(); - - if (wnaf[i] != 0) - { - if (wnaf[i] > 0) - { - q += preComp[(wnaf[i] - 1) / 2]; - } - else - { - // wnaf[i] < 0 - q -= preComp[(-wnaf[i] - 1) / 2]; - } - } - } - - return q; - } - - public static ECPoint Parse(string value, ECCurve curve) - { - return DecodePoint(value.HexToBytes(), curve); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(EncodePoint(true)); - } - - public override string ToString() - { - return EncodePoint(true).ToHexString(); - } - - public static bool TryParse(string value, ECCurve curve, out ECPoint point) - { - try - { - point = Parse(value, curve); - return true; - } - catch (FormatException) - { - point = null; - return false; - } - } - - internal ECPoint Twice() - { - if (this.IsInfinity) - return this; - if (this.Y.Value.Sign == 0) - return Curve.Infinity; - ECFieldElement TWO = new ECFieldElement(2, Curve); - ECFieldElement THREE = new ECFieldElement(3, Curve); - ECFieldElement gamma = (this.X.Square() * THREE + Curve.A) / (Y * TWO); - ECFieldElement x3 = gamma.Square() - this.X * TWO; - ECFieldElement y3 = gamma * (this.X - x3) - this.Y; - return new ECPoint(x3, y3, Curve); - } - - private static sbyte[] WindowNaf(sbyte width, BigInteger k) - { - sbyte[] wnaf = new sbyte[k.GetBitLength() + 1]; - short pow2wB = (short)(1 << width); - int i = 0; - int length = 0; - while (k.Sign > 0) - { - if (!k.IsEven) - { - BigInteger remainder = k % pow2wB; - if (remainder.TestBit(width - 1)) - { - wnaf[i] = (sbyte)(remainder - pow2wB); - } - else - { - wnaf[i] = (sbyte)remainder; - } - k -= wnaf[i]; - length = i; - } - else - { - wnaf[i] = 0; - } - k >>= 1; - i++; - } - length++; - sbyte[] wnafShort = new sbyte[length]; - Array.Copy(wnaf, 0, wnafShort, 0, length); - return wnafShort; - } - - public static ECPoint operator -(ECPoint x) - { - return new ECPoint(x.X, -x.Y, x.Curve); - } - - public static ECPoint operator *(ECPoint p, byte[] n) - { - if (p == null || n == null) - throw new ArgumentNullException(); - if (n.Length != 32) - throw new ArgumentException(); - if (p.IsInfinity) - return p; - //BigInteger的内存无法被保护,可能会有安全隐患。此处的k需要重写一个SecureBigInteger类来代替 - BigInteger k = new BigInteger(n.Reverse().Concat(new byte[1]).ToArray()); - if (k.Sign == 0) - return p.Curve.Infinity; - return Multiply(p, k); - } - - public static ECPoint operator +(ECPoint x, ECPoint y) - { - if (x.IsInfinity) - return y; - if (y.IsInfinity) - return x; - if (x.X.Equals(y.X)) - { - if (x.Y.Equals(y.Y)) - return x.Twice(); - Debug.Assert(x.Y.Equals(-y.Y)); - return x.Curve.Infinity; - } - ECFieldElement gamma = (y.Y - x.Y) / (y.X - x.X); - ECFieldElement x3 = gamma.Square() - x.X - y.X; - ECFieldElement y3 = gamma * (x.X - x3) - x.Y; - return new ECPoint(x3, y3, x.Curve); - } - - public static ECPoint operator -(ECPoint x, ECPoint y) - { - if (y.IsInfinity) - return x; - return x + (-y); - } - } -} diff --git a/neo/Cryptography/Helper.cs b/neo/Cryptography/Helper.cs deleted file mode 100644 index 539ecf3419..0000000000 --- a/neo/Cryptography/Helper.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Security; -using System.Security.Cryptography; -using System.Text; -using System.Threading; - -namespace Neo.Cryptography -{ - /// - /// 包含一系列密码学算法的扩展方法 - /// - public static class Helper - { - private static ThreadLocal _sha256 = new ThreadLocal(() => SHA256.Create()); - private static ThreadLocal _ripemd160 = new ThreadLocal(() => new RIPEMD160Managed()); - - internal static byte[] AES256Decrypt(this byte[] block, byte[] key) - { - using (Aes aes = Aes.Create()) - { - aes.Key = key; - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - using (ICryptoTransform decryptor = aes.CreateDecryptor()) - { - return decryptor.TransformFinalBlock(block, 0, block.Length); - } - } - } - - internal static byte[] AES256Encrypt(this byte[] block, byte[] key) - { - using (Aes aes = Aes.Create()) - { - aes.Key = key; - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - using (ICryptoTransform encryptor = aes.CreateEncryptor()) - { - return encryptor.TransformFinalBlock(block, 0, block.Length); - } - } - } - - internal static byte[] AesDecrypt(this 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); - } - } - } - - internal static byte[] AesEncrypt(this 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); - } - } - } - - public static byte[] Base58CheckDecode(this string input) - { - byte[] buffer = Base58.Decode(input); - if (buffer.Length < 4) throw new FormatException(); - byte[] checksum = buffer.Sha256(0, buffer.Length - 4).Sha256(); - if (!buffer.Skip(buffer.Length - 4).SequenceEqual(checksum.Take(4))) - throw new FormatException(); - return buffer.Take(buffer.Length - 4).ToArray(); - } - - public static string Base58CheckEncode(this byte[] data) - { - byte[] checksum = data.Sha256().Sha256(); - byte[] buffer = new byte[data.Length + 4]; - Buffer.BlockCopy(data, 0, buffer, 0, data.Length); - Buffer.BlockCopy(checksum, 0, buffer, data.Length, 4); - return Base58.Encode(buffer); - } - - /// - /// 求字节数组的ripemd160散列值 - /// - /// 字节数组 - /// 返回该散列值 - public static byte[] RIPEMD160(this IEnumerable value) - { - return _ripemd160.Value.ComputeHash(value.ToArray()); - } - - public static uint Murmur32(this IEnumerable value, uint seed) - { - using (Murmur3 murmur = new Murmur3(seed)) - { - return murmur.ComputeHash(value.ToArray()).ToUInt32(0); - } - } - - /// - /// 求字节数组的sha256散列值 - /// - /// 字节数组 - /// 返回该散列值 - public static byte[] Sha256(this IEnumerable value) - { - return _sha256.Value.ComputeHash(value.ToArray()); - } - - /// - /// 求字节数组的sha256散列值 - /// - /// 字节数组 - /// 偏移量,散列计算时从该偏移量处开始 - /// 要计算散列值的字节数量 - /// 返回该散列值 - public static byte[] Sha256(this byte[] value, int offset, int count) - { - return _sha256.Value.ComputeHash(value, offset, count); - } - - internal static byte[] ToAesKey(this 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; - } - } - - internal static byte[] ToAesKey(this SecureString password) - { - using (SHA256 sha256 = SHA256.Create()) - { - byte[] passwordBytes = password.ToArray(); - byte[] passwordHash = sha256.ComputeHash(passwordBytes); - byte[] passwordHash2 = sha256.ComputeHash(passwordHash); - Array.Clear(passwordBytes, 0, passwordBytes.Length); - Array.Clear(passwordHash, 0, passwordHash.Length); - return passwordHash2; - } - } - - internal static byte[] ToArray(this SecureString s) - { - if (s == null) - throw new NullReferenceException(); - if (s.Length == 0) - return new byte[0]; - List result = new List(); - IntPtr ptr = SecureStringMarshal.SecureStringToGlobalAllocAnsi(s); - try - { - int i = 0; - do - { - byte b = Marshal.ReadByte(ptr, i++); - if (b == 0) - break; - result.Add(b); - } while (true); - } - finally - { - Marshal.ZeroFreeGlobalAllocAnsi(ptr); - } - return result.ToArray(); - } - } -} diff --git a/neo/Cryptography/MerkleTree.cs b/neo/Cryptography/MerkleTree.cs deleted file mode 100644 index 76b2ac198a..0000000000 --- a/neo/Cryptography/MerkleTree.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Neo.Cryptography -{ - /// - /// 哈希树 - /// - public class MerkleTree - { - private MerkleTreeNode root; - - public int Depth { get; private set; } - - internal MerkleTree(UInt256[] hashes) - { - if (hashes.Length == 0) throw new ArgumentException(); - this.root = Build(hashes.Select(p => new MerkleTreeNode { Hash = p }).ToArray()); - int depth = 1; - for (MerkleTreeNode i = root; i.LeftChild != null; i = i.LeftChild) - depth++; - this.Depth = depth; - } - - private static MerkleTreeNode Build(MerkleTreeNode[] leaves) - { - if (leaves.Length == 0) throw new ArgumentException(); - if (leaves.Length == 1) return leaves[0]; - MerkleTreeNode[] parents = new MerkleTreeNode[(leaves.Length + 1) / 2]; - for (int i = 0; i < parents.Length; i++) - { - parents[i] = new MerkleTreeNode(); - parents[i].LeftChild = leaves[i * 2]; - leaves[i * 2].Parent = parents[i]; - if (i * 2 + 1 == leaves.Length) - { - parents[i].RightChild = parents[i].LeftChild; - } - else - { - parents[i].RightChild = leaves[i * 2 + 1]; - leaves[i * 2 + 1].Parent = parents[i]; - } - parents[i].Hash = new UInt256(Crypto.Default.Hash256(parents[i].LeftChild.Hash.ToArray().Concat(parents[i].RightChild.Hash.ToArray()).ToArray())); - } - return Build(parents); //TailCall - } - - /// - /// 计算根节点的值 - /// - /// 子节点列表 - /// 返回计算的结果 - public static UInt256 ComputeRoot(UInt256[] hashes) - { - if (hashes.Length == 0) throw new ArgumentException(); - if (hashes.Length == 1) return hashes[0]; - MerkleTree tree = new MerkleTree(hashes); - return tree.root.Hash; - } - - private static void DepthFirstSearch(MerkleTreeNode node, IList hashes) - { - if (node.LeftChild == null) - { - // if left is null, then right must be null - hashes.Add(node.Hash); - } - else - { - DepthFirstSearch(node.LeftChild, hashes); - DepthFirstSearch(node.RightChild, hashes); - } - } - - // depth-first order - public UInt256[] ToHashArray() - { - List hashes = new List(); - DepthFirstSearch(root, hashes); - return hashes.ToArray(); - } - - public void Trim(BitArray flags) - { - flags = new BitArray(flags); - flags.Length = 1 << (Depth - 1); - Trim(root, 0, Depth, flags); - } - - private static void Trim(MerkleTreeNode node, int index, int depth, BitArray flags) - { - if (depth == 1) return; - if (node.LeftChild == null) return; // if left is null, then right must be null - if (depth == 2) - { - if (!flags.Get(index * 2) && !flags.Get(index * 2 + 1)) - { - node.LeftChild = null; - node.RightChild = null; - } - } - else - { - Trim(node.LeftChild, index * 2, depth - 1, flags); - Trim(node.RightChild, index * 2 + 1, depth - 1, flags); - if (node.LeftChild.LeftChild == null && node.RightChild.RightChild == null) - { - node.LeftChild = null; - node.RightChild = null; - } - } - } - } -} diff --git a/neo/Cryptography/MerkleTreeNode.cs b/neo/Cryptography/MerkleTreeNode.cs deleted file mode 100644 index 6bfc75c8d1..0000000000 --- a/neo/Cryptography/MerkleTreeNode.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Neo.Cryptography -{ - internal class MerkleTreeNode - { - public UInt256 Hash; - public MerkleTreeNode Parent; - public MerkleTreeNode LeftChild; - public MerkleTreeNode RightChild; - - public bool IsLeaf => LeftChild == null && RightChild == null; - - public bool IsRoot => Parent == null; - } -} diff --git a/neo/Cryptography/Murmur3.cs b/neo/Cryptography/Murmur3.cs deleted file mode 100644 index e54c19ebc1..0000000000 --- a/neo/Cryptography/Murmur3.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; - -namespace Neo.Cryptography -{ - public sealed class Murmur3 : HashAlgorithm - { - private const uint c1 = 0xcc9e2d51; - private const uint c2 = 0x1b873593; - private const int r1 = 15; - private const int r2 = 13; - private const uint m = 5; - private const uint n = 0xe6546b64; - - private readonly uint seed; - private uint hash; - private int length; - - public override int HashSize => 32; - - public Murmur3(uint seed) - { - this.seed = seed; - Initialize(); - } - - protected override void HashCore(byte[] array, int ibStart, int cbSize) - { - length += cbSize; - int remainder = cbSize & 3; - int alignedLength = ibStart + (cbSize - remainder); - for (int i = ibStart; i < alignedLength; i += 4) - { - uint k = array.ToUInt32(i); - k *= c1; - k = RotateLeft(k, r1); - k *= c2; - hash ^= k; - hash = RotateLeft(hash, r2); - hash = hash * m + n; - } - if (remainder > 0) - { - uint remainingBytes = 0; - switch (remainder) - { - case 3: remainingBytes ^= (uint)array[alignedLength + 2] << 16; goto case 2; - case 2: remainingBytes ^= (uint)array[alignedLength + 1] << 8; goto case 1; - case 1: remainingBytes ^= array[alignedLength]; break; - } - remainingBytes *= c1; - remainingBytes = RotateLeft(remainingBytes, r1); - remainingBytes *= c2; - hash ^= remainingBytes; - } - } - - protected override byte[] HashFinal() - { - hash ^= (uint)length; - hash ^= hash >> 16; - hash *= 0x85ebca6b; - hash ^= hash >> 13; - hash *= 0xc2b2ae35; - hash ^= hash >> 16; - return BitConverter.GetBytes(hash); - } - - public override void Initialize() - { - hash = seed; - length = 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint RotateLeft(uint x, byte n) - { - return (x << n) | (x >> (32 - n)); - } - } -} diff --git a/neo/Cryptography/ProtectedMemoryContext.cs b/neo/Cryptography/ProtectedMemoryContext.cs deleted file mode 100644 index b9453a1a3b..0000000000 --- a/neo/Cryptography/ProtectedMemoryContext.cs +++ /dev/null @@ -1,39 +0,0 @@ -#if NET47 -using System; -using System.Collections.Generic; -using System.Security.Cryptography; - -namespace Neo.Cryptography -{ - internal class ProtectedMemoryContext : IDisposable - { - private static Dictionary counts = new Dictionary(); - private byte[] memory; - private MemoryProtectionScope scope; - - public ProtectedMemoryContext(byte[] memory, MemoryProtectionScope scope) - { - this.memory = memory; - this.scope = scope; - if (counts.ContainsKey(memory)) - { - counts[memory]++; - } - else - { - counts.Add(memory, 1); - ProtectedMemory.Unprotect(memory, scope); - } - } - - void IDisposable.Dispose() - { - if (--counts[memory] == 0) - { - counts.Remove(memory); - ProtectedMemory.Protect(memory, scope); - } - } - } -} -#endif \ No newline at end of file diff --git a/neo/Cryptography/RIPEMD160Managed.cs b/neo/Cryptography/RIPEMD160Managed.cs deleted file mode 100644 index 898780dc47..0000000000 --- a/neo/Cryptography/RIPEMD160Managed.cs +++ /dev/null @@ -1,1054 +0,0 @@ -#if !NET47 -using System; -using System.Runtime.InteropServices; -using System.Security; -using System.Security.Cryptography; - -namespace Neo.Cryptography -{ - [ComVisible(true)] - public class RIPEMD160Managed : HashAlgorithm - { - private byte[] _buffer; - private long _count; // Number of bytes in the hashed message - private uint[] _stateMD160; - private uint[] _blockDWords; - - public override int HashSize => 160; - - // - // public constructors - // - - public RIPEMD160Managed() - { - _stateMD160 = new uint[5]; - _blockDWords = new uint[16]; - _buffer = new byte[64]; - - InitializeState(); - } - - // - // public methods - // - - public override void Initialize() - { - InitializeState(); - - // Zeroize potentially sensitive information. - Array.Clear(_blockDWords, 0, _blockDWords.Length); - Array.Clear(_buffer, 0, _buffer.Length); - } - - [System.Security.SecuritySafeCritical] // auto-generated - protected override void HashCore(byte[] rgb, int ibStart, int cbSize) - { - _HashData(rgb, ibStart, cbSize); - } - - [System.Security.SecuritySafeCritical] // auto-generated - protected override byte[] HashFinal() - { - return _EndHash(); - } - - // - // private methods - // - - private void InitializeState() - { - _count = 0; - - // Use the same chaining values (IVs) as in SHA1, - // The convention is little endian however (same as MD4) - _stateMD160[0] = 0x67452301; - _stateMD160[1] = 0xefcdab89; - _stateMD160[2] = 0x98badcfe; - _stateMD160[3] = 0x10325476; - _stateMD160[4] = 0xc3d2e1f0; - } - - [System.Security.SecurityCritical] // auto-generated - private unsafe void _HashData(byte[] partIn, int ibStart, int cbSize) - { - int bufferLen; - int partInLen = cbSize; - int partInBase = ibStart; - - /* Compute length of buffer */ - bufferLen = (int)(_count & 0x3f); - - /* Update number of bytes */ - _count += partInLen; - - fixed (uint* stateMD160 = _stateMD160) - { - fixed (byte* buffer = _buffer) - { - fixed (uint* blockDWords = _blockDWords) - { - if ((bufferLen > 0) && (bufferLen + partInLen >= 64)) - { - Buffer.BlockCopy(partIn, partInBase, _buffer, bufferLen, 64 - bufferLen); - partInBase += (64 - bufferLen); - partInLen -= (64 - bufferLen); - MDTransform(blockDWords, stateMD160, buffer); - bufferLen = 0; - } - - /* Copy input to temporary buffer and hash */ - while (partInLen >= 64) - { - Buffer.BlockCopy(partIn, partInBase, _buffer, 0, 64); - partInBase += 64; - partInLen -= 64; - MDTransform(blockDWords, stateMD160, buffer); - } - - if (partInLen > 0) - { - Buffer.BlockCopy(partIn, partInBase, _buffer, bufferLen, partInLen); - } - } - } - } - } - - [SecurityCritical] // auto-generated - private byte[] _EndHash() - { - byte[] pad; - int padLen; - long bitCount; - byte[] hash = new byte[20]; - - /* Compute padding: 80 00 00 ... 00 00 - */ - - padLen = 64 - (int)(_count & 0x3f); - if (padLen <= 8) - padLen += 64; - - pad = new byte[padLen]; - pad[0] = 0x80; - - // Convert count to bit count - bitCount = _count * 8; - - // The convention for RIPEMD is little endian (the same as MD4) - pad[padLen - 1] = (byte)((bitCount >> 56) & 0xff); - pad[padLen - 2] = (byte)((bitCount >> 48) & 0xff); - pad[padLen - 3] = (byte)((bitCount >> 40) & 0xff); - pad[padLen - 4] = (byte)((bitCount >> 32) & 0xff); - pad[padLen - 5] = (byte)((bitCount >> 24) & 0xff); - pad[padLen - 6] = (byte)((bitCount >> 16) & 0xff); - pad[padLen - 7] = (byte)((bitCount >> 8) & 0xff); - pad[padLen - 8] = (byte)((bitCount >> 0) & 0xff); - - /* Digest padding */ - _HashData(pad, 0, pad.Length); - - /* Store digest */ - DWORDToLittleEndian(hash, _stateMD160, 5); - - return hash; - } - - [System.Security.SecurityCritical] // auto-generated - private static unsafe void MDTransform(uint* blockDWords, uint* state, byte* block) - { - uint aa = state[0]; - uint bb = state[1]; - uint cc = state[2]; - uint dd = state[3]; - uint ee = state[4]; - - uint aaa = aa; - uint bbb = bb; - uint ccc = cc; - uint ddd = dd; - uint eee = ee; - - DWORDFromLittleEndian(blockDWords, 16, block); - - /* - As we don't have macros in C# and we don't want to pay the cost of a function call - (which BTW is quite important here as we would have to pass 5 args by ref in - 16 * 10 = 160 function calls) - we'll prefer a less compact code to a less performant code - */ - - // Left Round 1 - // FF(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[0], 11); - aa += blockDWords[0] + F(bb, cc, dd); - aa = (aa << 11 | aa >> (32 - 11)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // FF(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[1], 14); - ee += blockDWords[1] + F(aa, bb, cc); - ee = (ee << 14 | ee >> (32 - 14)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // FF(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[2], 15); - dd += blockDWords[2] + F(ee, aa, bb); - dd = (dd << 15 | dd >> (32 - 15)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // FF(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[3], 12); - cc += blockDWords[3] + F(dd, ee, aa); - cc = (cc << 12 | cc >> (32 - 12)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // FF(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[4], 5); - bb += blockDWords[4] + F(cc, dd, ee); - bb = (bb << 5 | bb >> (32 - 5)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // FF(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[5], 8); - aa += blockDWords[5] + F(bb, cc, dd); - aa = (aa << 8 | aa >> (32 - 8)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // FF(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[6], 7); - ee += blockDWords[6] + F(aa, bb, cc); - ee = (ee << 7 | ee >> (32 - 7)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // FF(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[7], 9); - dd += blockDWords[7] + F(ee, aa, bb); - dd = (dd << 9 | dd >> (32 - 9)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // FF(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[8], 11); - cc += blockDWords[8] + F(dd, ee, aa); - cc = (cc << 11 | cc >> (32 - 11)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // FF(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[9], 13); - bb += blockDWords[9] + F(cc, dd, ee); - bb = (bb << 13 | bb >> (32 - 13)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // FF(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[10], 14); - aa += blockDWords[10] + F(bb, cc, dd); - aa = (aa << 14 | aa >> (32 - 14)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // FF(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[11], 15); - ee += blockDWords[11] + F(aa, bb, cc); - ee = (ee << 15 | ee >> (32 - 15)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // FF(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[12], 6); - dd += blockDWords[12] + F(ee, aa, bb); - dd = (dd << 6 | dd >> (32 - 6)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // FF(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[13], 7); - cc += blockDWords[13] + F(dd, ee, aa); - cc = (cc << 7 | cc >> (32 - 7)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // FF(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[14], 9); - bb += blockDWords[14] + F(cc, dd, ee); - bb = (bb << 9 | bb >> (32 - 9)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // FF(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[15], 8); - aa += blockDWords[15] + F(bb, cc, dd); - aa = (aa << 8 | aa >> (32 - 8)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // Left Round 2 - // GG(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[7], 7); - ee += G(aa, bb, cc) + blockDWords[7] + 0x5a827999; - ee = (ee << 7 | ee >> (32 - 7)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // GG(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[4], 6); - dd += G(ee, aa, bb) + blockDWords[4] + 0x5a827999; - dd = (dd << 6 | dd >> (32 - 6)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // GG(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[13], 8); - cc += G(dd, ee, aa) + blockDWords[13] + 0x5a827999; - cc = (cc << 8 | cc >> (32 - 8)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // GG(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[1], 13); - bb += G(cc, dd, ee) + blockDWords[1] + 0x5a827999; - bb = (bb << 13 | bb >> (32 - 13)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // GG(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[10], 11); - aa += G(bb, cc, dd) + blockDWords[10] + 0x5a827999; - aa = (aa << 11 | aa >> (32 - 11)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // GG(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[6], 9); - ee += G(aa, bb, cc) + blockDWords[6] + 0x5a827999; - ee = (ee << 9 | ee >> (32 - 9)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // GG(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[15], 7); - dd += G(ee, aa, bb) + blockDWords[15] + 0x5a827999; - dd = (dd << 7 | dd >> (32 - 7)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // GG(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[3], 15); - cc += G(dd, ee, aa) + blockDWords[3] + 0x5a827999; - cc = (cc << 15 | cc >> (32 - 15)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // GG(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[12], 7); - bb += G(cc, dd, ee) + blockDWords[12] + 0x5a827999; - bb = (bb << 7 | bb >> (32 - 7)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // GG(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[0], 12); - aa += G(bb, cc, dd) + blockDWords[0] + 0x5a827999; - aa = (aa << 12 | aa >> (32 - 12)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // GG(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[9], 15); - ee += G(aa, bb, cc) + blockDWords[9] + 0x5a827999; - ee = (ee << 15 | ee >> (32 - 15)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // GG(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[5], 9); - dd += G(ee, aa, bb) + blockDWords[5] + 0x5a827999; - dd = (dd << 9 | dd >> (32 - 9)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // GG(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[2], 11); - cc += G(dd, ee, aa) + blockDWords[2] + 0x5a827999; - cc = (cc << 11 | cc >> (32 - 11)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // GG(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[14], 7); - bb += G(cc, dd, ee) + blockDWords[14] + 0x5a827999; - bb = (bb << 7 | bb >> (32 - 7)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // GG(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[11], 13); - aa += G(bb, cc, dd) + blockDWords[11] + 0x5a827999; - aa = (aa << 13 | aa >> (32 - 13)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // GG(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[8], 12); - ee += G(aa, bb, cc) + blockDWords[8] + 0x5a827999; - ee = (ee << 12 | ee >> (32 - 12)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // Left Round 3 - // HH(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[3], 11); - dd += H(ee, aa, bb) + blockDWords[3] + 0x6ed9eba1; - dd = (dd << 11 | dd >> (32 - 11)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // HH(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[10], 13); - cc += H(dd, ee, aa) + blockDWords[10] + 0x6ed9eba1; - cc = (cc << 13 | cc >> (32 - 13)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // HH(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[14], 6); - bb += H(cc, dd, ee) + blockDWords[14] + 0x6ed9eba1; - bb = (bb << 6 | bb >> (32 - 6)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // HH(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[4], 7); - aa += H(bb, cc, dd) + blockDWords[4] + 0x6ed9eba1; - aa = (aa << 7 | aa >> (32 - 7)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // HH(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[9], 14); - ee += H(aa, bb, cc) + blockDWords[9] + 0x6ed9eba1; - ee = (ee << 14 | ee >> (32 - 14)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // HH(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[15], 9); - dd += H(ee, aa, bb) + blockDWords[15] + 0x6ed9eba1; - dd = (dd << 9 | dd >> (32 - 9)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // HH(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[8], 13); - cc += H(dd, ee, aa) + blockDWords[8] + 0x6ed9eba1; - cc = (cc << 13 | cc >> (32 - 13)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // HH(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[1], 15); - bb += H(cc, dd, ee) + blockDWords[1] + 0x6ed9eba1; - bb = (bb << 15 | bb >> (32 - 15)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // HH(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[2], 14); - aa += H(bb, cc, dd) + blockDWords[2] + 0x6ed9eba1; - aa = (aa << 14 | aa >> (32 - 14)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // HH(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[7], 8); - ee += H(aa, bb, cc) + blockDWords[7] + 0x6ed9eba1; - ee = (ee << 8 | ee >> (32 - 8)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // HH(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[0], 13); - dd += H(ee, aa, bb) + blockDWords[0] + 0x6ed9eba1; - dd = (dd << 13 | dd >> (32 - 13)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // HH(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[6], 6); - cc += H(dd, ee, aa) + blockDWords[6] + 0x6ed9eba1; - cc = (cc << 6 | cc >> (32 - 6)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // HH(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[13], 5); - bb += H(cc, dd, ee) + blockDWords[13] + 0x6ed9eba1; - bb = (bb << 5 | bb >> (32 - 5)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // HH(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[11], 12); - aa += H(bb, cc, dd) + blockDWords[11] + 0x6ed9eba1; - aa = (aa << 12 | aa >> (32 - 12)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // HH(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[5], 7); - ee += H(aa, bb, cc) + blockDWords[5] + 0x6ed9eba1; - ee = (ee << 7 | ee >> (32 - 7)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // HH(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[12], 5); - dd += H(ee, aa, bb) + blockDWords[12] + 0x6ed9eba1; - dd = (dd << 5 | dd >> (32 - 5)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // Left Round 4 - // II(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[1], 11); - cc += I(dd, ee, aa) + blockDWords[1] + 0x8f1bbcdc; - cc = (cc << 11 | cc >> (32 - 11)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // II(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[9], 12); - bb += I(cc, dd, ee) + blockDWords[9] + 0x8f1bbcdc; - bb = (bb << 12 | bb >> (32 - 12)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // II(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[11], 14); - aa += I(bb, cc, dd) + blockDWords[11] + 0x8f1bbcdc; - aa = (aa << 14 | aa >> (32 - 14)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // II(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[10], 15); - ee += I(aa, bb, cc) + blockDWords[10] + 0x8f1bbcdc; - ee = (ee << 15 | ee >> (32 - 15)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // II(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[0], 14); - dd += I(ee, aa, bb) + blockDWords[0] + 0x8f1bbcdc; - dd = (dd << 14 | dd >> (32 - 14)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // II(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[8], 15); - cc += I(dd, ee, aa) + blockDWords[8] + 0x8f1bbcdc; - cc = (cc << 15 | cc >> (32 - 15)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // II(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[12], 9); - bb += I(cc, dd, ee) + blockDWords[12] + 0x8f1bbcdc; - bb = (bb << 9 | bb >> (32 - 9)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // II(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[4], 8); - aa += I(bb, cc, dd) + blockDWords[4] + 0x8f1bbcdc; - aa = (aa << 8 | aa >> (32 - 8)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // II(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[13], 9); - ee += I(aa, bb, cc) + blockDWords[13] + 0x8f1bbcdc; - ee = (ee << 9 | ee >> (32 - 9)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // II(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[3], 14); - dd += I(ee, aa, bb) + blockDWords[3] + 0x8f1bbcdc; - dd = (dd << 14 | dd >> (32 - 14)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // II(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[7], 5); - cc += I(dd, ee, aa) + blockDWords[7] + 0x8f1bbcdc; - cc = (cc << 5 | cc >> (32 - 5)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // II(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[15], 6); - bb += I(cc, dd, ee) + blockDWords[15] + 0x8f1bbcdc; - bb = (bb << 6 | bb >> (32 - 6)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // II(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[14], 8); - aa += I(bb, cc, dd) + blockDWords[14] + 0x8f1bbcdc; - aa = (aa << 8 | aa >> (32 - 8)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // II(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[5], 6); - ee += I(aa, bb, cc) + blockDWords[5] + 0x8f1bbcdc; - ee = (ee << 6 | ee >> (32 - 6)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // II(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[6], 5); - dd += I(ee, aa, bb) + blockDWords[6] + 0x8f1bbcdc; - dd = (dd << 5 | dd >> (32 - 5)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // II(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[2], 12); - cc += I(dd, ee, aa) + blockDWords[2] + 0x8f1bbcdc; - cc = (cc << 12 | cc >> (32 - 12)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // Left Round 5 - // JJ(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[4], 9); - bb += J(cc, dd, ee) + blockDWords[4] + 0xa953fd4e; - bb = (bb << 9 | bb >> (32 - 9)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // JJ(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[0], 15); - aa += J(bb, cc, dd) + blockDWords[0] + 0xa953fd4e; - aa = (aa << 15 | aa >> (32 - 15)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // JJ(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[5], 5); - ee += J(aa, bb, cc) + blockDWords[5] + 0xa953fd4e; - ee = (ee << 5 | ee >> (32 - 5)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // JJ(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[9], 11); - dd += J(ee, aa, bb) + blockDWords[9] + 0xa953fd4e; - dd = (dd << 11 | dd >> (32 - 11)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // JJ(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[7], 6); - cc += J(dd, ee, aa) + blockDWords[7] + 0xa953fd4e; - cc = (cc << 6 | cc >> (32 - 6)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // JJ(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[12], 8); - bb += J(cc, dd, ee) + blockDWords[12] + 0xa953fd4e; - bb = (bb << 8 | bb >> (32 - 8)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // JJ(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[2], 13); - aa += J(bb, cc, dd) + blockDWords[2] + 0xa953fd4e; - aa = (aa << 13 | aa >> (32 - 13)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // JJ(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[10], 12); - ee += J(aa, bb, cc) + blockDWords[10] + 0xa953fd4e; - ee = (ee << 12 | ee >> (32 - 12)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // JJ(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[14], 5); - dd += J(ee, aa, bb) + blockDWords[14] + 0xa953fd4e; - dd = (dd << 5 | dd >> (32 - 5)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // JJ(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[1], 12); - cc += J(dd, ee, aa) + blockDWords[1] + 0xa953fd4e; - cc = (cc << 12 | cc >> (32 - 12)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // JJ(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[3], 13); - bb += J(cc, dd, ee) + blockDWords[3] + 0xa953fd4e; - bb = (bb << 13 | bb >> (32 - 13)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // JJ(ref aa, ref bb, ref cc, ref dd, ref ee, blockDWords[8], 14); - aa += J(bb, cc, dd) + blockDWords[8] + 0xa953fd4e; - aa = (aa << 14 | aa >> (32 - 14)) + ee; - cc = (cc << 10 | cc >> (32 - 10)); - - // JJ(ref ee, ref aa, ref bb, ref cc, ref dd, blockDWords[11], 11); - ee += J(aa, bb, cc) + blockDWords[11] + 0xa953fd4e; - ee = (ee << 11 | ee >> (32 - 11)) + dd; - bb = (bb << 10 | bb >> (32 - 10)); - - // JJ(ref dd, ref ee, ref aa, ref bb, ref cc, blockDWords[6], 8); - dd += J(ee, aa, bb) + blockDWords[6] + 0xa953fd4e; - dd = (dd << 8 | dd >> (32 - 8)) + cc; - aa = (aa << 10 | aa >> (32 - 10)); - - // JJ(ref cc, ref dd, ref ee, ref aa, ref bb, blockDWords[15], 5); - cc += J(dd, ee, aa) + blockDWords[15] + 0xa953fd4e; - cc = (cc << 5 | cc >> (32 - 5)) + bb; - ee = (ee << 10 | ee >> (32 - 10)); - - // JJ(ref bb, ref cc, ref dd, ref ee, ref aa, blockDWords[13], 6); - bb += J(cc, dd, ee) + blockDWords[13] + 0xa953fd4e; - bb = (bb << 6 | bb >> (32 - 6)) + aa; - dd = (dd << 10 | dd >> (32 - 10)); - - // Parallel Right Round 1 - // JJJ(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[5], 8); - aaa += J(bbb, ccc, ddd) + blockDWords[5] + 0x50a28be6; - aaa = (aaa << 8 | aaa >> (32 - 8)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // JJJ(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[14], 9); - eee += J(aaa, bbb, ccc) + blockDWords[14] + 0x50a28be6; - eee = (eee << 9 | eee >> (32 - 9)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // JJJ(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[7], 9); - ddd += J(eee, aaa, bbb) + blockDWords[7] + 0x50a28be6; - ddd = (ddd << 9 | ddd >> (32 - 9)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // JJJ(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[0], 11); - ccc += J(ddd, eee, aaa) + blockDWords[0] + 0x50a28be6; - ccc = (ccc << 11 | ccc >> (32 - 11)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // JJJ(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[9], 13); - bbb += J(ccc, ddd, eee) + blockDWords[9] + 0x50a28be6; - bbb = (bbb << 13 | bbb >> (32 - 13)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // JJJ(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[2], 15); - aaa += J(bbb, ccc, ddd) + blockDWords[2] + 0x50a28be6; - aaa = (aaa << 15 | aaa >> (32 - 15)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // JJJ(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[11], 15); - eee += J(aaa, bbb, ccc) + blockDWords[11] + 0x50a28be6; - eee = (eee << 15 | eee >> (32 - 15)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // JJJ(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[4], 5); - ddd += J(eee, aaa, bbb) + blockDWords[4] + 0x50a28be6; - ddd = (ddd << 5 | ddd >> (32 - 5)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // JJJ(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[13], 7); - ccc += J(ddd, eee, aaa) + blockDWords[13] + 0x50a28be6; - ccc = (ccc << 7 | ccc >> (32 - 7)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // JJJ(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[6], 7); - bbb += J(ccc, ddd, eee) + blockDWords[6] + 0x50a28be6; - bbb = (bbb << 7 | bbb >> (32 - 7)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // JJJ(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[15], 8); - aaa += J(bbb, ccc, ddd) + blockDWords[15] + 0x50a28be6; - aaa = (aaa << 8 | aaa >> (32 - 8)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // JJJ(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[8], 11); - eee += J(aaa, bbb, ccc) + blockDWords[8] + 0x50a28be6; - eee = (eee << 11 | eee >> (32 - 11)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // JJJ(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[1], 14); - ddd += J(eee, aaa, bbb) + blockDWords[1] + 0x50a28be6; - ddd = (ddd << 14 | ddd >> (32 - 14)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // JJJ(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[10], 14); - ccc += J(ddd, eee, aaa) + blockDWords[10] + 0x50a28be6; - ccc = (ccc << 14 | ccc >> (32 - 14)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // JJJ(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[3], 12); - bbb += J(ccc, ddd, eee) + blockDWords[3] + 0x50a28be6; - bbb = (bbb << 12 | bbb >> (32 - 12)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // JJJ(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[12], 6); - aaa += J(bbb, ccc, ddd) + blockDWords[12] + 0x50a28be6; - aaa = (aaa << 6 | aaa >> (32 - 6)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // Parallel Right Round 2 - // III(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[6], 9); - eee += I(aaa, bbb, ccc) + blockDWords[6] + 0x5c4dd124; - eee = (eee << 9 | eee >> (32 - 9)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // III(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[11], 13); - ddd += I(eee, aaa, bbb) + blockDWords[11] + 0x5c4dd124; - ddd = (ddd << 13 | ddd >> (32 - 13)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // III(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[3], 15); - ccc += I(ddd, eee, aaa) + blockDWords[3] + 0x5c4dd124; - ccc = (ccc << 15 | ccc >> (32 - 15)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // III(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[7], 7); - bbb += I(ccc, ddd, eee) + blockDWords[7] + 0x5c4dd124; - bbb = (bbb << 7 | bbb >> (32 - 7)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // III(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[0], 12); - aaa += I(bbb, ccc, ddd) + blockDWords[0] + 0x5c4dd124; - aaa = (aaa << 12 | aaa >> (32 - 12)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // III(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[13], 8); - eee += I(aaa, bbb, ccc) + blockDWords[13] + 0x5c4dd124; - eee = (eee << 8 | eee >> (32 - 8)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // III(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[5], 9); - ddd += I(eee, aaa, bbb) + blockDWords[5] + 0x5c4dd124; - ddd = (ddd << 9 | ddd >> (32 - 9)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // III(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[10], 11); - ccc += I(ddd, eee, aaa) + blockDWords[10] + 0x5c4dd124; - ccc = (ccc << 11 | ccc >> (32 - 11)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // III(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[14], 7); - bbb += I(ccc, ddd, eee) + blockDWords[14] + 0x5c4dd124; - bbb = (bbb << 7 | bbb >> (32 - 7)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // III(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[15], 7); - aaa += I(bbb, ccc, ddd) + blockDWords[15] + 0x5c4dd124; - aaa = (aaa << 7 | aaa >> (32 - 7)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // III(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[8], 12); - eee += I(aaa, bbb, ccc) + blockDWords[8] + 0x5c4dd124; - eee = (eee << 12 | eee >> (32 - 12)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // III(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[12], 7); - ddd += I(eee, aaa, bbb) + blockDWords[12] + 0x5c4dd124; - ddd = (ddd << 7 | ddd >> (32 - 7)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // III(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[4], 6); - ccc += I(ddd, eee, aaa) + blockDWords[4] + 0x5c4dd124; - ccc = (ccc << 6 | ccc >> (32 - 6)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // III(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[9], 15); - bbb += I(ccc, ddd, eee) + blockDWords[9] + 0x5c4dd124; - bbb = (bbb << 15 | bbb >> (32 - 15)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // III(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[1], 13); - aaa += I(bbb, ccc, ddd) + blockDWords[1] + 0x5c4dd124; - aaa = (aaa << 13 | aaa >> (32 - 13)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // III(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[2], 11); - eee += I(aaa, bbb, ccc) + blockDWords[2] + 0x5c4dd124; - eee = (eee << 11 | eee >> (32 - 11)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // Parallel Right Round 3 - // HHH(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[15], 9); - ddd += H(eee, aaa, bbb) + blockDWords[15] + 0x6d703ef3; - ddd = (ddd << 9 | ddd >> (32 - 9)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // HHH(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[5], 7); - ccc += H(ddd, eee, aaa) + blockDWords[5] + 0x6d703ef3; - ccc = (ccc << 7 | ccc >> (32 - 7)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // HHH(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[1], 15); - bbb += H(ccc, ddd, eee) + blockDWords[1] + 0x6d703ef3; - bbb = (bbb << 15 | bbb >> (32 - 15)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // HHH(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[3], 11); - aaa += H(bbb, ccc, ddd) + blockDWords[3] + 0x6d703ef3; - aaa = (aaa << 11 | aaa >> (32 - 11)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // HHH(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[7], 8); - eee += H(aaa, bbb, ccc) + blockDWords[7] + 0x6d703ef3; - eee = (eee << 8 | eee >> (32 - 8)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // HHH(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[14], 6); - ddd += H(eee, aaa, bbb) + blockDWords[14] + 0x6d703ef3; - ddd = (ddd << 6 | ddd >> (32 - 6)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // HHH(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[6], 6); - ccc += H(ddd, eee, aaa) + blockDWords[6] + 0x6d703ef3; - ccc = (ccc << 6 | ccc >> (32 - 6)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // HHH(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[9], 14); - bbb += H(ccc, ddd, eee) + blockDWords[9] + 0x6d703ef3; - bbb = (bbb << 14 | bbb >> (32 - 14)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // HHH(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[11], 12); - aaa += H(bbb, ccc, ddd) + blockDWords[11] + 0x6d703ef3; - aaa = (aaa << 12 | aaa >> (32 - 12)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // HHH(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[8], 13); - eee += H(aaa, bbb, ccc) + blockDWords[8] + 0x6d703ef3; - eee = (eee << 13 | eee >> (32 - 13)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // HHH(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[12], 5); - ddd += H(eee, aaa, bbb) + blockDWords[12] + 0x6d703ef3; - ddd = (ddd << 5 | ddd >> (32 - 5)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // HHH(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[2], 14); - ccc += H(ddd, eee, aaa) + blockDWords[2] + 0x6d703ef3; - ccc = (ccc << 14 | ccc >> (32 - 14)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // HHH(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[10], 13); - bbb += H(ccc, ddd, eee) + blockDWords[10] + 0x6d703ef3; - bbb = (bbb << 13 | bbb >> (32 - 13)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // HHH(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[0], 13); - aaa += H(bbb, ccc, ddd) + blockDWords[0] + 0x6d703ef3; - aaa = (aaa << 13 | aaa >> (32 - 13)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // HHH(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[4], 7); - eee += H(aaa, bbb, ccc) + blockDWords[4] + 0x6d703ef3; - eee = (eee << 7 | eee >> (32 - 7)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // HHH(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[13], 5); - ddd += H(eee, aaa, bbb) + blockDWords[13] + 0x6d703ef3; - ddd = (ddd << 5 | ddd >> (32 - 5)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // Parallel Right Round 4 - // GGG(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[8], 15); - ccc += G(ddd, eee, aaa) + blockDWords[8] + 0x7a6d76e9; - ccc = (ccc << 15 | ccc >> (32 - 15)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // GGG(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[6], 5); - bbb += G(ccc, ddd, eee) + blockDWords[6] + 0x7a6d76e9; - bbb = (bbb << 5 | bbb >> (32 - 5)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // GGG(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[4], 8); - aaa += G(bbb, ccc, ddd) + blockDWords[4] + 0x7a6d76e9; - aaa = (aaa << 8 | aaa >> (32 - 8)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // GGG(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[1], 11); - eee += G(aaa, bbb, ccc) + blockDWords[1] + 0x7a6d76e9; - eee = (eee << 11 | eee >> (32 - 11)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // GGG(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[3], 14); - ddd += G(eee, aaa, bbb) + blockDWords[3] + 0x7a6d76e9; - ddd = (ddd << 14 | ddd >> (32 - 14)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // GGG(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[11], 14); - ccc += G(ddd, eee, aaa) + blockDWords[11] + 0x7a6d76e9; - ccc = (ccc << 14 | ccc >> (32 - 14)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // GGG(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[15], 6); - bbb += G(ccc, ddd, eee) + blockDWords[15] + 0x7a6d76e9; - bbb = (bbb << 6 | bbb >> (32 - 6)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // GGG(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[0], 14); - aaa += G(bbb, ccc, ddd) + blockDWords[0] + 0x7a6d76e9; - aaa = (aaa << 14 | aaa >> (32 - 14)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // GGG(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[5], 6); - eee += G(aaa, bbb, ccc) + blockDWords[5] + 0x7a6d76e9; - eee = (eee << 6 | eee >> (32 - 6)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // GGG(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[12], 9); - ddd += G(eee, aaa, bbb) + blockDWords[12] + 0x7a6d76e9; - ddd = (ddd << 9 | ddd >> (32 - 9)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // GGG(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[2], 12); - ccc += G(ddd, eee, aaa) + blockDWords[2] + 0x7a6d76e9; - ccc = (ccc << 12 | ccc >> (32 - 12)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // GGG(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[13], 9); - bbb += G(ccc, ddd, eee) + blockDWords[13] + 0x7a6d76e9; - bbb = (bbb << 9 | bbb >> (32 - 9)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // GGG(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[9], 12); - aaa += G(bbb, ccc, ddd) + blockDWords[9] + 0x7a6d76e9; - aaa = (aaa << 12 | aaa >> (32 - 12)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // GGG(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[7], 5); - eee += G(aaa, bbb, ccc) + blockDWords[7] + 0x7a6d76e9; - eee = (eee << 5 | eee >> (32 - 5)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // GGG(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[10], 15); - ddd += G(eee, aaa, bbb) + blockDWords[10] + 0x7a6d76e9; - ddd = (ddd << 15 | ddd >> (32 - 15)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // GGG(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[14], 8); - ccc += G(ddd, eee, aaa) + blockDWords[14] + 0x7a6d76e9; - ccc = (ccc << 8 | ccc >> (32 - 8)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // Parallel Right Round 5 - // FFF(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[12], 8); - bbb += F(ccc, ddd, eee) + blockDWords[12]; - bbb = (bbb << 8 | bbb >> (32 - 8)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // FFF(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[15], 5); - aaa += F(bbb, ccc, ddd) + blockDWords[15]; - aaa = (aaa << 5 | aaa >> (32 - 5)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // FFF(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[10], 12); - eee += F(aaa, bbb, ccc) + blockDWords[10]; - eee = (eee << 12 | eee >> (32 - 12)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // FFF(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[4], 9); - ddd += F(eee, aaa, bbb) + blockDWords[4]; - ddd = (ddd << 9 | ddd >> (32 - 9)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // FFF(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[1], 12); - ccc += F(ddd, eee, aaa) + blockDWords[1]; - ccc = (ccc << 12 | ccc >> (32 - 12)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // FFF(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[5], 5); - bbb += F(ccc, ddd, eee) + blockDWords[5]; - bbb = (bbb << 5 | bbb >> (32 - 5)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // FFF(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[8], 14); - aaa += F(bbb, ccc, ddd) + blockDWords[8]; - aaa = (aaa << 14 | aaa >> (32 - 14)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // FFF(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[7], 6); - eee += F(aaa, bbb, ccc) + blockDWords[7]; - eee = (eee << 6 | eee >> (32 - 6)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // FFF(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[6], 8); - ddd += F(eee, aaa, bbb) + blockDWords[6]; - ddd = (ddd << 8 | ddd >> (32 - 8)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // FFF(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[2], 13); - ccc += F(ddd, eee, aaa) + blockDWords[2]; - ccc = (ccc << 13 | ccc >> (32 - 13)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // FFF(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[13], 6); - bbb += F(ccc, ddd, eee) + blockDWords[13]; - bbb = (bbb << 6 | bbb >> (32 - 6)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // FFF(ref aaa, ref bbb, ref ccc, ref ddd, ref eee, blockDWords[14], 5); - aaa += F(bbb, ccc, ddd) + blockDWords[14]; - aaa = (aaa << 5 | aaa >> (32 - 5)) + eee; - ccc = (ccc << 10 | ccc >> (32 - 10)); - - // FFF(ref eee, ref aaa, ref bbb, ref ccc, ref ddd, blockDWords[0], 15); - eee += F(aaa, bbb, ccc) + blockDWords[0]; - eee = (eee << 15 | eee >> (32 - 15)) + ddd; - bbb = (bbb << 10 | bbb >> (32 - 10)); - - // FFF(ref ddd, ref eee, ref aaa, ref bbb, ref ccc, blockDWords[3], 13); - ddd += F(eee, aaa, bbb) + blockDWords[3]; - ddd = (ddd << 13 | ddd >> (32 - 13)) + ccc; - aaa = (aaa << 10 | aaa >> (32 - 10)); - - // FFF(ref ccc, ref ddd, ref eee, ref aaa, ref bbb, blockDWords[9], 11); - ccc += F(ddd, eee, aaa) + blockDWords[9]; - ccc = (ccc << 11 | ccc >> (32 - 11)) + bbb; - eee = (eee << 10 | eee >> (32 - 10)); - - // FFF(ref bbb, ref ccc, ref ddd, ref eee, ref aaa, blockDWords[11], 11); - bbb += F(ccc, ddd, eee) + blockDWords[11]; - bbb = (bbb << 11 | bbb >> (32 - 11)) + aaa; - ddd = (ddd << 10 | ddd >> (32 - 10)); - - // Update the state of the hash object - ddd += cc + state[1]; - state[1] = state[2] + dd + eee; - state[2] = state[3] + ee + aaa; - state[3] = state[4] + aa + bbb; - state[4] = state[0] + bb + ccc; - state[0] = ddd; - } - - // The five basic functions - private static uint F(uint x, uint y, uint z) - { - return (x ^ y ^ z); - } - - private static uint G(uint x, uint y, uint z) - { - return ((x & y) | (~x & z)); - } - - private static uint H(uint x, uint y, uint z) - { - return ((x | ~y) ^ z); - } - - private static uint I(uint x, uint y, uint z) - { - return ((x & z) | (y & ~z)); - } - - private static uint J(uint x, uint y, uint z) - { - return (x ^ (y | ~z)); - } - - [SecurityCritical] // auto-generated - private unsafe static void DWORDFromLittleEndian(uint* x, int digits, byte* block) - { - int i; - int j; - - for (i = 0, j = 0; i < digits; i++, j += 4) - x[i] = (uint)(block[j] | (block[j + 1] << 8) | (block[j + 2] << 16) | (block[j + 3] << 24)); - } - - private static void DWORDToLittleEndian(byte[] block, uint[] x, int digits) - { - int i; - int j; - - for (i = 0, j = 0; i < digits; i++, j += 4) - { - block[j] = (byte)(x[i] & 0xff); - block[j + 1] = (byte)((x[i] >> 8) & 0xff); - block[j + 2] = (byte)((x[i] >> 16) & 0xff); - block[j + 3] = (byte)((x[i] >> 24) & 0xff); - } - } - } -} -#endif diff --git a/neo/Cryptography/SCrypt.cs b/neo/Cryptography/SCrypt.cs deleted file mode 100644 index 3022af296c..0000000000 --- a/neo/Cryptography/SCrypt.cs +++ /dev/null @@ -1,330 +0,0 @@ -using System; -using System.Security.Cryptography; - -namespace Neo.Cryptography -{ - public static class SCrypt - { - private unsafe static void BulkCopy(void* dst, void* src, int len) - { - var d = (byte*)dst; - var s = (byte*)src; - - while (len >= 8) - { - *(ulong*)d = *(ulong*)s; - d += 8; - s += 8; - len -= 8; - } - if (len >= 4) - { - *(uint*)d = *(uint*)s; - d += 4; - - s += 4; - len -= 4; - } - if (len >= 2) - { - *(ushort*)d = *(ushort*)s; - d += 2; - s += 2; - len -= 2; - } - if (len >= 1) - { - *d = *s; - } - } - - private unsafe static void BulkXor(void* dst, void* src, int len) - { - var d = (byte*)dst; - var s = (byte*)src; - - while (len >= 8) - { - *(ulong*)d ^= *(ulong*)s; - d += 8; - s += 8; - len -= 8; - } - if (len >= 4) - { - *(uint*)d ^= *(uint*)s; - d += 4; - s += 4; - len -= 4; - } - if (len >= 2) - { - *(ushort*)d ^= *(ushort*)s; - d += 2; - s += 2; - len -= 2; - } - if (len >= 1) - { - *d ^= *s; - } - } - - private unsafe static void Encode32(byte* p, uint x) - { - p[0] = (byte)(x & 0xff); - p[1] = (byte)((x >> 8) & 0xff); - p[2] = (byte)((x >> 16) & 0xff); - p[3] = (byte)((x >> 24) & 0xff); - } - - private unsafe static uint Decode32(byte* p) - { - return - ((uint)(p[0]) + - ((uint)(p[1]) << 8) + - ((uint)(p[2]) << 16) + - ((uint)(p[3]) << 24)); - } - - private unsafe static void Salsa208(uint* B) - { - uint x0 = B[0]; - uint x1 = B[1]; - uint x2 = B[2]; - uint x3 = B[3]; - uint x4 = B[4]; - uint x5 = B[5]; - uint x6 = B[6]; - uint x7 = B[7]; - uint x8 = B[8]; - uint x9 = B[9]; - uint x10 = B[10]; - uint x11 = B[11]; - uint x12 = B[12]; - uint x13 = B[13]; - uint x14 = B[14]; - uint x15 = B[15]; - - for (var i = 0; i < 8; i += 2) - { - //((x0 + x12) << 7) | ((x0 + x12) >> (32 - 7)); - /* Operate on columns. */ - x4 ^= R(x0 + x12, 7); x8 ^= R(x4 + x0, 9); - x12 ^= R(x8 + x4, 13); x0 ^= R(x12 + x8, 18); - - x9 ^= R(x5 + x1, 7); x13 ^= R(x9 + x5, 9); - x1 ^= R(x13 + x9, 13); x5 ^= R(x1 + x13, 18); - - x14 ^= R(x10 + x6, 7); x2 ^= R(x14 + x10, 9); - x6 ^= R(x2 + x14, 13); x10 ^= R(x6 + x2, 18); - - x3 ^= R(x15 + x11, 7); x7 ^= R(x3 + x15, 9); - x11 ^= R(x7 + x3, 13); x15 ^= R(x11 + x7, 18); - - /* Operate on rows. */ - x1 ^= R(x0 + x3, 7); x2 ^= R(x1 + x0, 9); - x3 ^= R(x2 + x1, 13); x0 ^= R(x3 + x2, 18); - - x6 ^= R(x5 + x4, 7); x7 ^= R(x6 + x5, 9); - x4 ^= R(x7 + x6, 13); x5 ^= R(x4 + x7, 18); - - x11 ^= R(x10 + x9, 7); x8 ^= R(x11 + x10, 9); - x9 ^= R(x8 + x11, 13); x10 ^= R(x9 + x8, 18); - - x12 ^= R(x15 + x14, 7); x13 ^= R(x12 + x15, 9); - x14 ^= R(x13 + x12, 13); x15 ^= R(x14 + x13, 18); - } - - B[0] += x0; - B[1] += x1; - B[2] += x2; - B[3] += x3; - B[4] += x4; - B[5] += x5; - B[6] += x6; - B[7] += x7; - B[8] += x8; - B[9] += x9; - B[10] += x10; - B[11] += x11; - B[12] += x12; - B[13] += x13; - B[14] += x14; - B[15] += x15; - } - - private unsafe static uint R(uint a, int b) - { - return (a << b) | (a >> (32 - b)); - } - - private unsafe static void BlockMix(uint* Bin, uint* Bout, uint* X, int r) - { - /* 1: X <-- B_{2r - 1} */ - BulkCopy(X, &Bin[(2 * r - 1) * 16], 64); - - /* 2: for i = 0 to 2r - 1 do */ - for (var i = 0; i < 2 * r; i += 2) - { - /* 3: X <-- H(X \xor B_i) */ - BulkXor(X, &Bin[i * 16], 64); - Salsa208(X); - - /* 4: Y_i <-- X */ - /* 6: B' <-- (Y_0, Y_2 ... Y_{2r-2}, Y_1, Y_3 ... Y_{2r-1}) */ - BulkCopy(&Bout[i * 8], X, 64); - - /* 3: X <-- H(X \xor B_i) */ - BulkXor(X, &Bin[i * 16 + 16], 64); - Salsa208(X); - - /* 4: Y_i <-- X */ - /* 6: B' <-- (Y_0, Y_2 ... Y_{2r-2}, Y_1, Y_3 ... Y_{2r-1}) */ - BulkCopy(&Bout[i * 8 + r * 16], X, 64); - } - } - - private unsafe static long Integerify(uint* B, int r) - { - var X = (uint*)(((byte*)B) + (2 * r - 1) * 64); - - return (((long)(X[1]) << 32) + X[0]); - } - - private unsafe static void SMix(byte* B, int r, int N, uint* V, uint* XY) - { - var X = XY; - var Y = &XY[32 * r]; - var Z = &XY[64 * r]; - - /* 1: X <-- B */ - for (var k = 0; k < 32 * r; k++) - { - X[k] = Decode32(&B[4 * k]); - } - - /* 2: for i = 0 to N - 1 do */ - for (var i = 0L; i < N; i += 2) - { - /* 3: V_i <-- X */ - BulkCopy(&V[i * (32 * r)], X, 128 * r); - - /* 4: X <-- H(X) */ - BlockMix(X, Y, Z, r); - - /* 3: V_i <-- X */ - BulkCopy(&V[(i + 1) * (32 * r)], Y, 128 * r); - - /* 4: X <-- H(X) */ - BlockMix(Y, X, Z, r); - } - - /* 6: for i = 0 to N - 1 do */ - for (var i = 0; i < N; i += 2) - { - /* 7: j <-- Integerify(X) mod N */ - var j = Integerify(X, r) & (N - 1); - - /* 8: X <-- H(X \xor V_j) */ - BulkXor(X, &V[j * (32 * r)], 128 * r); - BlockMix(X, Y, Z, r); - - /* 7: j <-- Integerify(X) mod N */ - j = Integerify(Y, r) & (N - 1); - - /* 8: X <-- H(X \xor V_j) */ - BulkXor(Y, &V[j * (32 * r)], 128 * r); - BlockMix(Y, X, Z, r); - } - - /* 10: B' <-- X */ - for (var k = 0; k < 32 * r; k++) - { - Encode32(&B[4 * k], X[k]); - } - } - -#if NET47 - public static byte[] DeriveKey(byte[] password, byte[] salt, int N, int r, int p, int derivedKeyLength) - { - return Replicon.Cryptography.SCrypt.SCrypt.DeriveKey(password, salt, (ulong)N, (uint)r, (uint)p, (uint)derivedKeyLength); - } -#else - public unsafe static byte[] DeriveKey(byte[] password, byte[] salt, int N, int r, int p, int derivedKeyLength) - { - var Ba = new byte[128 * r * p + 63]; - var XYa = new byte[256 * r + 63]; - var Va = new byte[128 * r * N + 63]; - var buf = new byte[derivedKeyLength]; - - var mac = new HMACSHA256(password); - - /* 1: (B_0 ... B_{p-1}) <-- PBKDF2(P, S, 1, p * MFLen) */ - PBKDF2_SHA256(mac, password, salt, salt.Length, 1, Ba, p * 128 * r); - - fixed (byte* B = Ba) - fixed (void* V = Va) - fixed (void* XY = XYa) - { - /* 2: for i = 0 to p - 1 do */ - for (var i = 0; i < p; i++) - { - /* 3: B_i <-- MF(B_i, N) */ - SMix(&B[i * 128 * r], r, N, (uint*)V, (uint*)XY); - } - } - - /* 5: DK <-- PBKDF2(P, B, 1, dkLen) */ - PBKDF2_SHA256(mac, password, Ba, p * 128 * r, 1, buf, buf.Length); - - return buf; - } -#endif - - private static void PBKDF2_SHA256(HMACSHA256 mac, byte[] password, byte[] salt, int saltLength, long iterationCount, byte[] derivedKey, int derivedKeyLength) - { - if (derivedKeyLength > (Math.Pow(2, 32) - 1) * 32) - { - throw new ArgumentException("Requested key length too long"); - } - - var U = new byte[32]; - var T = new byte[32]; - var saltBuffer = new byte[saltLength + 4]; - - var blockCount = (int)Math.Ceiling((double)derivedKeyLength / 32); - var r = derivedKeyLength - (blockCount - 1) * 32; - - Buffer.BlockCopy(salt, 0, saltBuffer, 0, saltLength); - - using (var incrementalHasher = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA256, mac.Key)) - { - for (int i = 1; i <= blockCount; i++) - { - saltBuffer[saltLength + 0] = (byte)(i >> 24); - saltBuffer[saltLength + 1] = (byte)(i >> 16); - saltBuffer[saltLength + 2] = (byte)(i >> 8); - saltBuffer[saltLength + 3] = (byte)(i); - - mac.Initialize(); - incrementalHasher.AppendData(saltBuffer, 0, saltBuffer.Length); - Buffer.BlockCopy(incrementalHasher.GetHashAndReset(), 0, U, 0, U.Length); - Buffer.BlockCopy(U, 0, T, 0, 32); - - for (long j = 1; j < iterationCount; j++) - { - incrementalHasher.AppendData(U, 0, U.Length); - Buffer.BlockCopy(incrementalHasher.GetHashAndReset(), 0, U, 0, U.Length); - for (int k = 0; k < 32; k++) - { - T[k] ^= U[k]; - } - } - - Buffer.BlockCopy(T, 0, derivedKey, (i - 1) * 32, (i == blockCount ? r : 32)); - } - } - } - } -} diff --git a/neo/Fixed8.cs b/neo/Fixed8.cs deleted file mode 100644 index 5c41616fa9..0000000000 --- a/neo/Fixed8.cs +++ /dev/null @@ -1,261 +0,0 @@ -using Neo.IO; -using System; -using System.Globalization; -using System.IO; - -namespace Neo -{ - /// - /// 精确到10^-8的64位定点数,将舍入误差降到最低。 - /// 通过控制乘数的精度,可以完全消除舍入误差。 - /// - public struct Fixed8 : IComparable, IEquatable, IFormattable, ISerializable - { - private const long D = 100000000; - internal long value; - - public static readonly Fixed8 MaxValue = new Fixed8 { value = long.MaxValue }; - - public static readonly Fixed8 MinValue = new Fixed8 { value = long.MinValue }; - - public static readonly Fixed8 One = new Fixed8 { value = D }; - - public static readonly Fixed8 Satoshi = new Fixed8 { value = 1 }; - - public static readonly Fixed8 Zero = default(Fixed8); - - public int Size => sizeof(long); - - public Fixed8(long data) - { - this.value = data; - } - - public Fixed8 Abs() - { - if (value >= 0) return this; - return new Fixed8 - { - value = -value - }; - } - - public Fixed8 Ceiling() - { - long remainder = value % D; - if (remainder == 0) return this; - if (remainder > 0) - return new Fixed8 - { - value = value - remainder + D - }; - else - return new Fixed8 - { - value = value - remainder - }; - } - - public int CompareTo(Fixed8 other) - { - return value.CompareTo(other.value); - } - - void ISerializable.Deserialize(BinaryReader reader) - { - value = reader.ReadInt64(); - } - - public bool Equals(Fixed8 other) - { - return value.Equals(other.value); - } - - public override bool Equals(object obj) - { - if (!(obj is Fixed8)) return false; - return Equals((Fixed8)obj); - } - - public static Fixed8 FromDecimal(decimal value) - { - value *= D; - if (value < long.MinValue || value > long.MaxValue) - throw new OverflowException(); - return new Fixed8 - { - value = (long)value - }; - } - - public long GetData() => value; - - public override int GetHashCode() - { - return value.GetHashCode(); - } - - public static Fixed8 Max(Fixed8 first, params Fixed8[] others) - { - foreach (Fixed8 other in others) - { - if (first.CompareTo(other) < 0) - first = other; - } - return first; - } - - public static Fixed8 Min(Fixed8 first, params Fixed8[] others) - { - foreach (Fixed8 other in others) - { - if (first.CompareTo(other) > 0) - first = other; - } - return first; - } - - public static Fixed8 Parse(string s) - { - return FromDecimal(decimal.Parse(s, NumberStyles.Float, CultureInfo.InvariantCulture)); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(value); - } - - public override string ToString() - { - return ((decimal)this).ToString(CultureInfo.InvariantCulture); - } - - public string ToString(string format) - { - return ((decimal)this).ToString(format); - } - - public string ToString(string format, IFormatProvider formatProvider) - { - return ((decimal)this).ToString(format, formatProvider); - } - - public static bool TryParse(string s, out Fixed8 result) - { - decimal d; - if (!decimal.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out d)) - { - result = default(Fixed8); - return false; - } - d *= D; - if (d < long.MinValue || d > long.MaxValue) - { - result = default(Fixed8); - return false; - } - result = new Fixed8 - { - value = (long)d - }; - return true; - } - - public static explicit operator decimal(Fixed8 value) - { - return value.value / (decimal)D; - } - - public static explicit operator long(Fixed8 value) - { - return value.value / D; - } - - public static bool operator ==(Fixed8 x, Fixed8 y) - { - return x.Equals(y); - } - - public static bool operator !=(Fixed8 x, Fixed8 y) - { - return !x.Equals(y); - } - - public static bool operator >(Fixed8 x, Fixed8 y) - { - return x.CompareTo(y) > 0; - } - - public static bool operator <(Fixed8 x, Fixed8 y) - { - return x.CompareTo(y) < 0; - } - - public static bool operator >=(Fixed8 x, Fixed8 y) - { - return x.CompareTo(y) >= 0; - } - - public static bool operator <=(Fixed8 x, Fixed8 y) - { - return x.CompareTo(y) <= 0; - } - - public static Fixed8 operator *(Fixed8 x, Fixed8 y) - { - const ulong QUO = (1ul << 63) / (D >> 1); - const ulong REM = (1ul << 63) % (D >> 1); - int sign = Math.Sign(x.value) * Math.Sign(y.value); - ulong ux = (ulong)Math.Abs(x.value); - ulong uy = (ulong)Math.Abs(y.value); - ulong xh = ux >> 32; - ulong xl = ux & 0x00000000fffffffful; - ulong yh = uy >> 32; - ulong yl = uy & 0x00000000fffffffful; - ulong rh = xh * yh; - ulong rm = xh * yl + xl * yh; - ulong rl = xl * yl; - ulong rmh = rm >> 32; - ulong rml = rm << 32; - rh += rmh; - rl += rml; - if (rl < rml) - ++rh; - if (rh >= D) - throw new OverflowException(); - ulong r = rh * QUO + (rh * REM + rl) / D; - x.value = (long)r * sign; - return x; - } - - public static Fixed8 operator *(Fixed8 x, long y) - { - x.value *= y; - return x; - } - - public static Fixed8 operator /(Fixed8 x, long y) - { - x.value /= y; - return x; - } - - public static Fixed8 operator +(Fixed8 x, Fixed8 y) - { - x.value = checked(x.value + y.value); - return x; - } - - public static Fixed8 operator -(Fixed8 x, Fixed8 y) - { - x.value = checked(x.value - y.value); - return x; - } - - public static Fixed8 operator -(Fixed8 value) - { - value.value = -value.value; - return value; - } - } -} diff --git a/neo/Helper.cs b/neo/Helper.cs deleted file mode 100644 index 27dd2c2261..0000000000 --- a/neo/Helper.cs +++ /dev/null @@ -1,263 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using System.Text; - -namespace Neo -{ - public static class Helper - { - private static readonly DateTime unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - private static int BitLen(int w) - { - return (w < 1 << 15 ? (w < 1 << 7 - ? (w < 1 << 3 ? (w < 1 << 1 - ? (w < 1 << 0 ? (w < 0 ? 32 : 0) : 1) - : (w < 1 << 2 ? 2 : 3)) : (w < 1 << 5 - ? (w < 1 << 4 ? 4 : 5) - : (w < 1 << 6 ? 6 : 7))) - : (w < 1 << 11 - ? (w < 1 << 9 ? (w < 1 << 8 ? 8 : 9) : (w < 1 << 10 ? 10 : 11)) - : (w < 1 << 13 ? (w < 1 << 12 ? 12 : 13) : (w < 1 << 14 ? 14 : 15)))) : (w < 1 << 23 ? (w < 1 << 19 - ? (w < 1 << 17 ? (w < 1 << 16 ? 16 : 17) : (w < 1 << 18 ? 18 : 19)) - : (w < 1 << 21 ? (w < 1 << 20 ? 20 : 21) : (w < 1 << 22 ? 22 : 23))) : (w < 1 << 27 - ? (w < 1 << 25 ? (w < 1 << 24 ? 24 : 25) : (w < 1 << 26 ? 26 : 27)) - : (w < 1 << 29 ? (w < 1 << 28 ? 28 : 29) : (w < 1 << 30 ? 30 : 31))))); - } - - internal static int GetBitLength(this BigInteger i) - { - byte[] b = i.ToByteArray(); - return (b.Length - 1) * 8 + BitLen(i.Sign > 0 ? b[b.Length - 1] : 255 - b[b.Length - 1]); - } - - internal static int GetLowestSetBit(this BigInteger i) - { - if (i.Sign == 0) - return -1; - byte[] b = i.ToByteArray(); - int w = 0; - while (b[w] == 0) - w++; - for (int x = 0; x < 8; x++) - if ((b[w] & 1 << x) > 0) - return x + w * 8; - throw new Exception(); - } - - public static byte[] HexToBytes(this string value) - { - if (value == null || value.Length == 0) - return new byte[0]; - if (value.Length % 2 == 1) - throw new FormatException(); - byte[] result = new byte[value.Length / 2]; - for (int i = 0; i < result.Length; i++) - result[i] = byte.Parse(value.Substring(i * 2, 2), NumberStyles.AllowHexSpecifier); - return result; - } - - internal static BigInteger Mod(this BigInteger x, BigInteger y) - { - x %= y; - if (x.Sign < 0) - x += y; - return x; - } - - internal static BigInteger ModInverse(this BigInteger a, BigInteger n) - { - BigInteger i = n, v = 0, d = 1; - while (a > 0) - { - BigInteger t = i / a, x = a; - a = i % x; - i = x; - x = d; - d = v - t * x; - v = x; - } - v %= n; - if (v < 0) v = (v + n) % n; - return v; - } - - internal static BigInteger NextBigInteger(this Random rand, int sizeInBits) - { - if (sizeInBits < 0) - throw new ArgumentException("sizeInBits must be non-negative"); - if (sizeInBits == 0) - return 0; - byte[] b = new byte[sizeInBits / 8 + 1]; - rand.NextBytes(b); - if (sizeInBits % 8 == 0) - b[b.Length - 1] = 0; - else - b[b.Length - 1] &= (byte)((1 << sizeInBits % 8) - 1); - return new BigInteger(b); - } - - internal static BigInteger NextBigInteger(this RandomNumberGenerator rng, int sizeInBits) - { - if (sizeInBits < 0) - throw new ArgumentException("sizeInBits must be non-negative"); - if (sizeInBits == 0) - return 0; - byte[] b = new byte[sizeInBits / 8 + 1]; - rng.GetBytes(b); - if (sizeInBits % 8 == 0) - b[b.Length - 1] = 0; - else - b[b.Length - 1] &= (byte)((1 << sizeInBits % 8) - 1); - return new BigInteger(b); - } - - public static Fixed8 Sum(this IEnumerable source) - { - long sum = 0; - checked - { - foreach (Fixed8 item in source) - { - sum += item.value; - } - } - return new Fixed8(sum); - } - - public static Fixed8 Sum(this IEnumerable source, Func selector) - { - return source.Select(selector).Sum(); - } - - internal static bool TestBit(this BigInteger i, int index) - { - return (i & (BigInteger.One << index)) > BigInteger.Zero; - } - - public static DateTime ToDateTime(this uint timestamp) - { - return unixEpoch.AddSeconds(timestamp).ToLocalTime(); - } - - public static DateTime ToDateTime(this ulong timestamp) - { - return unixEpoch.AddSeconds(timestamp).ToLocalTime(); - } - - public static string ToHexString(this IEnumerable value) - { - StringBuilder sb = new StringBuilder(); - foreach (byte b in value) - sb.AppendFormat("{0:x2}", b); - return sb.ToString(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - unsafe internal static int ToInt32(this byte[] value, int startIndex) - { - fixed (byte* pbyte = &value[startIndex]) - { - return *((int*)pbyte); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - unsafe internal static long ToInt64(this byte[] value, int startIndex) - { - fixed (byte* pbyte = &value[startIndex]) - { - return *((long*)pbyte); - } - } - - public static uint ToTimestamp(this DateTime time) - { - return (uint)(time.ToUniversalTime() - unixEpoch).TotalSeconds; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - unsafe internal static ushort ToUInt16(this byte[] value, int startIndex) - { - fixed (byte* pbyte = &value[startIndex]) - { - return *((ushort*)pbyte); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - unsafe internal static uint ToUInt32(this byte[] value, int startIndex) - { - fixed (byte* pbyte = &value[startIndex]) - { - return *((uint*)pbyte); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - unsafe internal static ulong ToUInt64(this byte[] value, int startIndex) - { - fixed (byte* pbyte = &value[startIndex]) - { - return *((ulong*)pbyte); - } - } - - internal static long WeightedAverage(this IEnumerable source, Func valueSelector, Func weightSelector) - { - long sum_weight = 0; - long sum_value = 0; - foreach (T item in source) - { - long weight = weightSelector(item); - sum_weight += weight; - sum_value += valueSelector(item) * weight; - } - if (sum_value == 0) return 0; - return sum_value / sum_weight; - } - - internal static IEnumerable WeightedFilter(this IList source, double start, double end, Func weightSelector, Func resultSelector) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (start < 0 || start > 1) throw new ArgumentOutOfRangeException(nameof(start)); - if (end < start || start + end > 1) throw new ArgumentOutOfRangeException(nameof(end)); - if (weightSelector == null) throw new ArgumentNullException(nameof(weightSelector)); - if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); - if (source.Count == 0 || start == end) yield break; - double amount = source.Sum(weightSelector); - long sum = 0; - double current = 0; - foreach (T item in source) - { - if (current >= end) break; - long weight = weightSelector(item); - sum += weight; - double old = current; - current = sum / amount; - if (current <= start) continue; - if (old < start) - { - if (current > end) - { - weight = (long)((end - start) * amount); - } - else - { - weight = (long)((current - start) * amount); - } - } - else if (current > end) - { - weight = (long)((end - old) * amount); - } - yield return resultSelector(item, weight); - } - } - } -} diff --git a/neo/IO/Caching/Cache.cs b/neo/IO/Caching/Cache.cs deleted file mode 100644 index 3970f2c389..0000000000 --- a/neo/IO/Caching/Cache.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Neo.IO.Caching -{ - internal abstract class Cache : ICollection, IDisposable - { - protected class CacheItem - { - public TKey Key; - public TValue Value; - public DateTime Time; - - public CacheItem(TKey key, TValue value) - { - this.Key = key; - this.Value = value; - this.Time = DateTime.Now; - } - } - - public readonly object SyncRoot = new object(); - protected readonly Dictionary InnerDictionary = new Dictionary(); - private readonly int max_capacity; - - public TValue this[TKey key] - { - get - { - lock (SyncRoot) - { - if (!InnerDictionary.TryGetValue(key, out CacheItem item)) throw new KeyNotFoundException(); - OnAccess(item); - return item.Value; - } - } - } - - public int Count - { - get - { - lock (SyncRoot) - { - return InnerDictionary.Count; - } - } - } - - public bool IsReadOnly - { - get - { - return false; - } - } - - public Cache(int max_capacity) - { - this.max_capacity = max_capacity; - } - - public void Add(TValue item) - { - TKey key = GetKeyForItem(item); - lock (SyncRoot) - { - AddInternal(key, item); - } - } - - private void AddInternal(TKey key, TValue item) - { - if (InnerDictionary.TryGetValue(key, out CacheItem cacheItem)) - { - OnAccess(cacheItem); - } - else - { - if (InnerDictionary.Count >= max_capacity) - { - //TODO: 对PLINQ查询进行性能测试,以便确定此处使用何种算法更优(并行或串行) - foreach (CacheItem item_del in InnerDictionary.Values.AsParallel().OrderBy(p => p.Time).Take(InnerDictionary.Count - max_capacity + 1)) - { - RemoveInternal(item_del); - } - } - InnerDictionary.Add(key, new CacheItem(key, item)); - } - } - - public void AddRange(IEnumerable items) - { - lock (SyncRoot) - { - foreach (TValue item in items) - { - TKey key = GetKeyForItem(item); - AddInternal(key, item); - } - } - } - - public void Clear() - { - lock (SyncRoot) - { - foreach (CacheItem item_del in InnerDictionary.Values.ToArray()) - { - RemoveInternal(item_del); - } - } - } - - public bool Contains(TKey key) - { - lock (SyncRoot) - { - if (!InnerDictionary.TryGetValue(key, out CacheItem cacheItem)) return false; - OnAccess(cacheItem); - return true; - } - } - - public bool Contains(TValue item) - { - return Contains(GetKeyForItem(item)); - } - - public void CopyTo(TValue[] array, int arrayIndex) - { - if (array == null) throw new ArgumentNullException(); - if (arrayIndex < 0) throw new ArgumentOutOfRangeException(); - if (arrayIndex + InnerDictionary.Count > array.Length) throw new ArgumentException(); - foreach (TValue item in this) - { - array[arrayIndex++] = item; - } - } - - public void Dispose() - { - Clear(); - } - - public IEnumerator GetEnumerator() - { - lock (SyncRoot) - { - foreach (TValue item in InnerDictionary.Values.Select(p => p.Value)) - { - yield return item; - } - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - protected abstract TKey GetKeyForItem(TValue item); - - public bool Remove(TKey key) - { - lock (SyncRoot) - { - if (!InnerDictionary.TryGetValue(key, out CacheItem cacheItem)) return false; - RemoveInternal(cacheItem); - return true; - } - } - - protected abstract void OnAccess(CacheItem item); - - public bool Remove(TValue item) - { - return Remove(GetKeyForItem(item)); - } - - private void RemoveInternal(CacheItem item) - { - InnerDictionary.Remove(item.Key); - IDisposable disposable = item.Value as IDisposable; - if (disposable != null) - { - disposable.Dispose(); - } - } - - public bool TryGet(TKey key, out TValue item) - { - lock (SyncRoot) - { - if (InnerDictionary.TryGetValue(key, out CacheItem cacheItem)) - { - OnAccess(cacheItem); - item = cacheItem.Value; - return true; - } - } - item = default(TValue); - return false; - } - } -} diff --git a/neo/IO/Caching/CloneCache.cs b/neo/IO/Caching/CloneCache.cs deleted file mode 100644 index 951de4b942..0000000000 --- a/neo/IO/Caching/CloneCache.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Neo.IO.Caching -{ - internal class CloneCache : DataCache - where TKey : IEquatable, ISerializable - where TValue : class, ICloneable, ISerializable, new() - { - private DataCache innerCache; - - public CloneCache(DataCache innerCache) - { - this.innerCache = innerCache; - } - - protected override void AddInternal(TKey key, TValue value) - { - innerCache.Add(key, value); - } - - public override void DeleteInternal(TKey key) - { - innerCache.Delete(key); - } - - protected override IEnumerable> FindInternal(byte[] key_prefix) - { - foreach (KeyValuePair pair in innerCache.Find(key_prefix)) - yield return new KeyValuePair(pair.Key, pair.Value.Clone()); - } - - protected override TValue GetInternal(TKey key) - { - return innerCache[key].Clone(); - } - - protected override TValue TryGetInternal(TKey key) - { - return innerCache.TryGet(key)?.Clone(); - } - - protected override void UpdateInternal(TKey key, TValue value) - { - innerCache.GetAndChange(key).FromReplica(value); - } - } -} diff --git a/neo/IO/Caching/DataCache.cs b/neo/IO/Caching/DataCache.cs deleted file mode 100644 index 0ae9f6ba26..0000000000 --- a/neo/IO/Caching/DataCache.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Neo.IO.Caching -{ - public abstract class DataCache - where TKey : IEquatable, ISerializable - where TValue : class, ICloneable, ISerializable, new() - { - protected internal class Trackable - { - public TKey Key; - public TValue Item; - public TrackState State; - } - - private Dictionary dictionary = new Dictionary(); - - public TValue this[TKey key] - { - get - { - if (dictionary.TryGetValue(key, out Trackable trackable)) - { - if (trackable.State == TrackState.Deleted) - throw new KeyNotFoundException(); - } - else - { - trackable = new Trackable - { - Key = key, - Item = GetInternal(key), - State = TrackState.None - }; - dictionary.Add(key, trackable); - } - return trackable.Item; - } - } - - public void Add(TKey key, TValue value) - { - if (dictionary.TryGetValue(key, out Trackable trackable) && trackable.State != TrackState.Deleted) - throw new ArgumentException(); - dictionary[key] = new Trackable - { - Key = key, - Item = value, - State = trackable == null ? TrackState.Added : TrackState.Changed - }; - } - - protected abstract void AddInternal(TKey key, TValue value); - - public void Commit() - { - foreach (Trackable trackable in GetChangeSet()) - switch (trackable.State) - { - case TrackState.Added: - AddInternal(trackable.Key, trackable.Item); - break; - case TrackState.Changed: - UpdateInternal(trackable.Key, trackable.Item); - break; - case TrackState.Deleted: - DeleteInternal(trackable.Key); - break; - } - } - - public DataCache CreateSnapshot() - { - return new CloneCache(this); - } - - public void Delete(TKey key) - { - if (dictionary.TryGetValue(key, out Trackable trackable)) - { - if (trackable.State == TrackState.Added) - dictionary.Remove(key); - else - trackable.State = TrackState.Deleted; - } - else - { - TValue item = TryGetInternal(key); - if (item == null) return; - dictionary.Add(key, new Trackable - { - Key = key, - Item = item, - State = TrackState.Deleted - }); - } - } - - public abstract void DeleteInternal(TKey key); - - public void DeleteWhere(Func predicate) - { - foreach (Trackable trackable in dictionary.Where(p => p.Value.State != TrackState.Deleted && predicate(p.Key, p.Value.Item)).Select(p => p.Value)) - trackable.State = TrackState.Deleted; - } - - public IEnumerable> Find(byte[] key_prefix = null) - { - foreach (var pair in FindInternal(key_prefix ?? new byte[0])) - if (!dictionary.ContainsKey(pair.Key)) - yield return pair; - foreach (var pair in dictionary) - if (pair.Value.State != TrackState.Deleted && (key_prefix == null || pair.Key.ToArray().Take(key_prefix.Length).SequenceEqual(key_prefix))) - yield return new KeyValuePair(pair.Key, pair.Value.Item); - } - - protected abstract IEnumerable> FindInternal(byte[] key_prefix); - - protected internal IEnumerable GetChangeSet() - { - return dictionary.Values.Where(p => p.State != TrackState.None); - } - - protected abstract TValue GetInternal(TKey key); - - public TValue GetAndChange(TKey key, Func factory = null) - { - if (dictionary.TryGetValue(key, out Trackable trackable)) - { - if (trackable.State == TrackState.Deleted) - { - if (factory == null) throw new KeyNotFoundException(); - trackable.Item = factory(); - trackable.State = TrackState.Changed; - } - else if (trackable.State == TrackState.None) - { - trackable.State = TrackState.Changed; - } - } - else - { - trackable = new Trackable - { - Key = key, - Item = TryGetInternal(key) - }; - if (trackable.Item == null) - { - if (factory == null) throw new KeyNotFoundException(); - trackable.Item = factory(); - trackable.State = TrackState.Added; - } - else - { - trackable.State = TrackState.Changed; - } - dictionary.Add(key, trackable); - } - return trackable.Item; - } - - public TValue GetOrAdd(TKey key, Func factory) - { - if (dictionary.TryGetValue(key, out Trackable trackable)) - { - if (trackable.State == TrackState.Deleted) - { - trackable.Item = factory(); - trackable.State = TrackState.Changed; - } - } - else - { - trackable = new Trackable - { - Key = key, - Item = TryGetInternal(key) - }; - if (trackable.Item == null) - { - trackable.Item = factory(); - trackable.State = TrackState.Added; - } - else - { - trackable.State = TrackState.None; - } - dictionary.Add(key, trackable); - } - return trackable.Item; - } - - public TValue TryGet(TKey key) - { - if (dictionary.TryGetValue(key, out Trackable trackable)) - { - if (trackable.State == TrackState.Deleted) return null; - return trackable.Item; - } - TValue value = TryGetInternal(key); - if (value == null) return null; - dictionary.Add(key, new Trackable - { - Key = key, - Item = value, - State = TrackState.None - }); - return value; - } - - protected abstract TValue TryGetInternal(TKey key); - - protected abstract void UpdateInternal(TKey key, TValue value); - } -} diff --git a/neo/IO/Caching/FIFOCache.cs b/neo/IO/Caching/FIFOCache.cs deleted file mode 100644 index 4aa7198b50..0000000000 --- a/neo/IO/Caching/FIFOCache.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Neo.IO.Caching -{ - internal abstract class FIFOCache : Cache - { - public FIFOCache(int max_capacity) - : base(max_capacity) - { - } - - protected override void OnAccess(CacheItem item) - { - } - } -} diff --git a/neo/IO/Caching/LRUCache.cs b/neo/IO/Caching/LRUCache.cs deleted file mode 100644 index fa3b7a03d8..0000000000 --- a/neo/IO/Caching/LRUCache.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Neo.IO.Caching -{ - internal abstract class LRUCache : Cache - { - public LRUCache(int max_capacity) - : base(max_capacity) - { - } - - protected override void OnAccess(CacheItem item) - { - item.Time = DateTime.Now; - } - } -} diff --git a/neo/IO/Caching/MetaDataCache.cs b/neo/IO/Caching/MetaDataCache.cs deleted file mode 100644 index 20615cc287..0000000000 --- a/neo/IO/Caching/MetaDataCache.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace Neo.IO.Caching -{ - public abstract class MetaDataCache where T : class, ISerializable, new() - { - protected T Item; - protected TrackState State; - private Func factory; - - protected abstract T TryGetInternal(); - - protected MetaDataCache(Func factory) - { - this.factory = factory; - } - - public T Get() - { - if (Item == null) - { - Item = TryGetInternal(); - } - if (Item == null) - { - Item = factory?.Invoke() ?? new T(); - State = TrackState.Added; - } - return Item; - } - - public T GetAndChange() - { - T item = Get(); - if (State == TrackState.None) - State = TrackState.Changed; - return item; - } - } -} diff --git a/neo/IO/Caching/ReflectionCache.cs b/neo/IO/Caching/ReflectionCache.cs deleted file mode 100644 index 800561c6d9..0000000000 --- a/neo/IO/Caching/ReflectionCache.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Neo.IO.Caching -{ - public class ReflectionCache : Dictionary - { - /// - /// Constructor - /// - public ReflectionCache() { } - /// - /// Constructor - /// - /// Enum type - public static ReflectionCache CreateFromEnum() where EnumType : struct, IConvertible - { - Type enumType = typeof(EnumType); - - if (!enumType.GetTypeInfo().IsEnum) - throw new ArgumentException("K must be an enumerated type"); - - // Cache all types - ReflectionCache r = new ReflectionCache(); - - foreach (object t in Enum.GetValues(enumType)) - { - // Get enumn member - MemberInfo[] memInfo = enumType.GetMember(t.ToString()); - if (memInfo == null || memInfo.Length != 1) - throw (new FormatException()); - - // Get attribute - ReflectionCacheAttribute attribute = memInfo[0].GetCustomAttributes(typeof(ReflectionCacheAttribute), false) - .Cast() - .FirstOrDefault(); - - if (attribute == null) - throw (new FormatException()); - - // Append to cache - r.Add((T)t, attribute.Type); - } - return r; - } - /// - /// Create object from key - /// - /// Key - /// Default value - public object CreateInstance(T key, object def = null) - { - Type tp; - - // Get Type from cache - if (TryGetValue(key, out tp)) return Activator.CreateInstance(tp); - - // return null - return def; - } - /// - /// Create object from key - /// - /// Type - /// Key - /// Default value - public K CreateInstance(T key, K def = default(K)) - { - Type tp; - - // Get Type from cache - if (TryGetValue(key, out tp)) return (K)Activator.CreateInstance(tp); - - // return null - return def; - } - } -} \ No newline at end of file diff --git a/neo/IO/Caching/ReflectionCacheAttribute.cs b/neo/IO/Caching/ReflectionCacheAttribute.cs deleted file mode 100644 index e1c1200278..0000000000 --- a/neo/IO/Caching/ReflectionCacheAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Neo.IO.Caching -{ - public class ReflectionCacheAttribute : Attribute - { - /// - /// Type - /// - public Type Type { get; private set; } - - /// - /// Constructor - /// - /// Type - public ReflectionCacheAttribute(Type type) - { - Type = type; - } - } -} \ No newline at end of file diff --git a/neo/IO/Caching/RelayCache.cs b/neo/IO/Caching/RelayCache.cs deleted file mode 100644 index 0c7805074c..0000000000 --- a/neo/IO/Caching/RelayCache.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Neo.Network; - -namespace Neo.IO.Caching -{ - internal class RelayCache : FIFOCache - { - public RelayCache(int max_capacity) - : base(max_capacity) - { - } - - protected override UInt256 GetKeyForItem(IInventory item) - { - return item.Hash; - } - } -} diff --git a/neo/IO/Caching/TrackState.cs b/neo/IO/Caching/TrackState.cs deleted file mode 100644 index aa144aab34..0000000000 --- a/neo/IO/Caching/TrackState.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Neo.IO.Caching -{ - public enum TrackState : byte - { - None, - Added, - Changed, - Deleted - } -} diff --git a/neo/IO/Data/LevelDB/DB.cs b/neo/IO/Data/LevelDB/DB.cs deleted file mode 100644 index 17c448b428..0000000000 --- a/neo/IO/Data/LevelDB/DB.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; - -namespace Neo.IO.Data.LevelDB -{ - internal 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, Slice key) - { - IntPtr error; - Native.leveldb_delete(handle, options.handle, key.buffer, (UIntPtr)key.buffer.Length, out error); - NativeHelper.CheckError(error); - } - - public Slice Get(ReadOptions options, Slice key) - { - UIntPtr length; - IntPtr error; - IntPtr value = Native.leveldb_get(handle, options.handle, key.buffer, (UIntPtr)key.buffer.Length, out length, out error); - try - { - NativeHelper.CheckError(error); - if (value == IntPtr.Zero) - throw new LevelDBException("not found"); - return new Slice(value, length); - } - finally - { - if (value != IntPtr.Zero) Native.leveldb_free(value); - } - } - - 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 error; - IntPtr handle = Native.leveldb_open(options.handle, name, out error); - NativeHelper.CheckError(error); - return new DB(handle); - } - - public void Put(WriteOptions options, Slice key, Slice value) - { - IntPtr error; - Native.leveldb_put(handle, options.handle, key.buffer, (UIntPtr)key.buffer.Length, value.buffer, (UIntPtr)value.buffer.Length, out error); - NativeHelper.CheckError(error); - } - - public bool TryGet(ReadOptions options, Slice key, out Slice value) - { - UIntPtr length; - IntPtr error; - IntPtr v = Native.leveldb_get(handle, options.handle, key.buffer, (UIntPtr)key.buffer.Length, out length, out error); - if (error != IntPtr.Zero) - { - Native.leveldb_free(error); - value = default(Slice); - return false; - } - if (v == IntPtr.Zero) - { - value = default(Slice); - return false; - } - value = new Slice(v, length); - Native.leveldb_free(v); - return true; - } - - public void Write(WriteOptions options, WriteBatch write_batch) - { - // There's a bug in .Net Core. - // When calling DB.Write(), it will throw LevelDBException sometimes. - // But when you try to catch the exception, the bug disappears. - // We shall remove the "try...catch" clause when Microsoft fix the bug. - byte retry = 0; - while (true) - { - try - { - IntPtr error; - Native.leveldb_write(handle, options.handle, write_batch.handle, out error); - NativeHelper.CheckError(error); - break; - } - catch (LevelDBException ex) - { - if (++retry >= 4) throw; - System.IO.File.AppendAllText("leveldb.log", ex.Message + "\r\n"); - } - } - } - } -} diff --git a/neo/IO/Data/LevelDB/DbCache.cs b/neo/IO/Data/LevelDB/DbCache.cs deleted file mode 100644 index 26e31ab46f..0000000000 --- a/neo/IO/Data/LevelDB/DbCache.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Neo.IO.Caching; -using System; -using System.Collections.Generic; - -namespace Neo.IO.Data.LevelDB -{ - internal class DbCache : DataCache - where TKey : IEquatable, ISerializable, new() - where TValue : class, ICloneable, ISerializable, new() - { - private DB db; - private WriteBatch batch; - private byte prefix; - - public DbCache(DB db, byte prefix, WriteBatch batch = null) - { - this.db = db; - this.batch = batch; - this.prefix = prefix; - } - - protected override void AddInternal(TKey key, TValue value) - { - batch?.Put(prefix, key, value); - } - - public override void DeleteInternal(TKey key) - { - batch?.Delete(prefix, key); - } - - protected override IEnumerable> FindInternal(byte[] key_prefix) - { - return db.Find(ReadOptions.Default, SliceBuilder.Begin(prefix).Add(key_prefix), (k, v) => new KeyValuePair(k.ToArray().AsSerializable(1), v.ToArray().AsSerializable())); - } - - protected override TValue GetInternal(TKey key) - { - return db.Get(ReadOptions.Default, prefix, key); - } - - protected override TValue TryGetInternal(TKey key) - { - return db.TryGet(ReadOptions.Default, prefix, key); - } - - protected override void UpdateInternal(TKey key, TValue value) - { - batch?.Put(prefix, key, value); - } - } -} diff --git a/neo/IO/Data/LevelDB/DbMetaDataCache.cs b/neo/IO/Data/LevelDB/DbMetaDataCache.cs deleted file mode 100644 index 90582f0e23..0000000000 --- a/neo/IO/Data/LevelDB/DbMetaDataCache.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Neo.IO.Caching; -using System; - -namespace Neo.IO.Data.LevelDB -{ - internal class DbMetaDataCache : MetaDataCache where T : class, ISerializable, new() - { - private DB db; - private byte prefix; - - public DbMetaDataCache(DB db, byte prefix, Func factory = null) - : base(factory) - { - this.db = db; - this.prefix = prefix; - } - - public void Commit(WriteBatch batch) - { - switch (State) - { - case TrackState.Added: - case TrackState.Changed: - batch.Put(prefix, Item.ToArray()); - break; - case TrackState.Deleted: - batch.Delete(prefix); - break; - } - } - - protected override T TryGetInternal() - { - if (!db.TryGet(ReadOptions.Default, prefix, out Slice slice)) - return null; - return slice.ToArray().AsSerializable(); - } - } -} diff --git a/neo/IO/Data/LevelDB/Helper.cs b/neo/IO/Data/LevelDB/Helper.cs deleted file mode 100644 index 7c77b1d736..0000000000 --- a/neo/IO/Data/LevelDB/Helper.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Neo.IO.Data.LevelDB -{ - internal static class Helper - { - public static void Delete(this WriteBatch batch, byte prefix, ISerializable key) - { - batch.Delete(SliceBuilder.Begin(prefix).Add(key)); - } - - public static IEnumerable Find(this DB db, ReadOptions options, byte prefix) where T : class, ISerializable, new() - { - return Find(db, options, SliceBuilder.Begin(prefix), (k, v) => v.ToArray().AsSerializable()); - } - - public static IEnumerable Find(this DB db, ReadOptions options, Slice prefix, Func resultSelector) - { - using (Iterator it = db.NewIterator(options)) - { - for (it.Seek(prefix); it.Valid(); it.Next()) - { - Slice key = it.Key(); - byte[] x = key.ToArray(); - byte[] y = prefix.ToArray(); - if (x.Length < y.Length) break; - if (!x.Take(y.Length).SequenceEqual(y)) break; - yield return resultSelector(key, it.Value()); - } - } - } - - public static T Get(this DB db, ReadOptions options, byte prefix, ISerializable key) where T : class, ISerializable, new() - { - return db.Get(options, SliceBuilder.Begin(prefix).Add(key)).ToArray().AsSerializable(); - } - - public static T Get(this DB db, ReadOptions options, byte prefix, ISerializable key, Func resultSelector) - { - return resultSelector(db.Get(options, SliceBuilder.Begin(prefix).Add(key))); - } - - public static void Put(this WriteBatch batch, byte prefix, ISerializable key, ISerializable value) - { - batch.Put(SliceBuilder.Begin(prefix).Add(key), value.ToArray()); - } - - public static T TryGet(this DB db, ReadOptions options, byte prefix, ISerializable key) where T : class, ISerializable, new() - { - Slice slice; - if (!db.TryGet(options, SliceBuilder.Begin(prefix).Add(key), out slice)) - return null; - return slice.ToArray().AsSerializable(); - } - - public static T TryGet(this DB db, ReadOptions options, byte prefix, ISerializable key, Func resultSelector) where T : class - { - Slice slice; - if (!db.TryGet(options, SliceBuilder.Begin(prefix).Add(key), out slice)) - return null; - return resultSelector(slice); - } - } -} diff --git a/neo/IO/Data/LevelDB/Iterator.cs b/neo/IO/Data/LevelDB/Iterator.cs deleted file mode 100644 index 4163a2506c..0000000000 --- a/neo/IO/Data/LevelDB/Iterator.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; - -namespace Neo.IO.Data.LevelDB -{ - internal class Iterator : IDisposable - { - private IntPtr handle; - - internal Iterator(IntPtr handle) - { - this.handle = handle; - } - - private void CheckError() - { - IntPtr error; - Native.leveldb_iter_get_error(handle, out error); - NativeHelper.CheckError(error); - } - - public void Dispose() - { - if (handle != IntPtr.Zero) - { - Native.leveldb_iter_destroy(handle); - handle = IntPtr.Zero; - } - } - - public Slice Key() - { - UIntPtr length; - IntPtr key = Native.leveldb_iter_key(handle, out length); - CheckError(); - return new Slice(key, length); - } - - public void Next() - { - Native.leveldb_iter_next(handle); - CheckError(); - } - - public void Prev() - { - Native.leveldb_iter_prev(handle); - CheckError(); - } - - public void Seek(Slice target) - { - Native.leveldb_iter_seek(handle, target.buffer, (UIntPtr)target.buffer.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 Slice Value() - { - UIntPtr length; - IntPtr value = Native.leveldb_iter_value(handle, out length); - CheckError(); - return new Slice(value, length); - } - } -} diff --git a/neo/IO/Data/LevelDB/LevelDBException.cs b/neo/IO/Data/LevelDB/LevelDBException.cs deleted file mode 100644 index 3f6eb547a0..0000000000 --- a/neo/IO/Data/LevelDB/LevelDBException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Data.Common; - -namespace Neo.IO.Data.LevelDB -{ - internal class LevelDBException : DbException - { - internal LevelDBException(string message) - : base(message) - { - } - } -} diff --git a/neo/IO/Data/LevelDB/Native.cs b/neo/IO/Data/LevelDB/Native.cs deleted file mode 100644 index ae0892e32a..0000000000 --- a/neo/IO/Data/LevelDB/Native.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace Neo.IO.Data.LevelDB -{ - internal enum CompressionType : byte - { - kNoCompression = 0x0, - kSnappyCompression = 0x1 - } - - internal static class Native - { -#if NET47 - static Native() - { - string platform = IntPtr.Size == 8 ? "x64" : "x86"; - LoadLibrary(Path.Combine(AppContext.BaseDirectory, platform, "libleveldb")); - } - - [DllImport("kernel32")] - private static extern IntPtr LoadLibrary(string dllToLoad); -#endif - - #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); - #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 - { - 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/neo/IO/Data/LevelDB/Options.cs b/neo/IO/Data/LevelDB/Options.cs deleted file mode 100644 index 949a47e6a4..0000000000 --- a/neo/IO/Data/LevelDB/Options.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; - -namespace Neo.IO.Data.LevelDB -{ - internal 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/neo/IO/Data/LevelDB/ReadOptions.cs b/neo/IO/Data/LevelDB/ReadOptions.cs deleted file mode 100644 index a3acecfea1..0000000000 --- a/neo/IO/Data/LevelDB/ReadOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; - -namespace Neo.IO.Data.LevelDB -{ - internal 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/neo/IO/Data/LevelDB/Slice.cs b/neo/IO/Data/LevelDB/Slice.cs deleted file mode 100644 index e36f46c994..0000000000 --- a/neo/IO/Data/LevelDB/Slice.cs +++ /dev/null @@ -1,244 +0,0 @@ -using Neo.Cryptography; -using System; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; - -namespace Neo.IO.Data.LevelDB -{ - internal struct Slice : IComparable, IEquatable - { - internal byte[] buffer; - - internal Slice(IntPtr data, UIntPtr length) - { - buffer = new byte[(int)length]; - Marshal.Copy(data, buffer, 0, (int)length); - } - - public int CompareTo(Slice other) - { - for (int i = 0; i < buffer.Length && i < other.buffer.Length; i++) - { - int r = buffer[i].CompareTo(other.buffer[i]); - if (r != 0) return r; - } - return buffer.Length.CompareTo(other.buffer.Length); - } - - public bool Equals(Slice other) - { - if (buffer.Length != other.buffer.Length) return false; - return buffer.SequenceEqual(other.buffer); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (!(obj is Slice)) return false; - return Equals((Slice)obj); - } - - public override int GetHashCode() - { - return (int)buffer.Murmur32(0); - } - - public byte[] ToArray() - { - return buffer ?? new byte[0]; - } - - unsafe public bool ToBoolean() - { - if (buffer.Length != sizeof(bool)) - throw new InvalidCastException(); - fixed (byte* pbyte = &buffer[0]) - { - return *((bool*)pbyte); - } - } - - public byte ToByte() - { - if (buffer.Length != sizeof(byte)) - throw new InvalidCastException(); - return buffer[0]; - } - - unsafe public double ToDouble() - { - if (buffer.Length != sizeof(double)) - throw new InvalidCastException(); - fixed (byte* pbyte = &buffer[0]) - { - return *((double*)pbyte); - } - } - - unsafe public short ToInt16() - { - if (buffer.Length != sizeof(short)) - throw new InvalidCastException(); - fixed (byte* pbyte = &buffer[0]) - { - return *((short*)pbyte); - } - } - - unsafe public int ToInt32() - { - if (buffer.Length != sizeof(int)) - throw new InvalidCastException(); - fixed (byte* pbyte = &buffer[0]) - { - return *((int*)pbyte); - } - } - - unsafe public long ToInt64() - { - if (buffer.Length != sizeof(long)) - throw new InvalidCastException(); - fixed (byte* pbyte = &buffer[0]) - { - return *((long*)pbyte); - } - } - - unsafe public float ToSingle() - { - if (buffer.Length != sizeof(float)) - throw new InvalidCastException(); - fixed (byte* pbyte = &buffer[0]) - { - return *((float*)pbyte); - } - } - - public override string ToString() - { - return Encoding.UTF8.GetString(buffer); - } - - unsafe public ushort ToUInt16() - { - if (buffer.Length != sizeof(ushort)) - throw new InvalidCastException(); - fixed (byte* pbyte = &buffer[0]) - { - return *((ushort*)pbyte); - } - } - - unsafe public uint ToUInt32(int index = 0) - { - if (buffer.Length != sizeof(uint) + index) - throw new InvalidCastException(); - fixed (byte* pbyte = &buffer[index]) - { - return *((uint*)pbyte); - } - } - - unsafe public ulong ToUInt64() - { - if (buffer.Length != sizeof(ulong)) - throw new InvalidCastException(); - fixed (byte* pbyte = &buffer[0]) - { - return *((ulong*)pbyte); - } - } - - public static implicit operator Slice(byte[] data) - { - return new Slice { buffer = data }; - } - - public static implicit operator Slice(bool data) - { - return new Slice { buffer = BitConverter.GetBytes(data) }; - } - - public static implicit operator Slice(byte data) - { - return new Slice { buffer = new[] { data } }; - } - - public static implicit operator Slice(double data) - { - return new Slice { buffer = BitConverter.GetBytes(data) }; - } - - public static implicit operator Slice(short data) - { - return new Slice { buffer = BitConverter.GetBytes(data) }; - } - - public static implicit operator Slice(int data) - { - return new Slice { buffer = BitConverter.GetBytes(data) }; - } - - public static implicit operator Slice(long data) - { - return new Slice { buffer = BitConverter.GetBytes(data) }; - } - - public static implicit operator Slice(float data) - { - return new Slice { buffer = BitConverter.GetBytes(data) }; - } - - public static implicit operator Slice(string data) - { - return new Slice { buffer = Encoding.UTF8.GetBytes(data) }; - } - - public static implicit operator Slice(ushort data) - { - return new Slice { buffer = BitConverter.GetBytes(data) }; - } - - public static implicit operator Slice(uint data) - { - return new Slice { buffer = BitConverter.GetBytes(data) }; - } - - public static implicit operator Slice(ulong data) - { - return new Slice { buffer = BitConverter.GetBytes(data) }; - } - - public static bool operator <(Slice x, Slice y) - { - return x.CompareTo(y) < 0; - } - - public static bool operator <=(Slice x, Slice y) - { - return x.CompareTo(y) <= 0; - } - - public static bool operator >(Slice x, Slice y) - { - return x.CompareTo(y) > 0; - } - - public static bool operator >=(Slice x, Slice y) - { - return x.CompareTo(y) >= 0; - } - - public static bool operator ==(Slice x, Slice y) - { - return x.Equals(y); - } - - public static bool operator !=(Slice x, Slice y) - { - return !x.Equals(y); - } - } -} diff --git a/neo/IO/Data/LevelDB/SliceBuilder.cs b/neo/IO/Data/LevelDB/SliceBuilder.cs deleted file mode 100644 index 4cdda9d441..0000000000 --- a/neo/IO/Data/LevelDB/SliceBuilder.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Neo.IO.Data.LevelDB -{ - internal class SliceBuilder - { - private List data = new List(); - - private SliceBuilder() - { - } - - public SliceBuilder Add(byte value) - { - data.Add(value); - return this; - } - - public SliceBuilder Add(ushort value) - { - data.AddRange(BitConverter.GetBytes(value)); - return this; - } - - public SliceBuilder Add(uint value) - { - data.AddRange(BitConverter.GetBytes(value)); - return this; - } - - public SliceBuilder Add(long value) - { - data.AddRange(BitConverter.GetBytes(value)); - return this; - } - - public SliceBuilder Add(IEnumerable value) - { - data.AddRange(value); - return this; - } - - public SliceBuilder Add(string value) - { - data.AddRange(Encoding.UTF8.GetBytes(value)); - return this; - } - - public SliceBuilder Add(ISerializable value) - { - data.AddRange(value.ToArray()); - return this; - } - - public static SliceBuilder Begin() - { - return new SliceBuilder(); - } - - public static SliceBuilder Begin(byte prefix) - { - return new SliceBuilder().Add(prefix); - } - - public static implicit operator Slice(SliceBuilder value) - { - return value.data.ToArray(); - } - } -} diff --git a/neo/IO/Data/LevelDB/Snapshot.cs b/neo/IO/Data/LevelDB/Snapshot.cs deleted file mode 100644 index 9f198368a7..0000000000 --- a/neo/IO/Data/LevelDB/Snapshot.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Neo.IO.Data.LevelDB -{ - internal class Snapshot : IDisposable - { - internal IntPtr db, handle; - - internal Snapshot(IntPtr db) - { - this.db = db; - this.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/neo/IO/Data/LevelDB/WriteBatch.cs b/neo/IO/Data/LevelDB/WriteBatch.cs deleted file mode 100644 index 641746c972..0000000000 --- a/neo/IO/Data/LevelDB/WriteBatch.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Neo.IO.Data.LevelDB -{ - internal 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(Slice key) - { - Native.leveldb_writebatch_delete(handle, key.buffer, (UIntPtr)key.buffer.Length); - } - - public void Put(Slice key, Slice value) - { - Native.leveldb_writebatch_put(handle, key.buffer, (UIntPtr)key.buffer.Length, value.buffer, (UIntPtr)value.buffer.Length); - } - } -} diff --git a/neo/IO/Data/LevelDB/WriteOptions.cs b/neo/IO/Data/LevelDB/WriteOptions.cs deleted file mode 100644 index f16dfe5a02..0000000000 --- a/neo/IO/Data/LevelDB/WriteOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace Neo.IO.Data.LevelDB -{ - internal class WriteOptions - { - public static readonly WriteOptions Default = new WriteOptions(); - 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/neo/IO/Helper.cs b/neo/IO/Helper.cs deleted file mode 100644 index 323db921d8..0000000000 --- a/neo/IO/Helper.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text; - -namespace Neo.IO -{ - public static class Helper - { - public static T AsSerializable(this byte[] value, int start = 0) where T : ISerializable, new() - { - using (MemoryStream ms = new MemoryStream(value, start, value.Length - start, false)) - using (BinaryReader reader = new BinaryReader(ms, Encoding.UTF8)) - { - return reader.ReadSerializable(); - } - } - - public static ISerializable AsSerializable(this byte[] value, Type type) - { - if (!typeof(ISerializable).GetTypeInfo().IsAssignableFrom(type)) - throw new InvalidCastException(); - ISerializable serializable = (ISerializable)Activator.CreateInstance(type); - using (MemoryStream ms = new MemoryStream(value, false)) - using (BinaryReader reader = new BinaryReader(ms, Encoding.UTF8)) - { - serializable.Deserialize(reader); - } - return serializable; - } - - public static T[] AsSerializableArray(this byte[] value, int max = 0x10000000) where T : ISerializable, new() - { - using (MemoryStream ms = new MemoryStream(value, false)) - using (BinaryReader reader = new BinaryReader(ms, Encoding.UTF8)) - { - return reader.ReadSerializableArray(max); - } - } - - internal static int GetVarSize(int value) - { - if (value < 0xFD) - return sizeof(byte); - else if (value <= 0xFFFF) - return sizeof(byte) + sizeof(ushort); - else - return sizeof(byte) + sizeof(uint); - } - - internal static int GetVarSize(this T[] value) - { - int value_size; - Type t = typeof(T); - if (typeof(ISerializable).IsAssignableFrom(t)) - { - value_size = value.OfType().Sum(p => p.Size); - } - else if (t.GetTypeInfo().IsEnum) - { - int element_size; - Type u = t.GetTypeInfo().GetEnumUnderlyingType(); - if (u == typeof(sbyte) || u == typeof(byte)) - element_size = 1; - else if (u == typeof(short) || u == typeof(ushort)) - element_size = 2; - else if (u == typeof(int) || u == typeof(uint)) - element_size = 4; - else //if (u == typeof(long) || u == typeof(ulong)) - element_size = 8; - value_size = value.Length * element_size; - } - else - { - value_size = value.Length * Marshal.SizeOf(); - } - return GetVarSize(value.Length) + value_size; - } - - internal static int GetVarSize(this string value) - { - int size = Encoding.UTF8.GetByteCount(value); - return GetVarSize(size) + size; - } - - public static string ReadFixedString(this BinaryReader reader, int length) - { - byte[] data = reader.ReadBytes(length); - return Encoding.UTF8.GetString(data.TakeWhile(p => p != 0).ToArray()); - } - - public static T ReadSerializable(this BinaryReader reader) where T : ISerializable, new() - { - T obj = new T(); - obj.Deserialize(reader); - return obj; - } - - public static T[] ReadSerializableArray(this BinaryReader reader, int max = 0x10000000) where T : ISerializable, new() - { - T[] array = new T[reader.ReadVarInt((ulong)max)]; - for (int i = 0; i < array.Length; i++) - { - array[i] = new T(); - array[i].Deserialize(reader); - } - return array; - } - - public static byte[] ReadVarBytes(this BinaryReader reader, int max = 0X7fffffc7) - { - return reader.ReadBytes((int)reader.ReadVarInt((ulong)max)); - } - - public static ulong ReadVarInt(this BinaryReader reader, ulong max = ulong.MaxValue) - { - byte fb = reader.ReadByte(); - ulong value; - if (fb == 0xFD) - value = reader.ReadUInt16(); - else if (fb == 0xFE) - value = reader.ReadUInt32(); - else if (fb == 0xFF) - value = reader.ReadUInt64(); - else - value = fb; - if (value > max) throw new FormatException(); - return value; - } - - public static string ReadVarString(this BinaryReader reader, int max = 0X7fffffc7) - { - return Encoding.UTF8.GetString(reader.ReadVarBytes(max)); - } - - public static byte[] ToArray(this ISerializable value) - { - using (MemoryStream ms = new MemoryStream()) - using (BinaryWriter writer = new BinaryWriter(ms, Encoding.UTF8)) - { - value.Serialize(writer); - writer.Flush(); - return ms.ToArray(); - } - } - - public static byte[] ToByteArray(this T[] value) where T : ISerializable - { - using (MemoryStream ms = new MemoryStream()) - using (BinaryWriter writer = new BinaryWriter(ms, Encoding.UTF8)) - { - writer.Write(value); - writer.Flush(); - return ms.ToArray(); - } - } - - public static void Write(this BinaryWriter writer, ISerializable value) - { - value.Serialize(writer); - } - - public static void Write(this BinaryWriter writer, T[] value) where T : ISerializable - { - writer.WriteVarInt(value.Length); - for (int i = 0; i < value.Length; i++) - { - value[i].Serialize(writer); - } - } - - public static void WriteFixedString(this BinaryWriter writer, string value, int length) - { - if (value == null) - throw new ArgumentNullException(nameof(value)); - if (value.Length > length) - throw new ArgumentException(); - byte[] bytes = Encoding.UTF8.GetBytes(value); - if (bytes.Length > length) - throw new ArgumentException(); - writer.Write(bytes); - if (bytes.Length < length) - writer.Write(new byte[length - bytes.Length]); - } - - public static void WriteVarBytes(this BinaryWriter writer, byte[] value) - { - writer.WriteVarInt(value.Length); - writer.Write(value); - } - - public static void WriteVarInt(this BinaryWriter writer, long value) - { - if (value < 0) - throw new ArgumentOutOfRangeException(); - if (value < 0xFD) - { - writer.Write((byte)value); - } - else if (value <= 0xFFFF) - { - writer.Write((byte)0xFD); - writer.Write((ushort)value); - } - else if (value <= 0xFFFFFFFF) - { - writer.Write((byte)0xFE); - writer.Write((uint)value); - } - else - { - writer.Write((byte)0xFF); - writer.Write(value); - } - } - - public static void WriteVarString(this BinaryWriter writer, string value) - { - writer.WriteVarBytes(Encoding.UTF8.GetBytes(value)); - } - } -} diff --git a/neo/IO/ICloneable.cs b/neo/IO/ICloneable.cs deleted file mode 100644 index 4df61c84b8..0000000000 --- a/neo/IO/ICloneable.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Neo.IO -{ - public interface ICloneable - { - T Clone(); - void FromReplica(T replica); - } -} diff --git a/neo/IO/ISerializable.cs b/neo/IO/ISerializable.cs deleted file mode 100644 index b91b18c86d..0000000000 --- a/neo/IO/ISerializable.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.IO; - -namespace Neo.IO -{ - /// - /// 为序列化提供一个接口 - /// - public interface ISerializable - { - int Size { get; } - - /// - /// 序列化 - /// - /// 存放序列化后的结果 - void Serialize(BinaryWriter writer); - - /// - /// 反序列化 - /// - /// 数据来源 - void Deserialize(BinaryReader reader); - } -} diff --git a/neo/IO/Json/JArray.cs b/neo/IO/Json/JArray.cs deleted file mode 100644 index 9bbfcc0868..0000000000 --- a/neo/IO/Json/JArray.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Neo.IO.Json -{ - public class JArray : JObject, IList - { - private List items = new List(); - - public JArray(params JObject[] items) : this((IEnumerable)items) - { - } - - public JArray(IEnumerable items) - { - this.items.AddRange(items); - } - - public JObject this[int index] - { - get - { - return items[index]; - } - set - { - items[index] = value; - } - } - - public int Count - { - get - { - return items.Count; - } - } - - public bool IsReadOnly - { - get - { - return false; - } - } - - public void Add(JObject item) - { - items.Add(item); - } - - public void Clear() - { - items.Clear(); - } - - public bool Contains(JObject item) - { - return items.Contains(item); - } - - public void CopyTo(JObject[] array, int arrayIndex) - { - items.CopyTo(array, arrayIndex); - } - - public IEnumerator GetEnumerator() - { - return items.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public int IndexOf(JObject item) - { - return items.IndexOf(item); - } - - public void Insert(int index, JObject item) - { - items.Insert(index, item); - } - - internal new static JArray Parse(TextReader reader) - { - SkipSpace(reader); - if (reader.Read() != '[') throw new FormatException(); - SkipSpace(reader); - JArray array = new JArray(); - while (reader.Peek() != ']') - { - if (reader.Peek() == ',') reader.Read(); - JObject obj = JObject.Parse(reader); - array.items.Add(obj); - SkipSpace(reader); - } - reader.Read(); - return array; - } - - public bool Remove(JObject item) - { - return items.Remove(item); - } - - public void RemoveAt(int index) - { - items.RemoveAt(index); - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (JObject item in items) - { - if (item == null) - sb.Append("null"); - else - sb.Append(item); - sb.Append(','); - } - if (items.Count == 0) - { - sb.Append(']'); - } - else - { - sb[sb.Length - 1] = ']'; - } - return sb.ToString(); - } - } -} diff --git a/neo/IO/Json/JBoolean.cs b/neo/IO/Json/JBoolean.cs deleted file mode 100644 index b4cfc42e0f..0000000000 --- a/neo/IO/Json/JBoolean.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.IO; - -namespace Neo.IO.Json -{ - public class JBoolean : JObject - { - public bool Value { get; private set; } - - public JBoolean(bool value = false) - { - this.Value = value; - } - - public override bool AsBoolean() - { - return Value; - } - - public override string AsString() - { - return Value.ToString().ToLower(); - } - - public override bool CanConvertTo(Type type) - { - if (type == typeof(bool)) - return true; - if (type == typeof(string)) - return true; - return false; - } - - internal new static JBoolean Parse(TextReader reader) - { - SkipSpace(reader); - char firstChar = (char)reader.Read(); - if (firstChar == 't') - { - int c2 = reader.Read(); - int c3 = reader.Read(); - int c4 = reader.Read(); - if (c2 == 'r' && c3 == 'u' && c4 == 'e') - { - return new JBoolean(true); - } - } - else if (firstChar == 'f') - { - int c2 = reader.Read(); - int c3 = reader.Read(); - int c4 = reader.Read(); - int c5 = reader.Read(); - if (c2 == 'a' && c3 == 'l' && c4 == 's' && c5 == 'e') - { - return new JBoolean(false); - } - } - throw new FormatException(); - } - - public override string ToString() - { - return Value.ToString().ToLower(); - } - } -} diff --git a/neo/IO/Json/JNumber.cs b/neo/IO/Json/JNumber.cs deleted file mode 100644 index 5f90c9b182..0000000000 --- a/neo/IO/Json/JNumber.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using System.Text; - -namespace Neo.IO.Json -{ - public class JNumber : JObject - { - public double Value { get; private set; } - - public JNumber(double value = 0) - { - this.Value = value; - } - - public override bool AsBoolean() - { - if (Value == 0) - return false; - return true; - } - - public override T AsEnum(bool ignoreCase = false) - { - Type t = typeof(T); - TypeInfo ti = t.GetTypeInfo(); - if (!ti.IsEnum) - throw new InvalidCastException(); - if (ti.GetEnumUnderlyingType() == typeof(byte)) - return (T)Enum.ToObject(t, (byte)Value); - if (ti.GetEnumUnderlyingType() == typeof(int)) - return (T)Enum.ToObject(t, (int)Value); - if (ti.GetEnumUnderlyingType() == typeof(long)) - return (T)Enum.ToObject(t, (long)Value); - if (ti.GetEnumUnderlyingType() == typeof(sbyte)) - return (T)Enum.ToObject(t, (sbyte)Value); - if (ti.GetEnumUnderlyingType() == typeof(short)) - return (T)Enum.ToObject(t, (short)Value); - if (ti.GetEnumUnderlyingType() == typeof(uint)) - return (T)Enum.ToObject(t, (uint)Value); - if (ti.GetEnumUnderlyingType() == typeof(ulong)) - return (T)Enum.ToObject(t, (ulong)Value); - if (ti.GetEnumUnderlyingType() == typeof(ushort)) - return (T)Enum.ToObject(t, (ushort)Value); - throw new InvalidCastException(); - } - - public override double AsNumber() - { - return Value; - } - - public override string AsString() - { - return Value.ToString(); - } - - public override bool CanConvertTo(Type type) - { - if (type == typeof(bool)) - return true; - if (type == typeof(double)) - return true; - if (type == typeof(string)) - return true; - TypeInfo ti = type.GetTypeInfo(); - if (ti.IsEnum && Enum.IsDefined(type, Convert.ChangeType(Value, ti.GetEnumUnderlyingType()))) - return true; - return false; - } - - internal new static JNumber Parse(TextReader reader) - { - SkipSpace(reader); - StringBuilder sb = new StringBuilder(); - while (true) - { - char c = (char)reader.Peek(); - if (c >= '0' && c <= '9' || c == '.' || c == '-') - { - sb.Append(c); - reader.Read(); - } - else - { - break; - } - } - return new JNumber(double.Parse(sb.ToString())); - } - - public override string ToString() - { - return Value.ToString(); - } - - public DateTime ToTimestamp() - { - if (Value < 0 || Value > ulong.MaxValue) - throw new InvalidCastException(); - return ((ulong)Value).ToDateTime(); - } - } -} diff --git a/neo/IO/Json/JObject.cs b/neo/IO/Json/JObject.cs deleted file mode 100644 index 734b6dd59c..0000000000 --- a/neo/IO/Json/JObject.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Neo.IO.Json -{ - public class JObject - { - public static readonly JObject Null = null; - private Dictionary properties = new Dictionary(); - - public JObject this[string name] - { - get - { - properties.TryGetValue(name, out JObject value); - return value; - } - set - { - properties[name] = value; - } - } - - public IReadOnlyDictionary Properties => properties; - - public virtual bool AsBoolean() - { - throw new InvalidCastException(); - } - - public bool AsBooleanOrDefault(bool value = false) - { - if (!CanConvertTo(typeof(bool))) - return value; - return AsBoolean(); - } - - public virtual T AsEnum(bool ignoreCase = false) - { - throw new InvalidCastException(); - } - - public T AsEnumOrDefault(T value = default(T), bool ignoreCase = false) - { - if (!CanConvertTo(typeof(T))) - return value; - return AsEnum(ignoreCase); - } - - public virtual double AsNumber() - { - throw new InvalidCastException(); - } - - public double AsNumberOrDefault(double value = 0) - { - if (!CanConvertTo(typeof(double))) - return value; - return AsNumber(); - } - - public virtual string AsString() - { - throw new InvalidCastException(); - } - - public string AsStringOrDefault(string value = null) - { - if (!CanConvertTo(typeof(string))) - return value; - return AsString(); - } - - public virtual bool CanConvertTo(Type type) - { - return false; - } - - public bool ContainsProperty(string key) - { - return properties.ContainsKey(key); - } - - public static JObject Parse(TextReader reader) - { - SkipSpace(reader); - char firstChar = (char)reader.Peek(); - if (firstChar == '\"' || firstChar == '\'') - { - return JString.Parse(reader); - } - if (firstChar == '[') - { - return JArray.Parse(reader); - } - if ((firstChar >= '0' && firstChar <= '9') || firstChar == '-') - { - return JNumber.Parse(reader); - } - if (firstChar == 't' || firstChar == 'f') - { - return JBoolean.Parse(reader); - } - if (firstChar == 'n') - { - return ParseNull(reader); - } - if (reader.Read() != '{') throw new FormatException(); - SkipSpace(reader); - JObject obj = new JObject(); - while (reader.Peek() != '}') - { - if (reader.Peek() == ',') reader.Read(); - SkipSpace(reader); - string name = JString.Parse(reader).Value; - SkipSpace(reader); - if (reader.Read() != ':') throw new FormatException(); - JObject value = Parse(reader); - obj.properties.Add(name, value); - SkipSpace(reader); - } - reader.Read(); - return obj; - } - - public static JObject Parse(string value) - { - using (StringReader reader = new StringReader(value)) - { - return Parse(reader); - } - } - - private static JObject ParseNull(TextReader reader) - { - char firstChar = (char)reader.Read(); - if (firstChar == 'n') - { - int c2 = reader.Read(); - int c3 = reader.Read(); - int c4 = reader.Read(); - if (c2 == 'u' && c3 == 'l' && c4 == 'l') - { - return null; - } - } - throw new FormatException(); - } - - protected static void SkipSpace(TextReader reader) - { - while (reader.Peek() == ' ' || reader.Peek() == '\t' || reader.Peek() == '\r' || reader.Peek() == '\n') - { - reader.Read(); - } - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('{'); - foreach (KeyValuePair pair in properties) - { - sb.Append('"'); - sb.Append(pair.Key); - sb.Append('"'); - sb.Append(':'); - if (pair.Value == null) - { - sb.Append("null"); - } - else - { - sb.Append(pair.Value); - } - sb.Append(','); - } - if (properties.Count == 0) - { - sb.Append('}'); - } - else - { - sb[sb.Length - 1] = '}'; - } - return sb.ToString(); - } - - public static implicit operator JObject(Enum value) - { - return new JString(value.ToString()); - } - - public static implicit operator JObject(JObject[] value) - { - return new JArray(value); - } - - public static implicit operator JObject(bool value) - { - return new JBoolean(value); - } - - public static implicit operator JObject(double value) - { - return new JNumber(value); - } - - public static implicit operator JObject(string value) - { - return value == null ? null : new JString(value); - } - } -} diff --git a/neo/IO/Json/JString.cs b/neo/IO/Json/JString.cs deleted file mode 100644 index be0055f75f..0000000000 --- a/neo/IO/Json/JString.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Text; -using System.Text.Encodings.Web; - -namespace Neo.IO.Json -{ - public class JString : JObject - { - public string Value { get; private set; } - - public JString(string value) - { - if (value == null) - throw new ArgumentNullException(); - this.Value = value; - } - - public override bool AsBoolean() - { - switch (Value.ToLower()) - { - case "0": - case "f": - case "false": - case "n": - case "no": - case "off": - return false; - default: - return true; - } - } - - public override T AsEnum(bool ignoreCase = false) - { - try - { - return (T)Enum.Parse(typeof(T), Value, ignoreCase); - } - catch - { - throw new InvalidCastException(); - } - } - - public override double AsNumber() - { - try - { - return double.Parse(Value); - } - catch - { - throw new InvalidCastException(); - } - } - - public override string AsString() - { - return Value; - } - - public override bool CanConvertTo(Type type) - { - if (type == typeof(bool)) - return true; - if (type.GetTypeInfo().IsEnum && Enum.IsDefined(type, Value)) - return true; - if (type == typeof(double)) - return true; - if (type == typeof(string)) - return true; - return false; - } - - internal new static JString Parse(TextReader reader) - { - SkipSpace(reader); - char[] buffer = new char[4]; - char firstChar = (char)reader.Read(); - if (firstChar != '\"' && firstChar != '\'') throw new FormatException(); - StringBuilder sb = new StringBuilder(); - while (true) - { - char c = (char)reader.Read(); - if (c == 65535) throw new FormatException(); - if (c == firstChar) break; - if (c == '\\') - { - c = (char)reader.Read(); - if (c == 'u') - { - reader.Read(buffer, 0, 4); - c = (char)short.Parse(new string(buffer), NumberStyles.HexNumber); - } - } - sb.Append(c); - } - return new JString(sb.ToString()); - } - - public override string ToString() - { - return $"\"{JavaScriptEncoder.Default.Encode(Value)}\""; - } - } -} diff --git a/neo/IO/Wrappers/ByteWrapper.cs b/neo/IO/Wrappers/ByteWrapper.cs deleted file mode 100644 index d576a2efa9..0000000000 --- a/neo/IO/Wrappers/ByteWrapper.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.IO; - -namespace Neo.IO.Wrappers -{ - internal class ByteWrapper : SerializableWrapper - { - private byte value; - - public override int Size => sizeof(byte); - - public ByteWrapper(byte value) - { - this.value = value; - } - - public override void Deserialize(BinaryReader reader) - { - value = reader.ReadByte(); - } - - public override void Serialize(BinaryWriter writer) - { - writer.Write(value); - } - } -} diff --git a/neo/IO/Wrappers/SerializableWrapper.cs b/neo/IO/Wrappers/SerializableWrapper.cs deleted file mode 100644 index 7bc6ae3b3c..0000000000 --- a/neo/IO/Wrappers/SerializableWrapper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.IO; - -namespace Neo.IO.Wrappers -{ - public abstract class SerializableWrapper : ISerializable - { - public abstract int Size { get; } - - public abstract void Deserialize(BinaryReader reader); - public abstract void Serialize(BinaryWriter writer); - - public static implicit operator SerializableWrapper(byte value) - { - return new ByteWrapper(value); - } - } - - public abstract class SerializableWrapper : SerializableWrapper where T : IEquatable - { - } -} diff --git a/neo/Implementations/Blockchains/LevelDB/DataEntryPrefix.cs b/neo/Implementations/Blockchains/LevelDB/DataEntryPrefix.cs deleted file mode 100644 index e667c2c8cf..0000000000 --- a/neo/Implementations/Blockchains/LevelDB/DataEntryPrefix.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Neo.Implementations.Blockchains.LevelDB -{ - internal static class DataEntryPrefix - { - public const byte DATA_Block = 0x01; - public const byte DATA_Transaction = 0x02; - - public const byte ST_Account = 0x40; - public const byte ST_Coin = 0x44; - public const byte ST_SpentCoin = 0x45; - public const byte ST_Validator = 0x48; - public const byte ST_Asset = 0x4c; - public const byte ST_Contract = 0x50; - public const byte ST_Storage = 0x70; - - public const byte IX_HeaderHashList = 0x80; - public const byte IX_ValidatorsCount = 0x90; - - public const byte SYS_CurrentBlock = 0xc0; - public const byte SYS_CurrentHeader = 0xc1; - public const byte SYS_Version = 0xf0; - } -} diff --git a/neo/Implementations/Blockchains/LevelDB/LevelDBBlockchain.cs b/neo/Implementations/Blockchains/LevelDB/LevelDBBlockchain.cs deleted file mode 100644 index 31a40e8ed1..0000000000 --- a/neo/Implementations/Blockchains/LevelDB/LevelDBBlockchain.cs +++ /dev/null @@ -1,624 +0,0 @@ -using Neo.Core; -using Neo.Cryptography; -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.IO.Caching; -using Neo.IO.Data.LevelDB; -using Neo.SmartContract; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using Iterator = Neo.IO.Data.LevelDB.Iterator; - -namespace Neo.Implementations.Blockchains.LevelDB -{ - public class LevelDBBlockchain : Blockchain - { - public static event EventHandler ApplicationExecuted; - - private DB db; - private Thread thread_persistence; - private List header_index = new List(); - private Dictionary header_cache = new Dictionary(); - private Dictionary block_cache = new Dictionary(); - private uint current_block_height = 0; - private uint stored_header_count = 0; - private AutoResetEvent new_block_event = new AutoResetEvent(false); - private bool disposed = false; - - public override UInt256 CurrentBlockHash => header_index[(int)current_block_height]; - public override UInt256 CurrentHeaderHash => header_index[header_index.Count - 1]; - public override uint HeaderHeight => (uint)header_index.Count - 1; - public override uint Height => current_block_height; - public bool VerifyBlocks { get; set; } = true; - - /// - /// Return true if haven't got valid handle - /// - public override bool IsDisposed => disposed; - - public LevelDBBlockchain(string path) - { - header_index.Add(GenesisBlock.Hash); - Version version; - Slice value; - db = DB.Open(path, new Options { CreateIfMissing = true }); - if (db.TryGet(ReadOptions.Default, SliceBuilder.Begin(DataEntryPrefix.SYS_Version), out value) && Version.TryParse(value.ToString(), out version) && version >= Version.Parse("2.6.0")) - { - ReadOptions options = new ReadOptions { FillCache = false }; - value = db.Get(options, SliceBuilder.Begin(DataEntryPrefix.SYS_CurrentBlock)); - UInt256 current_header_hash = new UInt256(value.ToArray().Take(32).ToArray()); - this.current_block_height = value.ToArray().ToUInt32(32); - uint current_header_height = current_block_height; - if (db.TryGet(options, SliceBuilder.Begin(DataEntryPrefix.SYS_CurrentHeader), out value)) - { - current_header_hash = new UInt256(value.ToArray().Take(32).ToArray()); - current_header_height = value.ToArray().ToUInt32(32); - } - foreach (UInt256 hash in db.Find(options, SliceBuilder.Begin(DataEntryPrefix.IX_HeaderHashList), (k, v) => - { - using (MemoryStream ms = new MemoryStream(v.ToArray(), false)) - using (BinaryReader r = new BinaryReader(ms)) - { - return new - { - Index = k.ToArray().ToUInt32(1), - Hashes = r.ReadSerializableArray() - }; - } - }).OrderBy(p => p.Index).SelectMany(p => p.Hashes).ToArray()) - { - if (!hash.Equals(GenesisBlock.Hash)) - { - header_index.Add(hash); - } - stored_header_count++; - } - if (stored_header_count == 0) - { - Header[] headers = db.Find(options, SliceBuilder.Begin(DataEntryPrefix.DATA_Block), (k, v) => Header.FromTrimmedData(v.ToArray(), sizeof(long))).OrderBy(p => p.Index).ToArray(); - for (int i = 1; i < headers.Length; i++) - { - header_index.Add(headers[i].Hash); - } - } - else if (current_header_height >= stored_header_count) - { - for (UInt256 hash = current_header_hash; hash != header_index[(int)stored_header_count - 1];) - { - Header header = Header.FromTrimmedData(db.Get(options, SliceBuilder.Begin(DataEntryPrefix.DATA_Block).Add(hash)).ToArray(), sizeof(long)); - header_index.Insert((int)stored_header_count, hash); - hash = header.PrevHash; - } - } - } - else - { - WriteBatch batch = new WriteBatch(); - ReadOptions options = new ReadOptions { FillCache = false }; - using (Iterator it = db.NewIterator(options)) - { - for (it.SeekToFirst(); it.Valid(); it.Next()) - { - batch.Delete(it.Key()); - } - } - db.Write(WriteOptions.Default, batch); - Persist(GenesisBlock); - db.Put(WriteOptions.Default, SliceBuilder.Begin(DataEntryPrefix.SYS_Version), GetType().GetTypeInfo().Assembly.GetName().Version.ToString()); - } - thread_persistence = new Thread(PersistBlocks); - thread_persistence.Name = "LevelDBBlockchain.PersistBlocks"; - thread_persistence.Start(); - } - - public override bool AddBlock(Block block) - { - lock (block_cache) - { - if (!block_cache.ContainsKey(block.Hash)) - { - block_cache.Add(block.Hash, block); - } - } - lock (header_index) - { - if (block.Index - 1 >= header_index.Count) return false; - if (block.Index == header_index.Count) - { - if (VerifyBlocks && !block.Verify()) return false; - WriteBatch batch = new WriteBatch(); - OnAddHeader(block.Header, batch); - db.Write(WriteOptions.Default, batch); - } - if (block.Index < header_index.Count) - new_block_event.Set(); - } - return true; - } - - protected internal override void AddHeaders(IEnumerable
headers) - { - lock (header_index) - { - lock (header_cache) - { - WriteBatch batch = new WriteBatch(); - foreach (Header header in headers) - { - if (header.Index - 1 >= header_index.Count) break; - if (header.Index < header_index.Count) continue; - if (VerifyBlocks && !header.Verify()) break; - OnAddHeader(header, batch); - header_cache.Add(header.Hash, header); - } - db.Write(WriteOptions.Default, batch); - header_cache.Clear(); - } - } - } - - public override bool ContainsBlock(UInt256 hash) - { - return GetHeader(hash)?.Index <= current_block_height; - } - - public override bool ContainsTransaction(UInt256 hash) - { - Slice value; - return db.TryGet(ReadOptions.Default, SliceBuilder.Begin(DataEntryPrefix.DATA_Transaction).Add(hash), out value); - } - - public override bool ContainsUnspent(UInt256 hash, ushort index) - { - UnspentCoinState state = db.TryGet(ReadOptions.Default, DataEntryPrefix.ST_Coin, hash); - if (state == null) return false; - if (index >= state.Items.Length) return false; - return !state.Items[index].HasFlag(CoinState.Spent); - } - - public override void Dispose() - { - disposed = true; - new_block_event.Set(); - if (!thread_persistence.ThreadState.HasFlag(ThreadState.Unstarted)) - thread_persistence.Join(); - new_block_event.Dispose(); - if (db != null) - { - db.Dispose(); - db = null; - } - } - - public override AccountState GetAccountState(UInt160 script_hash) - { - return db.TryGet(ReadOptions.Default, DataEntryPrefix.ST_Account, script_hash); - } - - public override AssetState GetAssetState(UInt256 asset_id) - { - return db.TryGet(ReadOptions.Default, DataEntryPrefix.ST_Asset, asset_id); - } - - public override Block GetBlock(UInt256 hash) - { - return GetBlockInternal(ReadOptions.Default, hash); - } - - public override UInt256 GetBlockHash(uint height) - { - if (current_block_height < height) return null; - lock (header_index) - { - if (header_index.Count <= height) return null; - return header_index[(int)height]; - } - } - - private Block GetBlockInternal(ReadOptions options, UInt256 hash) - { - Slice value; - if (!db.TryGet(options, SliceBuilder.Begin(DataEntryPrefix.DATA_Block).Add(hash), out value)) - return null; - int height; - Block block = Block.FromTrimmedData(value.ToArray(), sizeof(long), p => GetTransaction(options, p, out height)); - if (block.Transactions.Length == 0) return null; - return block; - } - - public override ContractState GetContract(UInt160 hash) - { - return db.TryGet(ReadOptions.Default, DataEntryPrefix.ST_Contract, hash); - } - - public override IEnumerable GetEnrollments() - { - HashSet sv = new HashSet(StandbyValidators); - return db.Find(ReadOptions.Default, DataEntryPrefix.ST_Validator).Where(p => (p.Registered && p.Votes > Fixed8.Zero) || sv.Contains(p.PublicKey)); - } - - public override Header GetHeader(uint height) - { - UInt256 hash; - lock (header_index) - { - if (header_index.Count <= height) return null; - hash = header_index[(int)height]; - } - return GetHeader(hash); - } - - public override Header GetHeader(UInt256 hash) - { - lock (header_cache) - { - if (header_cache.TryGetValue(hash, out Header header)) - return header; - } - Slice value; - if (!db.TryGet(ReadOptions.Default, SliceBuilder.Begin(DataEntryPrefix.DATA_Block).Add(hash), out value)) - return null; - return Header.FromTrimmedData(value.ToArray(), sizeof(long)); - } - - public override Block GetNextBlock(UInt256 hash) - { - return GetBlockInternal(ReadOptions.Default, GetNextBlockHash(hash)); - } - - public override UInt256 GetNextBlockHash(UInt256 hash) - { - Header header = GetHeader(hash); - if (header == null) return null; - lock (header_index) - { - if (header.Index + 1 >= header_index.Count) - return null; - return header_index[(int)header.Index + 1]; - } - } - - public override StorageItem GetStorageItem(StorageKey key) - { - return db.TryGet(ReadOptions.Default, DataEntryPrefix.ST_Storage, key); - } - - public override long GetSysFeeAmount(UInt256 hash) - { - Slice value; - if (!db.TryGet(ReadOptions.Default, SliceBuilder.Begin(DataEntryPrefix.DATA_Block).Add(hash), out value)) - return 0; - return value.ToArray().ToInt64(0); - } - - public override MetaDataCache GetMetaData() - { - Type t = typeof(T); - if (t == typeof(ValidatorsCountState)) return new DbMetaDataCache(db, DataEntryPrefix.IX_ValidatorsCount); - throw new NotSupportedException(); - } - - public override DataCache GetStates() - { - Type t = typeof(TValue); - if (t == typeof(AccountState)) return new DbCache(db, DataEntryPrefix.ST_Account); - if (t == typeof(UnspentCoinState)) return new DbCache(db, DataEntryPrefix.ST_Coin); - if (t == typeof(SpentCoinState)) return new DbCache(db, DataEntryPrefix.ST_SpentCoin); - if (t == typeof(ValidatorState)) return new DbCache(db, DataEntryPrefix.ST_Validator); - if (t == typeof(AssetState)) return new DbCache(db, DataEntryPrefix.ST_Asset); - if (t == typeof(ContractState)) return new DbCache(db, DataEntryPrefix.ST_Contract); - if (t == typeof(StorageItem)) return new DbCache(db, DataEntryPrefix.ST_Storage); - throw new NotSupportedException(); - } - - public override Transaction GetTransaction(UInt256 hash, out int height) - { - return GetTransaction(ReadOptions.Default, hash, out height); - } - - private Transaction GetTransaction(ReadOptions options, UInt256 hash, out int height) - { - Slice value; - if (db.TryGet(options, SliceBuilder.Begin(DataEntryPrefix.DATA_Transaction).Add(hash), out value)) - { - byte[] data = value.ToArray(); - height = data.ToInt32(0); - return Transaction.DeserializeFrom(data, sizeof(uint)); - } - else - { - height = -1; - return null; - } - } - - public override Dictionary GetUnclaimed(UInt256 hash) - { - int height; - Transaction tx = GetTransaction(ReadOptions.Default, hash, out height); - if (tx == null) return null; - SpentCoinState state = db.TryGet(ReadOptions.Default, DataEntryPrefix.ST_SpentCoin, hash); - if (state != null) - { - return state.Items.ToDictionary(p => p.Key, p => new SpentCoin - { - Output = tx.Outputs[p.Key], - StartHeight = (uint)height, - EndHeight = p.Value - }); - } - else - { - return new Dictionary(); - } - } - - public override TransactionOutput GetUnspent(UInt256 hash, ushort index) - { - ReadOptions options = new ReadOptions(); - using (options.Snapshot = db.GetSnapshot()) - { - UnspentCoinState state = db.TryGet(options, DataEntryPrefix.ST_Coin, hash); - if (state == null) return null; - if (index >= state.Items.Length) return null; - if (state.Items[index].HasFlag(CoinState.Spent)) return null; - int height; - return GetTransaction(options, hash, out height).Outputs[index]; - } - } - - public override IEnumerable GetUnspent(UInt256 hash) - { - ReadOptions options = new ReadOptions(); - using (options.Snapshot = db.GetSnapshot()) - { - List outputs = new List(); - UnspentCoinState state = db.TryGet(options, DataEntryPrefix.ST_Coin, hash); - if (state != null) - { - int height; - Transaction tx = GetTransaction(options, hash, out height); - for (int i = 0; i < state.Items.Length; i++) - { - if (!state.Items[i].HasFlag(CoinState.Spent)) - { - outputs.Add(tx.Outputs[i]); - } - - } - } - return outputs; - } - } - - public override bool IsDoubleSpend(Transaction tx) - { - if (tx.Inputs.Length == 0) return false; - ReadOptions options = new ReadOptions(); - using (options.Snapshot = db.GetSnapshot()) - { - foreach (var group in tx.Inputs.GroupBy(p => p.PrevHash)) - { - UnspentCoinState state = db.TryGet(options, DataEntryPrefix.ST_Coin, group.Key); - if (state == null) return true; - if (group.Any(p => p.PrevIndex >= state.Items.Length || state.Items[p.PrevIndex].HasFlag(CoinState.Spent))) - return true; - } - } - return false; - } - - private void OnAddHeader(Header header, WriteBatch batch) - { - header_index.Add(header.Hash); - while ((int)header.Index - 2000 >= stored_header_count) - { - using (MemoryStream ms = new MemoryStream()) - using (BinaryWriter w = new BinaryWriter(ms)) - { - w.Write(header_index.Skip((int)stored_header_count).Take(2000).ToArray()); - w.Flush(); - batch.Put(SliceBuilder.Begin(DataEntryPrefix.IX_HeaderHashList).Add(stored_header_count), ms.ToArray()); - } - stored_header_count += 2000; - } - batch.Put(SliceBuilder.Begin(DataEntryPrefix.DATA_Block).Add(header.Hash), SliceBuilder.Begin().Add(0L).Add(header.ToArray())); - batch.Put(SliceBuilder.Begin(DataEntryPrefix.SYS_CurrentHeader), SliceBuilder.Begin().Add(header.Hash).Add(header.Index)); - } - - private void Persist(Block block) - { - WriteBatch batch = new WriteBatch(); - DbCache accounts = new DbCache(db, DataEntryPrefix.ST_Account, batch); - DbCache unspentcoins = new DbCache(db, DataEntryPrefix.ST_Coin, batch); - DbCache spentcoins = new DbCache(db, DataEntryPrefix.ST_SpentCoin, batch); - DbCache validators = new DbCache(db, DataEntryPrefix.ST_Validator, batch); - DbCache assets = new DbCache(db, DataEntryPrefix.ST_Asset, batch); - DbCache contracts = new DbCache(db, DataEntryPrefix.ST_Contract, batch); - DbCache storages = new DbCache(db, DataEntryPrefix.ST_Storage, batch); - DbMetaDataCache validators_count = new DbMetaDataCache(db, DataEntryPrefix.IX_ValidatorsCount); - long amount_sysfee = GetSysFeeAmount(block.PrevHash) + (long)block.Transactions.Sum(p => p.SystemFee); - batch.Put(SliceBuilder.Begin(DataEntryPrefix.DATA_Block).Add(block.Hash), SliceBuilder.Begin().Add(amount_sysfee).Add(block.Trim())); - foreach (Transaction tx in block.Transactions) - { - batch.Put(SliceBuilder.Begin(DataEntryPrefix.DATA_Transaction).Add(tx.Hash), SliceBuilder.Begin().Add(block.Index).Add(tx.ToArray())); - unspentcoins.Add(tx.Hash, new UnspentCoinState - { - Items = Enumerable.Repeat(CoinState.Confirmed, tx.Outputs.Length).ToArray() - }); - foreach (TransactionOutput output in tx.Outputs) - { - AccountState account = accounts.GetAndChange(output.ScriptHash, () => new AccountState(output.ScriptHash)); - if (account.Balances.ContainsKey(output.AssetId)) - account.Balances[output.AssetId] += output.Value; - else - account.Balances[output.AssetId] = output.Value; - if (output.AssetId.Equals(GoverningToken.Hash) && account.Votes.Length > 0) - { - foreach (ECPoint pubkey in account.Votes) - validators.GetAndChange(pubkey, () => new ValidatorState(pubkey)).Votes += output.Value; - validators_count.GetAndChange().Votes[account.Votes.Length - 1] += output.Value; - } - } - foreach (var group in tx.Inputs.GroupBy(p => p.PrevHash)) - { - Transaction tx_prev = GetTransaction(ReadOptions.Default, group.Key, out int height); - foreach (CoinReference input in group) - { - unspentcoins.GetAndChange(input.PrevHash).Items[input.PrevIndex] |= CoinState.Spent; - TransactionOutput out_prev = tx_prev.Outputs[input.PrevIndex]; - AccountState account = accounts.GetAndChange(out_prev.ScriptHash); - if (out_prev.AssetId.Equals(GoverningToken.Hash)) - { - spentcoins.GetAndChange(input.PrevHash, () => new SpentCoinState - { - TransactionHash = input.PrevHash, - TransactionHeight = (uint)height, - Items = new Dictionary() - }).Items.Add(input.PrevIndex, block.Index); - if (account.Votes.Length > 0) - { - foreach (ECPoint pubkey in account.Votes) - { - ValidatorState validator = validators.GetAndChange(pubkey); - validator.Votes -= out_prev.Value; - if (!validator.Registered && validator.Votes.Equals(Fixed8.Zero)) - validators.Delete(pubkey); - } - validators_count.GetAndChange().Votes[account.Votes.Length - 1] -= out_prev.Value; - } - } - account.Balances[out_prev.AssetId] -= out_prev.Value; - } - } - switch (tx) - { -#pragma warning disable CS0612 - case RegisterTransaction tx_register: - assets.Add(tx.Hash, new AssetState - { - AssetId = tx_register.Hash, - AssetType = tx_register.AssetType, - Name = tx_register.Name, - Amount = tx_register.Amount, - Available = Fixed8.Zero, - Precision = tx_register.Precision, - Fee = Fixed8.Zero, - FeeAddress = new UInt160(), - Owner = tx_register.Owner, - Admin = tx_register.Admin, - Issuer = tx_register.Admin, - Expiration = block.Index + 2 * 2000000, - IsFrozen = false - }); - break; -#pragma warning restore CS0612 - case IssueTransaction _: - foreach (TransactionResult result in tx.GetTransactionResults().Where(p => p.Amount < Fixed8.Zero)) - assets.GetAndChange(result.AssetId).Available -= result.Amount; - break; - case ClaimTransaction _: - foreach (CoinReference input in ((ClaimTransaction)tx).Claims) - { - if (spentcoins.TryGet(input.PrevHash)?.Items.Remove(input.PrevIndex) == true) - spentcoins.GetAndChange(input.PrevHash); - } - break; -#pragma warning disable CS0612 - case EnrollmentTransaction tx_enrollment: - validators.GetAndChange(tx_enrollment.PublicKey, () => new ValidatorState(tx_enrollment.PublicKey)).Registered = true; - break; -#pragma warning restore CS0612 - case StateTransaction tx_state: - foreach (StateDescriptor descriptor in tx_state.Descriptors) - switch (descriptor.Type) - { - case StateType.Account: - ProcessAccountStateDescriptor(descriptor, accounts, validators, validators_count); - break; - case StateType.Validator: - ProcessValidatorStateDescriptor(descriptor, validators); - break; - } - break; -#pragma warning disable CS0612 - case PublishTransaction tx_publish: - contracts.GetOrAdd(tx_publish.ScriptHash, () => new ContractState - { - Script = tx_publish.Script, - ParameterList = tx_publish.ParameterList, - ReturnType = tx_publish.ReturnType, - ContractProperties = (ContractPropertyState)Convert.ToByte(tx_publish.NeedStorage), - Name = tx_publish.Name, - CodeVersion = tx_publish.CodeVersion, - Author = tx_publish.Author, - Email = tx_publish.Email, - Description = tx_publish.Description - }); - break; -#pragma warning restore CS0612 - case InvocationTransaction tx_invocation: - CachedScriptTable script_table = new CachedScriptTable(contracts); - using (StateMachine service = new StateMachine(block, accounts, assets, contracts, storages)) - { - ApplicationEngine engine = new ApplicationEngine(TriggerType.Application, tx_invocation, script_table, service, tx_invocation.Gas); - engine.LoadScript(tx_invocation.Script, false); - if (engine.Execute()) - { - service.Commit(); - } - ApplicationExecuted?.Invoke(this, new ApplicationExecutedEventArgs(tx_invocation, service.Notifications.ToArray(), engine)); - } - break; - } - } - accounts.DeleteWhere((k, v) => !v.IsFrozen && v.Votes.Length == 0 && v.Balances.All(p => p.Value <= Fixed8.Zero)); - accounts.Commit(); - unspentcoins.DeleteWhere((k, v) => v.Items.All(p => p.HasFlag(CoinState.Spent))); - unspentcoins.Commit(); - spentcoins.DeleteWhere((k, v) => v.Items.Count == 0); - spentcoins.Commit(); - validators.Commit(); - assets.Commit(); - contracts.Commit(); - storages.Commit(); - validators_count.Commit(batch); - batch.Put(SliceBuilder.Begin(DataEntryPrefix.SYS_CurrentBlock), SliceBuilder.Begin().Add(block.Hash).Add(block.Index)); - db.Write(WriteOptions.Default, batch); - current_block_height = block.Index; - } - - private void PersistBlocks() - { - while (!disposed) - { - new_block_event.WaitOne(); - while (!disposed) - { - UInt256 hash; - lock (header_index) - { - if (header_index.Count <= current_block_height + 1) break; - hash = header_index[(int)current_block_height + 1]; - } - Block block; - lock (block_cache) - { - if (!block_cache.TryGetValue(hash, out block)) - break; - } - Persist(block); - OnPersistCompleted(block); - lock (block_cache) - { - block_cache.Remove(hash); - } - } - } - } - } -} diff --git a/neo/Implementations/Wallets/EntityFramework/Account.cs b/neo/Implementations/Wallets/EntityFramework/Account.cs deleted file mode 100644 index 009a6ed961..0000000000 --- a/neo/Implementations/Wallets/EntityFramework/Account.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Neo.Implementations.Wallets.EntityFramework -{ - internal class Account - { - public byte[] PrivateKeyEncrypted { get; set; } - public byte[] PublicKeyHash { get; set; } - } -} diff --git a/neo/Implementations/Wallets/EntityFramework/Address.cs b/neo/Implementations/Wallets/EntityFramework/Address.cs deleted file mode 100644 index 63cf54cfaa..0000000000 --- a/neo/Implementations/Wallets/EntityFramework/Address.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Neo.Implementations.Wallets.EntityFramework -{ - internal class Address - { - public byte[] ScriptHash { get; set; } - } -} diff --git a/neo/Implementations/Wallets/EntityFramework/Contract.cs b/neo/Implementations/Wallets/EntityFramework/Contract.cs deleted file mode 100644 index f236887921..0000000000 --- a/neo/Implementations/Wallets/EntityFramework/Contract.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Neo.Implementations.Wallets.EntityFramework -{ - internal 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/neo/Implementations/Wallets/EntityFramework/Key.cs b/neo/Implementations/Wallets/EntityFramework/Key.cs deleted file mode 100644 index 21c83e68cd..0000000000 --- a/neo/Implementations/Wallets/EntityFramework/Key.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Neo.Implementations.Wallets.EntityFramework -{ - internal class Key - { - public string Name { get; set; } - public byte[] Value { get; set; } - } -} diff --git a/neo/Implementations/Wallets/EntityFramework/UserWallet.cs b/neo/Implementations/Wallets/EntityFramework/UserWallet.cs deleted file mode 100644 index 8dee587bfc..0000000000 --- a/neo/Implementations/Wallets/EntityFramework/UserWallet.cs +++ /dev/null @@ -1,502 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Neo.Core; -using Neo.Cryptography; -using Neo.IO; -using Neo.SmartContract; -using Neo.Wallets; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Security; -using System.Security.Cryptography; - -namespace Neo.Implementations.Wallets.EntityFramework -{ - public class UserWallet : Wallet, IDisposable - { - public override event EventHandler BalanceChanged; - - private readonly string path; - private readonly byte[] iv; - private readonly byte[] masterKey; - private readonly Dictionary accounts; - private readonly Dictionary unconfirmed = new Dictionary(); - - public override string Name => Path.GetFileNameWithoutExtension(path); - public override uint WalletHeight => WalletIndexer.IndexHeight; - - public override Version Version - { - get - { - byte[] buffer = LoadStoredData("Version"); - if (buffer == null) return new Version(0, 0); - int major = buffer.ToInt32(0); - int minor = buffer.ToInt32(4); - int build = buffer.ToInt32(8); - int revision = buffer.ToInt32(12); - return new Version(major, minor, build, revision); - } - } - - private UserWallet(string path, byte[] passwordKey, bool create) - { - this.path = path; - if (create) - { - this.iv = new byte[16]; - this.masterKey = new byte[32]; - this.accounts = new Dictionary(); - using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) - { - rng.GetBytes(iv); - rng.GetBytes(masterKey); - } - Version version = Assembly.GetExecutingAssembly().GetName().Version; - BuildDatabase(); - SaveStoredData("PasswordHash", passwordKey.Sha256()); - SaveStoredData("IV", iv); - SaveStoredData("MasterKey", masterKey.AesEncrypt(passwordKey, iv)); - SaveStoredData("Version", new[] { version.Major, version.Minor, version.Build, version.Revision }.Select(p => BitConverter.GetBytes(p)).SelectMany(p => p).ToArray()); -#if NET47 - ProtectedMemory.Protect(masterKey, MemoryProtectionScope.SameProcess); -#endif - } - else - { - byte[] passwordHash = LoadStoredData("PasswordHash"); - if (passwordHash != null && !passwordHash.SequenceEqual(passwordKey.Sha256())) - throw new CryptographicException(); - this.iv = LoadStoredData("IV"); - this.masterKey = LoadStoredData("MasterKey").AesDecrypt(passwordKey, iv); -#if NET47 - ProtectedMemory.Protect(masterKey, MemoryProtectionScope.SameProcess); -#endif - this.accounts = LoadAccounts(); - WalletIndexer.RegisterAccounts(accounts.Keys); - } - WalletIndexer.BalanceChanged += WalletIndexer_BalanceChanged; - } - - private void AddAccount(UserWalletAccount account, bool is_import) - { - lock (accounts) - { - if (accounts.TryGetValue(account.ScriptHash, out UserWalletAccount account_old)) - { - if (account.Contract == null) - { - account.Contract = account_old.Contract; - } - } - else - { - WalletIndexer.RegisterAccounts(new[] { account.ScriptHash }, is_import ? 0 : Blockchain.Default?.Height ?? 0); - } - accounts[account.ScriptHash] = account; - } - using (WalletDataContext ctx = new WalletDataContext(path)) - { - if (account.HasKey) - { - byte[] decryptedPrivateKey = new byte[96]; - Buffer.BlockCopy(account.Key.PublicKey.EncodePoint(false), 1, decryptedPrivateKey, 0, 64); - using (account.Key.Decrypt()) - { - Buffer.BlockCopy(account.Key.PrivateKey, 0, decryptedPrivateKey, 64, 32); - } - byte[] encryptedPrivateKey = EncryptPrivateKey(decryptedPrivateKey); - Array.Clear(decryptedPrivateKey, 0, decryptedPrivateKey.Length); - Account db_account = ctx.Accounts.FirstOrDefault(p => p.PublicKeyHash.SequenceEqual(account.Key.PublicKeyHash.ToArray())); - if (db_account == null) - { - db_account = ctx.Accounts.Add(new Account - { - PrivateKeyEncrypted = encryptedPrivateKey, - PublicKeyHash = account.Key.PublicKeyHash.ToArray() - }).Entity; - } - else - { - db_account.PrivateKeyEncrypted = encryptedPrivateKey; - } - } - if (account.Contract != null) - { - Contract db_contract = ctx.Contracts.FirstOrDefault(p => p.ScriptHash.SequenceEqual(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.SequenceEqual(account.Contract.ScriptHash.ToArray())); - if (db_address == null) - { - ctx.Addresses.Add(new Address - { - ScriptHash = account.Contract.ScriptHash.ToArray() - }); - } - } - ctx.SaveChanges(); - } - } - - public override void ApplyTransaction(Transaction tx) - { - lock (unconfirmed) - { - unconfirmed[tx.Hash] = tx; - } - BalanceChanged?.Invoke(this, new BalanceEventArgs - { - Transaction = tx, - RelatedAccounts = tx.Scripts.Select(p => p.ScriptHash).Union(tx.Outputs.Select(p => p.ScriptHash)).Where(p => Contains(p)).ToArray(), - Height = null, - Time = DateTime.UtcNow.ToTimestamp() - }); - } - - private void BuildDatabase() - { - using (WalletDataContext ctx = new WalletDataContext(path)) - { - ctx.Database.EnsureDeleted(); - ctx.Database.EnsureCreated(); - } - } - - public bool ChangePassword(string password_old, string password_new) - { - if (!VerifyPassword(password_old)) return false; - byte[] passwordKey = password_new.ToAesKey(); -#if NET47 - using (new ProtectedMemoryContext(masterKey, MemoryProtectionScope.SameProcess)) -#endif - { - try - { - SaveStoredData("PasswordHash", passwordKey.Sha256()); - SaveStoredData("MasterKey", masterKey.AesEncrypt(passwordKey, iv)); - return true; - } - finally - { - Array.Clear(passwordKey, 0, passwordKey.Length); - } - } - } - - public override bool Contains(UInt160 scriptHash) - { - lock (accounts) - { - return accounts.ContainsKey(scriptHash); - } - } - - public static UserWallet Create(string path, string password) - { - return new UserWallet(path, password.ToAesKey(), true); - } - - public static UserWallet Create(string path, SecureString password) - { - return new UserWallet(path, password.ToAesKey(), true); - } - - public override WalletAccount CreateAccount(byte[] privateKey) - { - KeyPair key = new KeyPair(privateKey); - VerificationContract contract = new VerificationContract - { - Script = SmartContract.Contract.CreateSignatureRedeemScript(key.PublicKey), - ParameterList = new[] { ContractParameterType.Signature } - }; - UserWalletAccount account = new UserWalletAccount(contract.ScriptHash) - { - Key = key, - Contract = contract - }; - AddAccount(account, false); - return account; - } - - public override WalletAccount CreateAccount(SmartContract.Contract contract, KeyPair key = null) - { - VerificationContract verification_contract = contract as VerificationContract; - if (verification_contract == null) - { - verification_contract = new VerificationContract - { - Script = contract.Script, - ParameterList = contract.ParameterList - }; - } - UserWalletAccount account = new UserWalletAccount(verification_contract.ScriptHash) - { - Key = key, - Contract = verification_contract - }; - AddAccount(account, false); - return account; - } - - public override WalletAccount CreateAccount(UInt160 scriptHash) - { - UserWalletAccount account = new UserWalletAccount(scriptHash); - AddAccount(account, true); - return account; - } - - private byte[] DecryptPrivateKey(byte[] encryptedPrivateKey) - { - if (encryptedPrivateKey == null) throw new ArgumentNullException(nameof(encryptedPrivateKey)); - if (encryptedPrivateKey.Length != 96) throw new ArgumentException(); -#if NET47 - using (new ProtectedMemoryContext(masterKey, MemoryProtectionScope.SameProcess)) -#endif - { - return encryptedPrivateKey.AesDecrypt(masterKey, iv); - } - } - - public override bool DeleteAccount(UInt160 scriptHash) - { - UserWalletAccount account; - lock (accounts) - { - if (accounts.TryGetValue(scriptHash, out account)) - accounts.Remove(scriptHash); - } - if (account != null) - { - WalletIndexer.UnregisterAccounts(new[] { scriptHash }); - using (WalletDataContext ctx = new WalletDataContext(path)) - { - if (account.HasKey) - { - Account db_account = ctx.Accounts.First(p => p.PublicKeyHash.SequenceEqual(account.Key.PublicKeyHash.ToArray())); - ctx.Accounts.Remove(db_account); - } - if (account.Contract != null) - { - Contract db_contract = ctx.Contracts.First(p => p.ScriptHash.SequenceEqual(scriptHash.ToArray())); - ctx.Contracts.Remove(db_contract); - } - //delete address - { - Address db_address = ctx.Addresses.First(p => p.ScriptHash.SequenceEqual(scriptHash.ToArray())); - ctx.Addresses.Remove(db_address); - } - ctx.SaveChanges(); - } - return true; - } - return false; - } - - public void Dispose() - { - WalletIndexer.BalanceChanged -= WalletIndexer_BalanceChanged; - } - - private byte[] EncryptPrivateKey(byte[] decryptedPrivateKey) - { -#if NET47 - using (new ProtectedMemoryContext(masterKey, MemoryProtectionScope.SameProcess)) -#endif - { - return decryptedPrivateKey.AesEncrypt(masterKey, iv); - } - } - - public override Coin[] FindUnspentCoins(UInt256 asset_id, Fixed8 amount, UInt160[] from) - { - return FindUnspentCoins(FindUnspentCoins(from).ToArray().Where(p => GetAccount(p.Output.ScriptHash).Contract.IsStandard), asset_id, amount) ?? base.FindUnspentCoins(asset_id, amount, from); - } - - public override WalletAccount GetAccount(UInt160 scriptHash) - { - lock (accounts) - { - accounts.TryGetValue(scriptHash, out UserWalletAccount account); - return account; - } - } - - public override IEnumerable GetAccounts() - { - lock (accounts) - { - foreach (UserWalletAccount account in accounts.Values) - yield return account; - } - } - - public override IEnumerable GetCoins(IEnumerable accounts) - { - if (unconfirmed.Count == 0) - return WalletIndexer.GetCoins(accounts); - else - return GetCoinsInternal(); - IEnumerable GetCoinsInternal() - { - HashSet inputs, claims; - Coin[] coins_unconfirmed; - lock (unconfirmed) - { - inputs = new HashSet(unconfirmed.Values.SelectMany(p => p.Inputs)); - claims = new HashSet(unconfirmed.Values.OfType().SelectMany(p => p.Claims)); - coins_unconfirmed = unconfirmed.Values.Select(tx => tx.Outputs.Select((o, i) => new Coin - { - Reference = new CoinReference - { - PrevHash = tx.Hash, - PrevIndex = (ushort)i - }, - Output = o, - State = CoinState.Unconfirmed - })).SelectMany(p => p).ToArray(); - } - foreach (Coin coin in WalletIndexer.GetCoins(accounts)) - { - if (inputs.Contains(coin.Reference)) - { - if (coin.Output.AssetId.Equals(Blockchain.GoverningToken.Hash)) - yield return new Coin - { - Reference = coin.Reference, - Output = coin.Output, - State = coin.State | CoinState.Spent - }; - continue; - } - else if (claims.Contains(coin.Reference)) - { - continue; - } - yield return coin; - } - HashSet accounts_set = new HashSet(accounts); - foreach (Coin coin in coins_unconfirmed) - { - if (accounts_set.Contains(coin.Output.ScriptHash)) - yield return coin; - } - } - } - - public override IEnumerable GetTransactions() - { - foreach (UInt256 hash in WalletIndexer.GetTransactions(accounts.Keys)) - yield return hash; - lock (unconfirmed) - { - foreach (UInt256 hash in unconfirmed.Keys) - yield return hash; - } - } - - private Dictionary LoadAccounts() - { - using (WalletDataContext ctx = new WalletDataContext(path)) - { - Dictionary accounts = ctx.Addresses.Select(p => p.ScriptHash).AsEnumerable().Select(p => new UserWalletAccount(new UInt160(p))).ToDictionary(p => p.ScriptHash); - foreach (Contract db_contract in ctx.Contracts.Include(p => p.Account)) - { - VerificationContract contract = db_contract.RawData.AsSerializable(); - UserWalletAccount account = accounts[contract.ScriptHash]; - account.Contract = contract; - account.Key = new KeyPair(DecryptPrivateKey(db_contract.Account.PrivateKeyEncrypted)); - } - return accounts; - } - } - - private byte[] LoadStoredData(string name) - { - using (WalletDataContext ctx = new WalletDataContext(path)) - { - return ctx.Keys.FirstOrDefault(p => p.Name == name)?.Value; - } - } - - public static UserWallet Open(string path, string password) - { - return new UserWallet(path, password.ToAesKey(), false); - } - - public static UserWallet Open(string path, SecureString password) - { - return new UserWallet(path, password.ToAesKey(), false); - } - - private void SaveStoredData(string name, byte[] value) - { - using (WalletDataContext ctx = new WalletDataContext(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 password.ToAesKey().Sha256().SequenceEqual(LoadStoredData("PasswordHash")); - } - - private void WalletIndexer_BalanceChanged(object sender, BalanceEventArgs e) - { - lock (unconfirmed) - { - unconfirmed.Remove(e.Transaction.Hash); - } - UInt160[] relatedAccounts; - lock (accounts) - { - relatedAccounts = e.RelatedAccounts.Where(p => accounts.ContainsKey(p)).ToArray(); - } - if (relatedAccounts.Length > 0) - { - BalanceChanged?.Invoke(this, new BalanceEventArgs - { - Transaction = e.Transaction, - RelatedAccounts = relatedAccounts, - Height = e.Height, - Time = e.Time - }); - } - } - } -} diff --git a/neo/Implementations/Wallets/EntityFramework/UserWalletAccount.cs b/neo/Implementations/Wallets/EntityFramework/UserWalletAccount.cs deleted file mode 100644 index 9b5609fc04..0000000000 --- a/neo/Implementations/Wallets/EntityFramework/UserWalletAccount.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Neo.Wallets; - -namespace Neo.Implementations.Wallets.EntityFramework -{ - internal class UserWalletAccount : WalletAccount - { - public KeyPair Key; - - public override bool HasKey => Key != null; - - public UserWalletAccount(UInt160 scriptHash) - : base(scriptHash) - { - } - - public override KeyPair GetKey() - { - return Key; - } - } -} diff --git a/neo/Implementations/Wallets/EntityFramework/VerificationContract.cs b/neo/Implementations/Wallets/EntityFramework/VerificationContract.cs deleted file mode 100644 index c010d75eda..0000000000 --- a/neo/Implementations/Wallets/EntityFramework/VerificationContract.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Neo.IO; -using Neo.SmartContract; -using Neo.VM; -using System; -using System.IO; -using System.Linq; - -namespace Neo.Implementations.Wallets.EntityFramework -{ - public class VerificationContract : SmartContract.Contract, IEquatable, ISerializable - { - public int Size => 20 + ParameterList.GetVarSize() + Script.GetVarSize(); - - /// - /// 反序列化 - /// - /// 数据来源 - public void Deserialize(BinaryReader reader) - { - reader.ReadSerializable(); - ParameterList = reader.ReadVarBytes().Select(p => (ContractParameterType)p).ToArray(); - Script = reader.ReadVarBytes(); - } - - /// - /// 比较与另一个对象是否相等 - /// - /// 另一个对象 - /// 返回比较的结果 - public bool Equals(VerificationContract other) - { - if (ReferenceEquals(this, other)) return true; - if (ReferenceEquals(null, other)) return false; - return ScriptHash.Equals(other.ScriptHash); - } - - /// - /// 比较与另一个对象是否相等 - /// - /// 另一个对象 - /// 返回比较的结果 - public override bool Equals(object obj) - { - return Equals(obj as VerificationContract); - } - - /// - /// 获得HashCode - /// - /// 返回HashCode - public override int GetHashCode() - { - return ScriptHash.GetHashCode(); - } - - /// - /// 序列化 - /// - /// 存放序列化后的结果 - public void Serialize(BinaryWriter writer) - { - writer.Write(new UInt160()); - writer.WriteVarBytes(ParameterList.Select(p => (byte)p).ToArray()); - writer.WriteVarBytes(Script); - } - } -} diff --git a/neo/Implementations/Wallets/EntityFramework/WalletDataContext.cs b/neo/Implementations/Wallets/EntityFramework/WalletDataContext.cs deleted file mode 100644 index 0befc66d3a..0000000000 --- a/neo/Implementations/Wallets/EntityFramework/WalletDataContext.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; - -namespace Neo.Implementations.Wallets.EntityFramework -{ - internal 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 SqliteConnectionStringBuilder(); - sb.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.PrivateKeyEncrypted).HasColumnType("VarBinary").HasMaxLength(96).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/neo/Implementations/Wallets/NEP6/NEP6Account.cs b/neo/Implementations/Wallets/NEP6/NEP6Account.cs deleted file mode 100644 index 201b2bf30b..0000000000 --- a/neo/Implementations/Wallets/NEP6/NEP6Account.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Neo.IO.Json; -using Neo.Wallets; -using System; - -namespace Neo.Implementations.Wallets.NEP6 -{ - internal class NEP6Account : WalletAccount - { - private readonly NEP6Wallet wallet; - private readonly string nep2key; - private KeyPair key; - public JObject Extra; - - public bool Decrypted => nep2key == null || key != null; - public override bool HasKey => nep2key != null; - - public NEP6Account(NEP6Wallet wallet, UInt160 scriptHash, string nep2key = null) - : base(scriptHash) - { - this.wallet = wallet; - this.nep2key = nep2key; - } - - public NEP6Account(NEP6Wallet wallet, UInt160 scriptHash, KeyPair key, string password) - : this(wallet, scriptHash, key.Export(password, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P)) - { - this.key = key; - } - - public static NEP6Account FromJson(JObject json, NEP6Wallet wallet) - { - return new NEP6Account(wallet, Wallet.ToScriptHash(json["address"].AsString()), json["key"]?.AsString()) - { - Label = json["label"]?.AsString(), - IsDefault = json["isDefault"].AsBoolean(), - Lock = json["lock"].AsBoolean(), - Contract = NEP6Contract.FromJson(json["contract"]), - Extra = json["extra"] - }; - } - - public override KeyPair GetKey() - { - if (nep2key == null) return null; - if (key == null) - { - key = wallet.DecryptKey(nep2key); - } - return key; - } - - public KeyPair GetKey(string password) - { - if (nep2key == null) return null; - if (key == null) - { - key = new KeyPair(Wallet.GetPrivateKeyFromNEP2(nep2key, password, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P)); - } - return key; - } - - public JObject ToJson() - { - JObject account = new JObject(); - account["address"] = Wallet.ToAddress(ScriptHash); - account["label"] = Label; - account["isDefault"] = IsDefault; - account["lock"] = Lock; - account["key"] = nep2key; - account["contract"] = ((NEP6Contract)Contract)?.ToJson(); - account["extra"] = Extra; - return account; - } - - public bool VerifyPassword(string password) - { - try - { - Wallet.GetPrivateKeyFromNEP2(nep2key, password, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P); - return true; - } - catch (FormatException) - { - return false; - } - } - } -} diff --git a/neo/Implementations/Wallets/NEP6/NEP6Contract.cs b/neo/Implementations/Wallets/NEP6/NEP6Contract.cs deleted file mode 100644 index 2f2f096e1d..0000000000 --- a/neo/Implementations/Wallets/NEP6/NEP6Contract.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Neo.IO.Json; -using Neo.SmartContract; -using System.Linq; - -namespace Neo.Implementations.Wallets.NEP6 -{ - internal class NEP6Contract : Contract - { - public string[] ParameterNames; - public bool Deployed; - - public static NEP6Contract FromJson(JObject json) - { - if (json == null) return null; - return new NEP6Contract - { - Script = json["script"].AsString().HexToBytes(), - ParameterList = ((JArray)json["parameters"]).Select(p => p["type"].AsEnum()).ToArray(), - ParameterNames = ((JArray)json["parameters"]).Select(p => p["name"].AsString()).ToArray(), - Deployed = json["deployed"].AsBoolean() - }; - } - - public JObject ToJson() - { - JObject contract = new JObject(); - contract["script"] = Script.ToHexString(); - contract["parameters"] = new JArray(ParameterList.Zip(ParameterNames, (type, name) => - { - JObject parameter = new JObject(); - parameter["name"] = name; - parameter["type"] = type; - return parameter; - })); - contract["deployed"] = Deployed; - return contract; - } - } -} diff --git a/neo/Implementations/Wallets/NEP6/NEP6Wallet.cs b/neo/Implementations/Wallets/NEP6/NEP6Wallet.cs deleted file mode 100644 index db2c79be4f..0000000000 --- a/neo/Implementations/Wallets/NEP6/NEP6Wallet.cs +++ /dev/null @@ -1,427 +0,0 @@ -using Neo.Core; -using Neo.IO.Json; -using Neo.SmartContract; -using Neo.Wallets; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using UserWallet = Neo.Implementations.Wallets.EntityFramework.UserWallet; - -namespace Neo.Implementations.Wallets.NEP6 -{ - public class NEP6Wallet : Wallet, IDisposable - { - public override event EventHandler BalanceChanged; - - private readonly string path; - private string password; - private string name; - private Version version; - public readonly ScryptParameters Scrypt; - private readonly Dictionary accounts; - private readonly JObject extra; - private readonly Dictionary unconfirmed = new Dictionary(); - - public override string Name => name; - public override Version Version => version; - public override uint WalletHeight => WalletIndexer.IndexHeight; - - public NEP6Wallet(string path, string name = null) - { - this.path = path; - if (File.Exists(path)) - { - JObject wallet; - using (StreamReader reader = new StreamReader(path)) - { - wallet = JObject.Parse(reader); - } - this.name = wallet["name"]?.AsString(); - this.version = Version.Parse(wallet["version"].AsString()); - this.Scrypt = ScryptParameters.FromJson(wallet["scrypt"]); - this.accounts = ((JArray)wallet["accounts"]).Select(p => NEP6Account.FromJson(p, this)).ToDictionary(p => p.ScriptHash); - this.extra = wallet["extra"]; - WalletIndexer.RegisterAccounts(accounts.Keys); - } - else - { - this.name = name; - this.version = Version.Parse("1.0"); - this.Scrypt = ScryptParameters.Default; - this.accounts = new Dictionary(); - this.extra = JObject.Null; - } - WalletIndexer.BalanceChanged += WalletIndexer_BalanceChanged; - } - - private void AddAccount(NEP6Account account, bool is_import) - { - lock (accounts) - { - if (accounts.TryGetValue(account.ScriptHash, out NEP6Account account_old)) - { - account.Label = account_old.Label; - account.IsDefault = account_old.IsDefault; - account.Lock = account_old.Lock; - if (account.Contract == null) - { - account.Contract = account_old.Contract; - } - else - { - NEP6Contract contract_old = (NEP6Contract)account_old.Contract; - if (contract_old != null) - { - NEP6Contract contract = (NEP6Contract)account.Contract; - contract.ParameterNames = contract_old.ParameterNames; - contract.Deployed = contract_old.Deployed; - } - } - account.Extra = account_old.Extra; - } - else - { - WalletIndexer.RegisterAccounts(new[] { account.ScriptHash }, is_import ? 0 : Blockchain.Default?.Height ?? 0); - } - accounts[account.ScriptHash] = account; - } - } - - public override void ApplyTransaction(Transaction tx) - { - lock (unconfirmed) - { - unconfirmed[tx.Hash] = tx; - } - BalanceChanged?.Invoke(this, new BalanceEventArgs - { - Transaction = tx, - RelatedAccounts = tx.Scripts.Select(p => p.ScriptHash).Union(tx.Outputs.Select(p => p.ScriptHash)).Where(p => Contains(p)).ToArray(), - Height = null, - Time = DateTime.UtcNow.ToTimestamp() - }); - } - - public override bool Contains(UInt160 scriptHash) - { - lock (accounts) - { - return accounts.ContainsKey(scriptHash); - } - } - - public override WalletAccount CreateAccount(byte[] privateKey) - { - KeyPair key = new KeyPair(privateKey); - NEP6Contract contract = new NEP6Contract - { - Script = Contract.CreateSignatureRedeemScript(key.PublicKey), - ParameterList = new[] { ContractParameterType.Signature }, - ParameterNames = new[] { "signature" }, - Deployed = false - }; - NEP6Account account = new NEP6Account(this, contract.ScriptHash, key, password) - { - Contract = contract - }; - AddAccount(account, false); - return account; - } - - public override WalletAccount CreateAccount(Contract contract, KeyPair key = null) - { - NEP6Contract nep6contract = contract as NEP6Contract; - if (nep6contract == null) - { - nep6contract = new NEP6Contract - { - Script = contract.Script, - ParameterList = contract.ParameterList, - ParameterNames = contract.ParameterList.Select((p, i) => $"parameter{i}").ToArray(), - Deployed = false - }; - } - NEP6Account account; - if (key == null) - account = new NEP6Account(this, nep6contract.ScriptHash); - else - account = new NEP6Account(this, nep6contract.ScriptHash, key, password); - account.Contract = nep6contract; - AddAccount(account, false); - return account; - } - - public override WalletAccount CreateAccount(UInt160 scriptHash) - { - NEP6Account account = new NEP6Account(this, scriptHash); - AddAccount(account, true); - return account; - } - - public KeyPair DecryptKey(string nep2key) - { - return new KeyPair(GetPrivateKeyFromNEP2(nep2key, password, Scrypt.N, Scrypt.R, Scrypt.P)); - } - - public override bool DeleteAccount(UInt160 scriptHash) - { - bool removed; - lock (accounts) - { - removed = accounts.Remove(scriptHash); - } - if (removed) - { - WalletIndexer.UnregisterAccounts(new[] { scriptHash }); - } - return removed; - } - - public void Dispose() - { - WalletIndexer.BalanceChanged -= WalletIndexer_BalanceChanged; - } - - public override Coin[] FindUnspentCoins(UInt256 asset_id, Fixed8 amount, UInt160[] from) - { - return FindUnspentCoins(FindUnspentCoins(from).ToArray().Where(p => GetAccount(p.Output.ScriptHash).Contract.IsStandard), asset_id, amount) ?? base.FindUnspentCoins(asset_id, amount, from); - } - - public override WalletAccount GetAccount(UInt160 scriptHash) - { - lock (accounts) - { - accounts.TryGetValue(scriptHash, out NEP6Account account); - return account; - } - } - - public override IEnumerable GetAccounts() - { - lock (accounts) - { - foreach (NEP6Account account in accounts.Values) - yield return account; - } - } - - public override IEnumerable GetCoins(IEnumerable accounts) - { - if (unconfirmed.Count == 0) - return WalletIndexer.GetCoins(accounts); - else - return GetCoinsInternal(); - IEnumerable GetCoinsInternal() - { - HashSet inputs, claims; - Coin[] coins_unconfirmed; - lock (unconfirmed) - { - inputs = new HashSet(unconfirmed.Values.SelectMany(p => p.Inputs)); - claims = new HashSet(unconfirmed.Values.OfType().SelectMany(p => p.Claims)); - coins_unconfirmed = unconfirmed.Values.Select(tx => tx.Outputs.Select((o, i) => new Coin - { - Reference = new CoinReference - { - PrevHash = tx.Hash, - PrevIndex = (ushort)i - }, - Output = o, - State = CoinState.Unconfirmed - })).SelectMany(p => p).ToArray(); - } - foreach (Coin coin in WalletIndexer.GetCoins(accounts)) - { - if (inputs.Contains(coin.Reference)) - { - if (coin.Output.AssetId.Equals(Blockchain.GoverningToken.Hash)) - yield return new Coin - { - Reference = coin.Reference, - Output = coin.Output, - State = coin.State | CoinState.Spent - }; - continue; - } - else if (claims.Contains(coin.Reference)) - { - continue; - } - yield return coin; - } - HashSet accounts_set = new HashSet(accounts); - foreach (Coin coin in coins_unconfirmed) - { - if (accounts_set.Contains(coin.Output.ScriptHash)) - yield return coin; - } - } - } - - public override IEnumerable GetTransactions() - { - foreach (UInt256 hash in WalletIndexer.GetTransactions(accounts.Keys)) - yield return hash; - lock (unconfirmed) - { - foreach (UInt256 hash in unconfirmed.Keys) - yield return hash; - } - } - - public override WalletAccount Import(X509Certificate2 cert) - { - KeyPair key; - using (ECDsa ecdsa = cert.GetECDsaPrivateKey()) - { - key = new KeyPair(ecdsa.ExportParameters(true).D); - } - NEP6Contract contract = new NEP6Contract - { - Script = Contract.CreateSignatureRedeemScript(key.PublicKey), - ParameterList = new[] { ContractParameterType.Signature }, - ParameterNames = new[] { "signature" }, - Deployed = false - }; - NEP6Account account = new NEP6Account(this, contract.ScriptHash, key, password) - { - Contract = contract - }; - AddAccount(account, true); - return account; - } - - public override WalletAccount Import(string wif) - { - KeyPair key = new KeyPair(GetPrivateKeyFromWIF(wif)); - NEP6Contract contract = new NEP6Contract - { - Script = Contract.CreateSignatureRedeemScript(key.PublicKey), - ParameterList = new[] { ContractParameterType.Signature }, - ParameterNames = new[] { "signature" }, - Deployed = false - }; - NEP6Account account = new NEP6Account(this, contract.ScriptHash, key, password) - { - Contract = contract - }; - AddAccount(account, true); - return account; - } - - public override WalletAccount Import(string nep2, string passphrase) - { - KeyPair key = new KeyPair(GetPrivateKeyFromNEP2(nep2, passphrase)); - NEP6Contract contract = new NEP6Contract - { - Script = Contract.CreateSignatureRedeemScript(key.PublicKey), - ParameterList = new[] { ContractParameterType.Signature }, - ParameterNames = new[] { "signature" }, - Deployed = false - }; - NEP6Account account; - if (Scrypt.N == 16384 && Scrypt.R == 8 && Scrypt.P == 8) - account = new NEP6Account(this, contract.ScriptHash, nep2); - else - account = new NEP6Account(this, contract.ScriptHash, key, passphrase); - account.Contract = contract; - AddAccount(account, true); - return account; - } - - internal void Lock() - { - password = null; - } - - public static NEP6Wallet Migrate(string path, string db3path, string password) - { - using (UserWallet wallet_old = UserWallet.Open(db3path, password)) - { - NEP6Wallet wallet_new = new NEP6Wallet(path, wallet_old.Name); - using (wallet_new.Unlock(password)) - { - foreach (WalletAccount account in wallet_old.GetAccounts()) - { - wallet_new.CreateAccount(account.Contract, account.GetKey()); - } - } - return wallet_new; - } - } - - public void Save() - { - JObject wallet = new JObject(); - wallet["name"] = name; - wallet["version"] = version.ToString(); - wallet["scrypt"] = Scrypt.ToJson(); - wallet["accounts"] = new JArray(accounts.Values.Select(p => p.ToJson())); - wallet["extra"] = extra; - File.WriteAllText(path, wallet.ToString()); - } - - public IDisposable Unlock(string password) - { - if (!VerifyPassword(password)) - throw new CryptographicException(); - this.password = password; - return new WalletLocker(this); - } - - public override bool VerifyPassword(string password) - { - lock (accounts) - { - NEP6Account account = accounts.Values.FirstOrDefault(p => !p.Decrypted); - if (account == null) - { - account = accounts.Values.FirstOrDefault(p => p.HasKey); - } - if (account == null) return true; - if (account.Decrypted) - { - return account.VerifyPassword(password); - } - else - { - try - { - account.GetKey(password); - return true; - } - catch (FormatException) - { - return false; - } - } - } - } - - private void WalletIndexer_BalanceChanged(object sender, BalanceEventArgs e) - { - lock (unconfirmed) - { - unconfirmed.Remove(e.Transaction.Hash); - } - UInt160[] relatedAccounts; - lock (accounts) - { - relatedAccounts = e.RelatedAccounts.Where(p => accounts.ContainsKey(p)).ToArray(); - } - if (relatedAccounts.Length > 0) - { - BalanceChanged?.Invoke(this, new BalanceEventArgs - { - Transaction = e.Transaction, - RelatedAccounts = relatedAccounts, - Height = e.Height, - Time = e.Time - }); - } - } - } -} diff --git a/neo/Implementations/Wallets/NEP6/ScryptParameters.cs b/neo/Implementations/Wallets/NEP6/ScryptParameters.cs deleted file mode 100644 index 1d53d34142..0000000000 --- a/neo/Implementations/Wallets/NEP6/ScryptParameters.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Neo.IO.Json; - -namespace Neo.Implementations.Wallets.NEP6 -{ - public class ScryptParameters - { - public static ScryptParameters Default { get; } = new ScryptParameters(16384, 8, 8); - - public readonly int N, R, P; - - public ScryptParameters(int n, int r, int p) - { - this.N = n; - this.R = r; - this.P = p; - } - - public static ScryptParameters FromJson(JObject json) - { - return new ScryptParameters((int)json["n"].AsNumber(), (int)json["r"].AsNumber(), (int)json["p"].AsNumber()); - } - - public JObject ToJson() - { - JObject json = new JObject(); - json["n"] = N; - json["r"] = R; - json["p"] = P; - return json; - } - } -} diff --git a/neo/Implementations/Wallets/NEP6/WalletLocker.cs b/neo/Implementations/Wallets/NEP6/WalletLocker.cs deleted file mode 100644 index ce88f5bd6e..0000000000 --- a/neo/Implementations/Wallets/NEP6/WalletLocker.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Neo.Implementations.Wallets.NEP6 -{ - internal class WalletLocker : IDisposable - { - private NEP6Wallet wallet; - - public WalletLocker(NEP6Wallet wallet) - { - this.wallet = wallet; - } - - public void Dispose() - { - wallet.Lock(); - } - } -} diff --git a/neo/Network/IInventory.cs b/neo/Network/IInventory.cs deleted file mode 100644 index c4784cdc8c..0000000000 --- a/neo/Network/IInventory.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Neo.Core; - -namespace Neo.Network -{ - public interface IInventory : IVerifiable - { - UInt256 Hash { get; } - - InventoryType InventoryType { get; } - - bool Verify(); - } -} diff --git a/neo/Network/InventoryReceivingEventArgs.cs b/neo/Network/InventoryReceivingEventArgs.cs deleted file mode 100644 index 020d06678f..0000000000 --- a/neo/Network/InventoryReceivingEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.ComponentModel; - -namespace Neo.Network -{ - public class InventoryReceivingEventArgs : CancelEventArgs - { - public IInventory Inventory { get; } - - public InventoryReceivingEventArgs(IInventory inventory) - { - this.Inventory = inventory; - } - } -} diff --git a/neo/Network/InventoryType.cs b/neo/Network/InventoryType.cs deleted file mode 100644 index 4d50374401..0000000000 --- a/neo/Network/InventoryType.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Neo.Network -{ - /// - /// 定义清单中的对象类型 - /// - public enum InventoryType : byte - { - /// - /// 交易 - /// - TX = 0x01, - /// - /// 区块 - /// - Block = 0x02, - /// - /// 共识数据 - /// - Consensus = 0xe0 - } -} diff --git a/neo/Network/LocalNode.cs b/neo/Network/LocalNode.cs deleted file mode 100644 index 955803f6a9..0000000000 --- a/neo/Network/LocalNode.cs +++ /dev/null @@ -1,624 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Neo.Core; -using Neo.IO; -using Neo.IO.Caching; -using Neo.Network.Payloads; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Net.WebSockets; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Neo.Network -{ - public class LocalNode : IDisposable - { - public static event EventHandler InventoryReceiving; - public static event EventHandler InventoryReceived; - - public const uint ProtocolVersion = 0; - private const int ConnectedMax = 10; - private const int UnconnectedMax = 1000; - public const int MemoryPoolSize = 30000; - - private static readonly Dictionary mem_pool = new Dictionary(); - private readonly HashSet temp_pool = new HashSet(); - internal static readonly HashSet KnownHashes = new HashSet(); - internal readonly RelayCache RelayCache = new RelayCache(100); - - private static readonly HashSet unconnectedPeers = new HashSet(); - private static readonly HashSet badPeers = new HashSet(); - internal readonly List connectedPeers = new List(); - - internal static readonly HashSet LocalAddresses = new HashSet(); - internal ushort Port; - internal readonly uint Nonce; - private TcpListener listener; - private IWebHost ws_host; - private Thread connectThread; - private Thread poolThread; - private readonly AutoResetEvent new_tx_event = new AutoResetEvent(false); - private int started = 0; - private int disposed = 0; - private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - - public bool GlobalMissionsEnabled { get; set; } = true; - public int RemoteNodeCount => connectedPeers.Count; - public bool ServiceEnabled { get; set; } = true; - public bool UpnpEnabled { get; set; } = false; - public string UserAgent { get; set; } - - static LocalNode() - { - LocalAddresses.UnionWith(NetworkInterface.GetAllNetworkInterfaces().SelectMany(p => p.GetIPProperties().UnicastAddresses).Select(p => p.Address.MapToIPv6())); - } - - public LocalNode() - { - Random rand = new Random(); - this.Nonce = (uint)rand.Next(); - this.connectThread = new Thread(ConnectToPeersLoop) - { - IsBackground = true, - Name = "LocalNode.ConnectToPeersLoop" - }; - if (Blockchain.Default != null) - { - this.poolThread = new Thread(AddTransactionLoop) - { - IsBackground = true, - Name = "LocalNode.AddTransactionLoop" - }; - } - this.UserAgent = string.Format("/NEO:{0}/", GetType().GetTypeInfo().Assembly.GetName().Version.ToString(3)); - Blockchain.PersistCompleted += Blockchain_PersistCompleted; - } - - private async void AcceptPeers() - { -#if !NET47 - //There is a bug in .NET Core 2.0 that blocks async method which returns void. - await Task.Yield(); -#endif - while (!cancellationTokenSource.IsCancellationRequested) - { - Socket socket; - try - { - socket = await listener.AcceptSocketAsync(); - } - catch (ObjectDisposedException) - { - break; - } - catch (SocketException) - { - continue; - } - TcpRemoteNode remoteNode = new TcpRemoteNode(this, socket); - OnConnected(remoteNode); - } - } - - private static bool AddTransaction(Transaction tx) - { - if (Blockchain.Default == null) return false; - lock (mem_pool) - { - if (mem_pool.ContainsKey(tx.Hash)) return false; - if (Blockchain.Default.ContainsTransaction(tx.Hash)) return false; - if (!tx.Verify(mem_pool.Values)) return false; - mem_pool.Add(tx.Hash, tx); - CheckMemPool(); - } - return true; - } - - private void AddTransactionLoop() - { - while (!cancellationTokenSource.IsCancellationRequested) - { - new_tx_event.WaitOne(); - Transaction[] transactions; - lock (temp_pool) - { - if (temp_pool.Count == 0) continue; - transactions = temp_pool.ToArray(); - temp_pool.Clear(); - } - ConcurrentBag verified = new ConcurrentBag(); - lock (mem_pool) - { - transactions = transactions.Where(p => !mem_pool.ContainsKey(p.Hash) && !Blockchain.Default.ContainsTransaction(p.Hash)).ToArray(); - if (transactions.Length == 0) continue; - - Transaction[] tmpool = mem_pool.Values.Concat(transactions).ToArray(); - - transactions.AsParallel().ForAll(tx => - { - if (tx.Verify(tmpool)) - verified.Add(tx); - }); - - if (verified.Count == 0) continue; - - foreach (Transaction tx in verified) - mem_pool.Add(tx.Hash, tx); - - CheckMemPool(); - } - RelayDirectly(verified); - if (InventoryReceived != null) - foreach (Transaction tx in verified) - InventoryReceived(this, tx); - } - } - - public static void AllowHashes(IEnumerable hashes) - { - lock (KnownHashes) - { - KnownHashes.ExceptWith(hashes); - } - } - - private void Blockchain_PersistCompleted(object sender, Block block) - { - Transaction[] remain; - lock (mem_pool) - { - foreach (Transaction tx in block.Transactions) - { - mem_pool.Remove(tx.Hash); - } - if (mem_pool.Count == 0) return; - - remain = mem_pool.Values.ToArray(); - mem_pool.Clear(); - } - lock (temp_pool) - { - temp_pool.UnionWith(remain); - } - new_tx_event.Set(); - } - - private static void CheckMemPool() - { - if (mem_pool.Count <= MemoryPoolSize) return; - UInt256[] hashes = mem_pool.Values.AsParallel().OrderBy(p => p.NetworkFee / p.Size).Take(mem_pool.Count - MemoryPoolSize).Select(p => p.Hash).ToArray(); - foreach (UInt256 hash in hashes) - mem_pool.Remove(hash); - } - - public async Task ConnectToPeerAsync(string hostNameOrAddress, int port) - { - IPAddress ipAddress; - if (IPAddress.TryParse(hostNameOrAddress, out ipAddress)) - { - ipAddress = ipAddress.MapToIPv6(); - } - else - { - IPHostEntry entry; - try - { - entry = await Dns.GetHostEntryAsync(hostNameOrAddress); - } - catch (SocketException) - { - return; - } - ipAddress = entry.AddressList.FirstOrDefault(p => p.AddressFamily == AddressFamily.InterNetwork || p.IsIPv6Teredo)?.MapToIPv6(); - if (ipAddress == null) return; - } - await ConnectToPeerAsync(new IPEndPoint(ipAddress, port)); - } - - public async Task ConnectToPeerAsync(IPEndPoint remoteEndpoint) - { - if (remoteEndpoint.Port == Port && LocalAddresses.Contains(remoteEndpoint.Address)) return; - lock (unconnectedPeers) - { - unconnectedPeers.Remove(remoteEndpoint); - } - lock (connectedPeers) - { - if (connectedPeers.Any(p => remoteEndpoint.Equals(p.ListenerEndpoint))) - return; - } - TcpRemoteNode remoteNode = new TcpRemoteNode(this, remoteEndpoint); - if (await remoteNode.ConnectAsync()) - { - OnConnected(remoteNode); - } - } - - private void ConnectToPeersLoop() - { - while (!cancellationTokenSource.IsCancellationRequested) - { - int connectedCount = connectedPeers.Count; - int unconnectedCount = unconnectedPeers.Count; - if (connectedCount < ConnectedMax) - { - Task[] tasks = { }; - if (unconnectedCount > 0) - { - IPEndPoint[] endpoints; - lock (unconnectedPeers) - { - endpoints = unconnectedPeers.Take(ConnectedMax - connectedCount).ToArray(); - } - tasks = endpoints.Select(p => ConnectToPeerAsync(p)).ToArray(); - } - else if (connectedCount > 0) - { - lock (connectedPeers) - { - foreach (RemoteNode node in connectedPeers) - node.RequestPeers(); - } - } - else - { - tasks = Settings.Default.SeedList.OfType().Select(p => p.Split(':')).Select(p => ConnectToPeerAsync(p[0], int.Parse(p[1]))).ToArray(); - } - try - { - Task.WaitAll(tasks, cancellationTokenSource.Token); - } - catch (OperationCanceledException) - { - break; - } - } - for (int i = 0; i < 50 && !cancellationTokenSource.IsCancellationRequested; i++) - { - Thread.Sleep(100); - } - } - } - - public static bool ContainsTransaction(UInt256 hash) - { - lock (mem_pool) - { - return mem_pool.ContainsKey(hash); - } - } - - public void Dispose() - { - if (Interlocked.Exchange(ref disposed, 1) == 0) - { - cancellationTokenSource.Cancel(); - if (started > 0) - { - Blockchain.PersistCompleted -= Blockchain_PersistCompleted; - if (listener != null) listener.Stop(); - if (!connectThread.ThreadState.HasFlag(ThreadState.Unstarted)) connectThread.Join(); - lock (unconnectedPeers) - { - if (unconnectedPeers.Count < UnconnectedMax) - { - lock (connectedPeers) - { - unconnectedPeers.UnionWith(connectedPeers.Select(p => p.ListenerEndpoint).Where(p => p != null).Take(UnconnectedMax - unconnectedPeers.Count)); - } - } - } - RemoteNode[] nodes; - lock (connectedPeers) - { - nodes = connectedPeers.ToArray(); - } - Task.WaitAll(nodes.Select(p => Task.Run(() => p.Disconnect(false))).ToArray()); - new_tx_event.Set(); - if (poolThread?.ThreadState.HasFlag(ThreadState.Unstarted) == false) - poolThread.Join(); - new_tx_event.Dispose(); - } - } - } - - public static IEnumerable GetMemoryPool() - { - lock (mem_pool) - { - foreach (Transaction tx in mem_pool.Values) - yield return tx; - } - } - - public RemoteNode[] GetRemoteNodes() - { - lock (connectedPeers) - { - return connectedPeers.ToArray(); - } - } - - public static Transaction GetTransaction(UInt256 hash) - { - lock (mem_pool) - { - if (!mem_pool.TryGetValue(hash, out Transaction tx)) - return null; - return tx; - } - } - - internal void RequestGetBlocks() - { - RemoteNode[] nodes = GetRemoteNodes(); - - GetBlocksPayload payload = GetBlocksPayload.Create(Blockchain.Default.CurrentBlockHash); - - foreach (RemoteNode node in nodes) - node.EnqueueMessage("getblocks", payload); - } - - private static bool IsIntranetAddress(IPAddress address) - { - byte[] data = address.MapToIPv4().GetAddressBytes(); - Array.Reverse(data); - uint value = data.ToUInt32(0); - return (value & 0xff000000) == 0x0a000000 || (value & 0xff000000) == 0x7f000000 || (value & 0xfff00000) == 0xac100000 || (value & 0xffff0000) == 0xc0a80000 || (value & 0xffff0000) == 0xa9fe0000; - } - - public static void LoadState(Stream stream) - { - lock (unconnectedPeers) - { - unconnectedPeers.Clear(); - using (BinaryReader reader = new BinaryReader(stream, Encoding.ASCII, true)) - { - int count = reader.ReadInt32(); - for (int i = 0; i < count; i++) - { - IPAddress address = new IPAddress(reader.ReadBytes(4)); - int port = reader.ReadUInt16(); - unconnectedPeers.Add(new IPEndPoint(address.MapToIPv6(), port)); - } - } - } - } - - private void OnConnected(RemoteNode remoteNode) - { - lock (connectedPeers) - { - connectedPeers.Add(remoteNode); - } - remoteNode.Disconnected += RemoteNode_Disconnected; - remoteNode.InventoryReceived += RemoteNode_InventoryReceived; - remoteNode.PeersReceived += RemoteNode_PeersReceived; - remoteNode.StartProtocol(); - } - - private async Task ProcessWebSocketAsync(HttpContext context) - { - if (!context.WebSockets.IsWebSocketRequest) return; - WebSocket ws = await context.WebSockets.AcceptWebSocketAsync(); - WebSocketRemoteNode remoteNode = new WebSocketRemoteNode(this, ws, new IPEndPoint(context.Connection.RemoteIpAddress, context.Connection.RemotePort)); - OnConnected(remoteNode); - } - - public bool Relay(IInventory inventory) - { - if (inventory is MinerTransaction) return false; - lock (KnownHashes) - { - if (!KnownHashes.Add(inventory.Hash)) return false; - } - InventoryReceivingEventArgs args = new InventoryReceivingEventArgs(inventory); - InventoryReceiving?.Invoke(this, args); - if (args.Cancel) return false; - if (inventory is Block block) - { - if (Blockchain.Default == null) return false; - if (Blockchain.Default.ContainsBlock(block.Hash)) return false; - if (!Blockchain.Default.AddBlock(block)) return false; - } - else if (inventory is Transaction) - { - if (!AddTransaction((Transaction)inventory)) return false; - } - else //if (inventory is Consensus) - { - if (!inventory.Verify()) return false; - } - bool relayed = RelayDirectly(inventory); - InventoryReceived?.Invoke(this, inventory); - return relayed; - } - - public bool RelayDirectly(IInventory inventory) - { - bool relayed = false; - lock (connectedPeers) - { - RelayCache.Add(inventory); - foreach (RemoteNode node in connectedPeers) - relayed |= node.Relay(inventory); - } - return relayed; - } - - private void RelayDirectly(IReadOnlyCollection transactions) - { - lock (connectedPeers) - { - foreach (RemoteNode node in connectedPeers) - node.Relay(transactions); - } - } - - private void RemoteNode_Disconnected(object sender, bool error) - { - RemoteNode remoteNode = (RemoteNode)sender; - remoteNode.Disconnected -= RemoteNode_Disconnected; - remoteNode.InventoryReceived -= RemoteNode_InventoryReceived; - remoteNode.PeersReceived -= RemoteNode_PeersReceived; - if (error && remoteNode.ListenerEndpoint != null) - { - lock (badPeers) - { - badPeers.Add(remoteNode.ListenerEndpoint); - } - } - lock (unconnectedPeers) - { - lock (connectedPeers) - { - if (remoteNode.ListenerEndpoint != null) - { - unconnectedPeers.Remove(remoteNode.ListenerEndpoint); - } - connectedPeers.Remove(remoteNode); - } - } - } - - private void RemoteNode_InventoryReceived(object sender, IInventory inventory) - { - if (inventory is Transaction tx && tx.Type != TransactionType.ClaimTransaction && tx.Type != TransactionType.IssueTransaction) - { - if (Blockchain.Default == null) return; - lock (KnownHashes) - { - if (!KnownHashes.Add(inventory.Hash)) return; - } - InventoryReceivingEventArgs args = new InventoryReceivingEventArgs(inventory); - InventoryReceiving?.Invoke(this, args); - if (args.Cancel) return; - lock (temp_pool) - { - temp_pool.Add(tx); - } - new_tx_event.Set(); - } - else - { - Relay(inventory); - } - } - - private void RemoteNode_PeersReceived(object sender, IPEndPoint[] peers) - { - lock (unconnectedPeers) - { - if (unconnectedPeers.Count < UnconnectedMax) - { - lock (badPeers) - { - lock (connectedPeers) - { - unconnectedPeers.UnionWith(peers); - unconnectedPeers.ExceptWith(badPeers); - unconnectedPeers.ExceptWith(connectedPeers.Select(p => p.ListenerEndpoint)); - } - } - } - } - } - - public IPEndPoint[] GetUnconnectedPeers() - { - lock (unconnectedPeers) - { - return unconnectedPeers.ToArray(); - } - } - - public IPEndPoint[] GetBadPeers() - { - lock (badPeers) - { - return badPeers.ToArray(); - } - } - - public static void SaveState(Stream stream) - { - IPEndPoint[] peers; - lock (unconnectedPeers) - { - peers = unconnectedPeers.Take(UnconnectedMax).ToArray(); - } - using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true)) - { - writer.Write(peers.Length); - foreach (IPEndPoint endpoint in peers) - { - writer.Write(endpoint.Address.MapToIPv4().GetAddressBytes()); - writer.Write((ushort)endpoint.Port); - } - } - } - - public void Start(int port = 0, int ws_port = 0) - { - if (Interlocked.Exchange(ref started, 1) == 0) - { - Task.Run(async () => - { - if ((port > 0 || ws_port > 0) - && UpnpEnabled - && LocalAddresses.All(p => !p.IsIPv4MappedToIPv6 || IsIntranetAddress(p)) - && await UPnP.DiscoverAsync()) - { - try - { - LocalAddresses.Add(await UPnP.GetExternalIPAsync()); - if (port > 0) - await UPnP.ForwardPortAsync(port, ProtocolType.Tcp, "NEO"); - if (ws_port > 0) - await UPnP.ForwardPortAsync(ws_port, ProtocolType.Tcp, "NEO WebSocket"); - } - catch { } - } - connectThread.Start(); - poolThread?.Start(); - if (port > 0) - { - listener = new TcpListener(IPAddress.Any, port); - listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); - try - { - listener.Start(); - Port = (ushort)port; - AcceptPeers(); - } - catch (SocketException) { } - } - if (ws_port > 0) - { - ws_host = new WebHostBuilder().UseKestrel().UseUrls($"http://*:{ws_port}").Configure(app => app.UseWebSockets().Run(ProcessWebSocketAsync)).Build(); - ws_host.Start(); - } - }); - } - } - - public void SynchronizeMemoryPool() - { - lock (connectedPeers) - { - foreach (RemoteNode node in connectedPeers) - node.RequestMemoryPool(); - } - } - } -} diff --git a/neo/Network/Message.cs b/neo/Network/Message.cs deleted file mode 100644 index cb20b39cc5..0000000000 --- a/neo/Network/Message.cs +++ /dev/null @@ -1,154 +0,0 @@ -using Neo.Cryptography; -using Neo.IO; -using System; -using System.IO; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Neo.Network -{ - public class Message : ISerializable - { - private const int PayloadMaxSize = 0x02000000; - - public static readonly uint Magic = Settings.Default.Magic; - public string Command; - public uint Checksum; - public byte[] Payload; - - public int Size => sizeof(uint) + 12 + sizeof(int) + sizeof(uint) + Payload.Length; - - public static Message Create(string command, ISerializable payload = null) - { - return Create(command, payload == null ? new byte[0] : payload.ToArray()); - } - - public static Message Create(string command, byte[] payload) - { - return new Message - { - Command = command, - Checksum = GetChecksum(payload), - Payload = payload - }; - } - - void ISerializable.Deserialize(BinaryReader reader) - { - if (reader.ReadUInt32() != Magic) - throw new FormatException(); - this.Command = reader.ReadFixedString(12); - uint length = reader.ReadUInt32(); - if (length > PayloadMaxSize) - throw new FormatException(); - this.Checksum = reader.ReadUInt32(); - this.Payload = reader.ReadBytes((int)length); - if (GetChecksum(Payload) != Checksum) - throw new FormatException(); - } - - public static async Task DeserializeFromAsync(Stream stream, CancellationToken cancellationToken) - { - uint payload_length; - byte[] buffer = await FillBufferAsync(stream, 24, cancellationToken); - Message message = new Message(); - using (MemoryStream ms = new MemoryStream(buffer, false)) - using (BinaryReader reader = new BinaryReader(ms, Encoding.UTF8)) - { - if (reader.ReadUInt32() != Magic) - throw new FormatException(); - message.Command = reader.ReadFixedString(12); - payload_length = reader.ReadUInt32(); - if (payload_length > PayloadMaxSize) - throw new FormatException(); - message.Checksum = reader.ReadUInt32(); - } - if (payload_length > 0) - message.Payload = await FillBufferAsync(stream, (int)payload_length, cancellationToken); - else - message.Payload = new byte[0]; - if (GetChecksum(message.Payload) != message.Checksum) - throw new FormatException(); - return message; - } - - public static async Task DeserializeFromAsync(WebSocket socket, CancellationToken cancellationToken) - { - uint payload_length; - byte[] buffer = await FillBufferAsync(socket, 24, cancellationToken); - Message message = new Message(); - using (MemoryStream ms = new MemoryStream(buffer, false)) - using (BinaryReader reader = new BinaryReader(ms, Encoding.UTF8)) - { - if (reader.ReadUInt32() != Magic) - throw new FormatException(); - message.Command = reader.ReadFixedString(12); - payload_length = reader.ReadUInt32(); - if (payload_length > PayloadMaxSize) - throw new FormatException(); - message.Checksum = reader.ReadUInt32(); - } - if (payload_length > 0) - message.Payload = await FillBufferAsync(socket, (int)payload_length, cancellationToken); - else - message.Payload = new byte[0]; - if (GetChecksum(message.Payload) != message.Checksum) - throw new FormatException(); - return message; - } - - private static async Task FillBufferAsync(Stream stream, int buffer_size, CancellationToken cancellationToken) - { - const int MAX_SIZE = 1024; - byte[] buffer = new byte[buffer_size < MAX_SIZE ? buffer_size : MAX_SIZE]; - using (MemoryStream ms = new MemoryStream()) - { - while (buffer_size > 0) - { - int count = buffer_size < MAX_SIZE ? buffer_size : MAX_SIZE; - count = await stream.ReadAsync(buffer, 0, count, cancellationToken); - if (count <= 0) throw new IOException(); - ms.Write(buffer, 0, count); - buffer_size -= count; - } - return ms.ToArray(); - } - } - - private static async Task FillBufferAsync(WebSocket socket, int buffer_size, CancellationToken cancellationToken) - { - const int MAX_SIZE = 1024; - byte[] buffer = new byte[buffer_size < MAX_SIZE ? buffer_size : MAX_SIZE]; - using (MemoryStream ms = new MemoryStream()) - { - while (buffer_size > 0) - { - int count = buffer_size < MAX_SIZE ? buffer_size : MAX_SIZE; - ArraySegment segment = new ArraySegment(buffer, 0, count); - WebSocketReceiveResult result = await socket.ReceiveAsync(segment, cancellationToken); - if (result.Count <= 0 || result.MessageType != WebSocketMessageType.Binary) - throw new IOException(); - ms.Write(buffer, 0, result.Count); - buffer_size -= result.Count; - } - return ms.ToArray(); - } - } - - private static uint GetChecksum(byte[] value) - { - return Crypto.Default.Hash256(value).ToUInt32(0); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(Magic); - writer.WriteFixedString(Command, 12); - writer.Write(Payload.Length); - writer.Write(Checksum); - writer.Write(Payload); - } - } -} diff --git a/neo/Network/Payloads/AddrPayload.cs b/neo/Network/Payloads/AddrPayload.cs deleted file mode 100644 index f4c25027f9..0000000000 --- a/neo/Network/Payloads/AddrPayload.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Neo.IO; -using System.IO; - -namespace Neo.Network.Payloads -{ - internal class AddrPayload : ISerializable - { - public NetworkAddressWithTime[] AddressList; - - public int Size => AddressList.GetVarSize(); - - public static AddrPayload Create(params NetworkAddressWithTime[] addresses) - { - return new AddrPayload - { - AddressList = addresses - }; - } - - void ISerializable.Deserialize(BinaryReader reader) - { - this.AddressList = reader.ReadSerializableArray(200); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(AddressList); - } - } -} diff --git a/neo/Network/Payloads/ConsensusPayload.cs b/neo/Network/Payloads/ConsensusPayload.cs deleted file mode 100644 index ab79f31d4f..0000000000 --- a/neo/Network/Payloads/ConsensusPayload.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Neo.Core; -using Neo.Cryptography; -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.SmartContract; -using Neo.VM; -using System; -using System.IO; - -namespace Neo.Network.Payloads -{ - public class ConsensusPayload : IInventory - { - public uint Version; - public UInt256 PrevHash; - public uint BlockIndex; - public ushort ValidatorIndex; - public uint Timestamp; - public byte[] Data; - public Witness Script; - - private UInt256 _hash = null; - UInt256 IInventory.Hash - { - get - { - if (_hash == null) - { - _hash = new UInt256(Crypto.Default.Hash256(this.GetHashData())); - } - return _hash; - } - } - - InventoryType IInventory.InventoryType => InventoryType.Consensus; - - Witness[] IVerifiable.Scripts - { - get - { - return new[] { Script }; - } - set - { - if (value.Length != 1) throw new ArgumentException(); - Script = value[0]; - } - } - - public int Size => sizeof(uint) + PrevHash.Size + sizeof(uint) + sizeof(ushort) + sizeof(uint) + Data.GetVarSize() + 1 + Script.Size; - - void ISerializable.Deserialize(BinaryReader reader) - { - ((IVerifiable)this).DeserializeUnsigned(reader); - if (reader.ReadByte() != 1) throw new FormatException(); - Script = reader.ReadSerializable(); - } - - void IVerifiable.DeserializeUnsigned(BinaryReader reader) - { - Version = reader.ReadUInt32(); - PrevHash = reader.ReadSerializable(); - BlockIndex = reader.ReadUInt32(); - ValidatorIndex = reader.ReadUInt16(); - Timestamp = reader.ReadUInt32(); - Data = reader.ReadVarBytes(); - } - - byte[] IScriptContainer.GetMessage() - { - return this.GetHashData(); - } - - UInt160[] IVerifiable.GetScriptHashesForVerifying() - { - if (Blockchain.Default == null) - throw new InvalidOperationException(); - ECPoint[] validators = Blockchain.Default.GetValidators(); - if (validators.Length <= ValidatorIndex) - throw new InvalidOperationException(); - return new[] { Contract.CreateSignatureRedeemScript(validators[ValidatorIndex]).ToScriptHash() }; - } - - void ISerializable.Serialize(BinaryWriter writer) - { - ((IVerifiable)this).SerializeUnsigned(writer); - writer.Write((byte)1); writer.Write(Script); - } - - void IVerifiable.SerializeUnsigned(BinaryWriter writer) - { - writer.Write(Version); - writer.Write(PrevHash); - writer.Write(BlockIndex); - writer.Write(ValidatorIndex); - writer.Write(Timestamp); - writer.WriteVarBytes(Data); - } - - public bool Verify() - { - if (Blockchain.Default == null) return false; - if (BlockIndex <= Blockchain.Default.Height) - return false; - return this.VerifyScripts(); - } - } -} diff --git a/neo/Network/Payloads/FilterAddPayload.cs b/neo/Network/Payloads/FilterAddPayload.cs deleted file mode 100644 index 24a48acdae..0000000000 --- a/neo/Network/Payloads/FilterAddPayload.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Neo.IO; -using System.IO; - -namespace Neo.Network.Payloads -{ - internal class FilterAddPayload : ISerializable - { - public byte[] Data; - - public int Size => Data.GetVarSize(); - - void ISerializable.Deserialize(BinaryReader reader) - { - Data = reader.ReadVarBytes(520); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.WriteVarBytes(Data); - } - } -} diff --git a/neo/Network/Payloads/FilterLoadPayload.cs b/neo/Network/Payloads/FilterLoadPayload.cs deleted file mode 100644 index 337e275eb6..0000000000 --- a/neo/Network/Payloads/FilterLoadPayload.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Neo.Cryptography; -using Neo.IO; -using System; -using System.IO; - -namespace Neo.Network.Payloads -{ - internal class FilterLoadPayload : ISerializable - { - public byte[] Filter; - public byte K; - public uint Tweak; - - public int Size => Filter.GetVarSize() + sizeof(byte) + sizeof(uint); - - public static FilterLoadPayload Create(BloomFilter filter) - { - byte[] buffer = new byte[filter.M / 8]; - filter.GetBits(buffer); - return new FilterLoadPayload - { - Filter = buffer, - K = (byte)filter.K, - Tweak = filter.Tweak - }; - } - - void ISerializable.Deserialize(BinaryReader reader) - { - Filter = reader.ReadVarBytes(36000); - K = reader.ReadByte(); - if (K > 50) throw new FormatException(); - Tweak = reader.ReadUInt32(); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.WriteVarBytes(Filter); - writer.Write(K); - writer.Write(Tweak); - } - } -} diff --git a/neo/Network/Payloads/GetBlocksPayload.cs b/neo/Network/Payloads/GetBlocksPayload.cs deleted file mode 100644 index d91ac172b1..0000000000 --- a/neo/Network/Payloads/GetBlocksPayload.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Neo.IO; -using System.IO; - -namespace Neo.Network.Payloads -{ - internal class GetBlocksPayload : ISerializable - { - public UInt256[] HashStart; - public UInt256 HashStop; - - public int Size => HashStart.GetVarSize() + HashStop.Size; - - public static GetBlocksPayload Create(UInt256 hash_start, UInt256 hash_stop = null) - { - return new GetBlocksPayload - { - HashStart = new[] { hash_start }, - HashStop = hash_stop ?? UInt256.Zero - }; - } - - void ISerializable.Deserialize(BinaryReader reader) - { - HashStart = reader.ReadSerializableArray(16); - HashStop = reader.ReadSerializable(); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(HashStart); - writer.Write(HashStop); - } - } -} diff --git a/neo/Network/Payloads/HeadersPayload.cs b/neo/Network/Payloads/HeadersPayload.cs deleted file mode 100644 index 69ddf2f3ab..0000000000 --- a/neo/Network/Payloads/HeadersPayload.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Neo.Core; -using Neo.IO; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Neo.Network.Payloads -{ - internal class HeadersPayload : ISerializable - { - public Header[] Headers; - - public int Size => Headers.GetVarSize(); - - public static HeadersPayload Create(IEnumerable
headers) - { - return new HeadersPayload - { - Headers = headers.ToArray() - }; - } - - void ISerializable.Deserialize(BinaryReader reader) - { - Headers = reader.ReadSerializableArray
(2000); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(Headers); - } - } -} diff --git a/neo/Network/Payloads/InvPayload.cs b/neo/Network/Payloads/InvPayload.cs deleted file mode 100644 index 03d15efb35..0000000000 --- a/neo/Network/Payloads/InvPayload.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Neo.IO; -using System; -using System.IO; - -namespace Neo.Network.Payloads -{ - internal class InvPayload : ISerializable - { - public InventoryType Type; - public UInt256[] Hashes; - - public int Size => sizeof(InventoryType) + Hashes.GetVarSize(); - - public static InvPayload Create(InventoryType type, params UInt256[] hashes) - { - return new InvPayload - { - Type = type, - Hashes = hashes - }; - } - - void ISerializable.Deserialize(BinaryReader reader) - { - Type = (InventoryType)reader.ReadByte(); - if (!Enum.IsDefined(typeof(InventoryType), Type)) - throw new FormatException(); - Hashes = reader.ReadSerializableArray(); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write((byte)Type); - writer.Write(Hashes); - } - } -} diff --git a/neo/Network/Payloads/MerkleBlockPayload.cs b/neo/Network/Payloads/MerkleBlockPayload.cs deleted file mode 100644 index 53733a41d0..0000000000 --- a/neo/Network/Payloads/MerkleBlockPayload.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Neo.Core; -using Neo.Cryptography; -using Neo.IO; -using System.Collections; -using System.IO; -using System.Linq; - -namespace Neo.Network.Payloads -{ - internal class MerkleBlockPayload : BlockBase - { - public int TxCount; - public UInt256[] Hashes; - public byte[] Flags; - - public override int Size => base.Size + sizeof(int) + Hashes.GetVarSize() + Flags.GetVarSize(); - - public static MerkleBlockPayload Create(Block block, BitArray flags) - { - MerkleTree tree = new MerkleTree(block.Transactions.Select(p => p.Hash).ToArray()); - tree.Trim(flags); - byte[] buffer = new byte[(flags.Length + 7) / 8]; - flags.CopyTo(buffer, 0); - return new MerkleBlockPayload - { - Version = block.Version, - PrevHash = block.PrevHash, - MerkleRoot = block.MerkleRoot, - Timestamp = block.Timestamp, - Index = block.Index, - ConsensusData = block.ConsensusData, - NextConsensus = block.NextConsensus, - Script = block.Script, - TxCount = block.Transactions.Length, - Hashes = tree.ToHashArray(), - Flags = buffer - }; - } - - public override void Deserialize(BinaryReader reader) - { - base.Deserialize(reader); - TxCount = (int)reader.ReadVarInt(int.MaxValue); - Hashes = reader.ReadSerializableArray(); - Flags = reader.ReadVarBytes(); - } - - public override void Serialize(BinaryWriter writer) - { - base.Serialize(writer); - writer.WriteVarInt(TxCount); - writer.Write(Hashes); - writer.WriteVarBytes(Flags); - } - } -} diff --git a/neo/Network/Payloads/NetworkAddressWithTime.cs b/neo/Network/Payloads/NetworkAddressWithTime.cs deleted file mode 100644 index 85743fbd8d..0000000000 --- a/neo/Network/Payloads/NetworkAddressWithTime.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Neo.IO; -using System; -using System.IO; -using System.Linq; -using System.Net; - -namespace Neo.Network.Payloads -{ - internal class NetworkAddressWithTime : ISerializable - { - public const ulong NODE_NETWORK = 1; - - public uint Timestamp; - public ulong Services; - public IPEndPoint EndPoint; - - public int Size => sizeof(uint) + sizeof(ulong) + 16 + sizeof(ushort); - - public static NetworkAddressWithTime Create(IPEndPoint endpoint, ulong services, uint timestamp) - { - return new NetworkAddressWithTime - { - Timestamp = timestamp, - Services = services, - EndPoint = endpoint - }; - } - - void ISerializable.Deserialize(BinaryReader reader) - { - Timestamp = reader.ReadUInt32(); - Services = reader.ReadUInt64(); - byte[] data = reader.ReadBytes(16); - if (data.Length != 16) throw new FormatException(); - IPAddress address = new IPAddress(data); - data = reader.ReadBytes(2); - if (data.Length != 2) throw new FormatException(); - ushort port = data.Reverse().ToArray().ToUInt16(0); - EndPoint = new IPEndPoint(address, port); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(Timestamp); - writer.Write(Services); - writer.Write(EndPoint.Address.GetAddressBytes()); - writer.Write(BitConverter.GetBytes((ushort)EndPoint.Port).Reverse().ToArray()); - } - } -} diff --git a/neo/Network/Payloads/VersionPayload.cs b/neo/Network/Payloads/VersionPayload.cs deleted file mode 100644 index 7c33b64168..0000000000 --- a/neo/Network/Payloads/VersionPayload.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Neo.Core; -using Neo.IO; -using System; -using System.IO; - -namespace Neo.Network.Payloads -{ - public class VersionPayload : ISerializable - { - public uint Version; - public ulong Services; - public uint Timestamp; - public ushort Port; - public uint Nonce; - public string UserAgent; - public uint StartHeight; - public bool Relay; - - public int Size => sizeof(uint) + sizeof(ulong) + sizeof(uint) + sizeof(ushort) + sizeof(uint) + UserAgent.GetVarSize() + sizeof(uint) + sizeof(bool); - - public static VersionPayload Create(int port, uint nonce, string userAgent) - { - return new VersionPayload - { - Version = LocalNode.ProtocolVersion, - Services = NetworkAddressWithTime.NODE_NETWORK, - Timestamp = DateTime.Now.ToTimestamp(), - Port = (ushort)port, - Nonce = nonce, - UserAgent = userAgent, - StartHeight = Blockchain.Default?.Height ?? 0, - Relay = true - }; - } - - void ISerializable.Deserialize(BinaryReader reader) - { - Version = reader.ReadUInt32(); - Services = reader.ReadUInt64(); - Timestamp = reader.ReadUInt32(); - Port = reader.ReadUInt16(); - Nonce = reader.ReadUInt32(); - UserAgent = reader.ReadVarString(1024); - StartHeight = reader.ReadUInt32(); - Relay = reader.ReadBoolean(); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(Version); - writer.Write(Services); - writer.Write(Timestamp); - writer.Write(Port); - writer.Write(Nonce); - writer.WriteVarString(UserAgent); - writer.Write(StartHeight); - writer.Write(Relay); - } - } -} diff --git a/neo/Network/RPC/RpcException.cs b/neo/Network/RPC/RpcException.cs deleted file mode 100644 index 5a2120f083..0000000000 --- a/neo/Network/RPC/RpcException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Neo.Network.RPC -{ - public class RpcException : Exception - { - public RpcException(int code, string message) : base(message) - { - HResult = code; - } - } -} diff --git a/neo/Network/RPC/RpcServer.cs b/neo/Network/RPC/RpcServer.cs deleted file mode 100644 index a4c28342a1..0000000000 --- a/neo/Network/RPC/RpcServer.cs +++ /dev/null @@ -1,405 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Neo.Core; -using Neo.IO; -using Neo.IO.Json; -using Neo.SmartContract; -using Neo.VM; -using Neo.Wallets; -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; - -namespace Neo.Network.RPC -{ - public class RpcServer : IDisposable - { - protected readonly LocalNode LocalNode; - private IWebHost host; - - public RpcServer(LocalNode localNode) - { - this.LocalNode = localNode; - } - - private static JObject CreateErrorResponse(JObject id, int code, string message, JObject data = null) - { - JObject response = CreateResponse(id); - response["error"] = new JObject(); - response["error"]["code"] = code; - response["error"]["message"] = message; - if (data != null) - response["error"]["data"] = data; - return response; - } - - private static JObject CreateResponse(JObject id) - { - JObject response = new JObject(); - response["jsonrpc"] = "2.0"; - response["id"] = id; - return response; - } - - public void Dispose() - { - if (host != null) - { - host.Dispose(); - host = null; - } - } - - private static JObject GetInvokeResult(byte[] script) - { - ApplicationEngine engine = ApplicationEngine.Run(script); - JObject json = new JObject(); - json["script"] = script.ToHexString(); - json["state"] = engine.State; - json["gas_consumed"] = engine.GasConsumed.ToString(); - json["stack"] = new JArray(engine.EvaluationStack.Select(p => p.ToParameter().ToJson())); - return json; - } - - protected virtual JObject Process(string method, JArray _params) - { - switch (method) - { - case "getaccountstate": - { - UInt160 script_hash = Wallet.ToScriptHash(_params[0].AsString()); - AccountState account = Blockchain.Default.GetAccountState(script_hash) ?? new AccountState(script_hash); - return account.ToJson(); - } - case "getassetstate": - { - UInt256 asset_id = UInt256.Parse(_params[0].AsString()); - AssetState asset = Blockchain.Default.GetAssetState(asset_id); - return asset?.ToJson() ?? throw new RpcException(-100, "Unknown asset"); - } - case "getbestblockhash": - return Blockchain.Default.CurrentBlockHash.ToString(); - case "getblock": - { - Block block; - if (_params[0] is JNumber) - { - uint index = (uint)_params[0].AsNumber(); - block = Blockchain.Default.GetBlock(index); - } - else - { - UInt256 hash = UInt256.Parse(_params[0].AsString()); - block = Blockchain.Default.GetBlock(hash); - } - if (block == null) - throw new RpcException(-100, "Unknown block"); - bool verbose = _params.Count >= 2 && _params[1].AsBooleanOrDefault(false); - if (verbose) - { - JObject json = block.ToJson(); - json["confirmations"] = Blockchain.Default.Height - block.Index + 1; - UInt256 hash = Blockchain.Default.GetNextBlockHash(block.Hash); - if (hash != null) - json["nextblockhash"] = hash.ToString(); - return json; - } - else - { - return block.ToArray().ToHexString(); - } - } - case "getblockcount": - return Blockchain.Default.Height + 1; - case "getblockhash": - { - uint height = (uint)_params[0].AsNumber(); - if (height >= 0 && height <= Blockchain.Default.Height) - { - return Blockchain.Default.GetBlockHash(height).ToString(); - } - else - { - throw new RpcException(-100, "Invalid Height"); - } - } - case "getblocksysfee": - { - uint height = (uint)_params[0].AsNumber(); - if (height >= 0 && height <= Blockchain.Default.Height) - { - return Blockchain.Default.GetSysFeeAmount(height).ToString(); - } - else - { - throw new RpcException(-100, "Invalid Height"); - } - } - case "getconnectioncount": - return LocalNode.RemoteNodeCount; - case "getcontractstate": - { - UInt160 script_hash = UInt160.Parse(_params[0].AsString()); - ContractState contract = Blockchain.Default.GetContract(script_hash); - return contract?.ToJson() ?? throw new RpcException(-100, "Unknown contract"); - } - case "getrawmempool": - return new JArray(LocalNode.GetMemoryPool().Select(p => (JObject)p.Hash.ToString())); - case "getrawtransaction": - { - UInt256 hash = UInt256.Parse(_params[0].AsString()); - bool verbose = _params.Count >= 2 && _params[1].AsBooleanOrDefault(false); - int height = -1; - Transaction tx = LocalNode.GetTransaction(hash); - if (tx == null) - tx = Blockchain.Default.GetTransaction(hash, out height); - if (tx == null) - throw new RpcException(-100, "Unknown transaction"); - if (verbose) - { - JObject json = tx.ToJson(); - if (height >= 0) - { - Header header = Blockchain.Default.GetHeader((uint)height); - json["blockhash"] = header.Hash.ToString(); - json["confirmations"] = Blockchain.Default.Height - header.Index + 1; - json["blocktime"] = header.Timestamp; - } - return json; - } - else - { - return tx.ToArray().ToHexString(); - } - } - case "getstorage": - { - UInt160 script_hash = UInt160.Parse(_params[0].AsString()); - byte[] key = _params[1].AsString().HexToBytes(); - StorageItem item = Blockchain.Default.GetStorageItem(new StorageKey - { - ScriptHash = script_hash, - Key = key - }) ?? new StorageItem(); - return item.Value?.ToHexString(); - } - case "gettxout": - { - UInt256 hash = UInt256.Parse(_params[0].AsString()); - ushort index = (ushort)_params[1].AsNumber(); - return Blockchain.Default.GetUnspent(hash, index)?.ToJson(index); - } - case "invoke": - { - UInt160 script_hash = UInt160.Parse(_params[0].AsString()); - ContractParameter[] parameters = ((JArray)_params[1]).Select(p => ContractParameter.FromJson(p)).ToArray(); - byte[] script; - using (ScriptBuilder sb = new ScriptBuilder()) - { - script = sb.EmitAppCall(script_hash, parameters).ToArray(); - } - return GetInvokeResult(script); - } - case "invokefunction": - { - UInt160 script_hash = UInt160.Parse(_params[0].AsString()); - string operation = _params[1].AsString(); - ContractParameter[] args = _params.Count >= 3 ? ((JArray)_params[2]).Select(p => ContractParameter.FromJson(p)).ToArray() : new ContractParameter[0]; - byte[] script; - using (ScriptBuilder sb = new ScriptBuilder()) - { - script = sb.EmitAppCall(script_hash, operation, args).ToArray(); - } - return GetInvokeResult(script); - } - case "invokescript": - { - byte[] script = _params[0].AsString().HexToBytes(); - return GetInvokeResult(script); - } - case "sendrawtransaction": - { - Transaction tx = Transaction.DeserializeFrom(_params[0].AsString().HexToBytes()); - return LocalNode.Relay(tx); - } - case "submitblock": - { - Block block = _params[0].AsString().HexToBytes().AsSerializable(); - return LocalNode.Relay(block); - } - case "validateaddress": - { - JObject json = new JObject(); - UInt160 scriptHash; - try - { - scriptHash = Wallet.ToScriptHash(_params[0].AsString()); - } - catch - { - scriptHash = null; - } - json["address"] = _params[0]; - json["isvalid"] = scriptHash != null; - return json; - } - case "getpeers": - { - JObject json = new JObject(); - - { - JArray unconnectedPeers = new JArray(); - foreach (IPEndPoint peer in LocalNode.GetUnconnectedPeers()) - { - JObject peerJson = new JObject(); - peerJson["address"] = peer.Address.ToString(); - peerJson["port"] = peer.Port; - unconnectedPeers.Add(peerJson); - } - json["unconnected"] = unconnectedPeers; - } - - { - JArray badPeers = new JArray(); - foreach (IPEndPoint peer in LocalNode.GetBadPeers()) - { - JObject peerJson = new JObject(); - peerJson["address"] = peer.Address.ToString(); - peerJson["port"] = peer.Port; - badPeers.Add(peerJson); - } - json["bad"] = badPeers; - } - - { - JArray connectedPeers = new JArray(); - foreach (RemoteNode node in LocalNode.GetRemoteNodes()) - { - JObject peerJson = new JObject(); - peerJson["address"] = node.RemoteEndpoint.Address.ToString(); - peerJson["port"] = node.ListenerEndpoint?.Port ?? 0; - connectedPeers.Add(peerJson); - } - json["connected"] = connectedPeers; - } - - return json; - } - case "getversion": - { - JObject json = new JObject(); - json["port"] = LocalNode.Port; - json["nonce"] = LocalNode.Nonce; - json["useragent"] = LocalNode.UserAgent; - return json; - } - default: - throw new RpcException(-32601, "Method not found"); - } - } - - private async Task ProcessAsync(HttpContext context) - { - context.Response.Headers["Access-Control-Allow-Origin"] = "*"; - context.Response.Headers["Access-Control-Allow-Methods"] = "GET, POST"; - context.Response.Headers["Access-Control-Allow-Headers"] = "Content-Type"; - context.Response.Headers["Access-Control-Max-Age"] = "31536000"; - if (context.Request.Method != "GET" && context.Request.Method != "POST") return; - JObject 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"] = double.Parse(id); - request["method"] = method; - request["params"] = JObject.Parse(_params); - } - } - else if (context.Request.Method == "POST") - { - using (StreamReader reader = new StreamReader(context.Request.Body)) - { - try - { - request = JObject.Parse(reader); - } - catch (FormatException) { } - } - } - JObject response; - if (request == null) - { - response = CreateErrorResponse(null, -32700, "Parse error"); - } - else if (request is JArray array) - { - if (array.Count == 0) - { - response = CreateErrorResponse(request["id"], -32600, "Invalid Request"); - } - else - { - response = array.Select(p => ProcessRequest(p)).Where(p => p != null).ToArray(); - } - } - else - { - response = ProcessRequest(request); - } - if (response == null || (response as JArray)?.Count == 0) return; - context.Response.ContentType = "application/json-rpc"; - await context.Response.WriteAsync(response.ToString()); - } - - private JObject ProcessRequest(JObject request) - { - if (!request.ContainsProperty("id")) return null; - if (!request.ContainsProperty("method") || !request.ContainsProperty("params") || !(request["params"] is JArray)) - { - return CreateErrorResponse(request["id"], -32600, "Invalid Request"); - } - JObject result = null; - try - { - result = Process(request["method"].AsString(), (JArray)request["params"]); - } - catch (Exception ex) - { -#if DEBUG - return CreateErrorResponse(request["id"], ex.HResult, ex.Message, ex.StackTrace); -#else - return CreateErrorResponse(request["id"], ex.HResult, ex.Message); -#endif - } - JObject response = CreateResponse(request["id"]); - response["result"] = result; - return response; - } - - public void Start(int port, string sslCert = null, string password = null) - { - host = new WebHostBuilder().UseKestrel(options => options.Listen(IPAddress.Any, port, listenOptions => - { - if (!string.IsNullOrEmpty(sslCert)) - listenOptions.UseHttps(sslCert, password); - })).Configure(app => app.Run(ProcessAsync)).Build(); - host.Start(); - } - } -} diff --git a/neo/Network/RemoteNode.cs b/neo/Network/RemoteNode.cs deleted file mode 100644 index c0546bc14b..0000000000 --- a/neo/Network/RemoteNode.cs +++ /dev/null @@ -1,545 +0,0 @@ -using Neo.Core; -using Neo.Cryptography; -using Neo.IO; -using Neo.Network.Payloads; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace Neo.Network -{ - public abstract class RemoteNode : IDisposable - { - public event EventHandler Disconnected; - internal event EventHandler InventoryReceived; - internal event EventHandler PeersReceived; - - private static readonly TimeSpan HalfMinute = TimeSpan.FromSeconds(30); - private static readonly TimeSpan OneMinute = TimeSpan.FromMinutes(1); - private static readonly TimeSpan HalfHour = TimeSpan.FromMinutes(30); - - private Queue message_queue_high = new Queue(); - private Queue message_queue_low = new Queue(); - private static HashSet missions_global = new HashSet(); - private HashSet missions = new HashSet(); - private DateTime mission_start = DateTime.Now.AddYears(100); - - private LocalNode localNode; - private int disposed = 0; - private BloomFilter bloom_filter; - - public VersionPayload Version { get; private set; } - public IPEndPoint RemoteEndpoint { get; protected set; } - public IPEndPoint ListenerEndpoint { get; protected set; } - - protected RemoteNode(LocalNode localNode) - { - this.localNode = localNode; - } - - public virtual void Disconnect(bool error) - { - if (Interlocked.Exchange(ref disposed, 1) == 0) - { - Disconnected?.Invoke(this, error); - bool needSync = false; - lock (missions_global) - lock (missions) - if (missions.Count > 0) - { - missions_global.ExceptWith(missions); - needSync = true; - } - if (needSync) - localNode.RequestGetBlocks(); - } - } - - public void Dispose() - { - Disconnect(false); - } - - public void EnqueueMessage(string command, ISerializable payload = null) - { - bool is_single = false; - switch (command) - { - case "addr": - case "getaddr": - case "getblocks": - case "getheaders": - case "mempool": - is_single = true; - break; - } - Queue message_queue; - switch (command) - { - case "alert": - case "consensus": - case "filteradd": - case "filterclear": - case "filterload": - case "getaddr": - case "mempool": - message_queue = message_queue_high; - break; - default: - message_queue = message_queue_low; - break; - } - lock (message_queue) - { - if (!is_single || message_queue.All(p => p.Command != command)) - { - message_queue.Enqueue(Message.Create(command, payload)); - } - } - } - - private void OnAddrMessageReceived(AddrPayload payload) - { - IPEndPoint[] peers = payload.AddressList.Select(p => p.EndPoint).Where(p => p.Port != localNode.Port || !LocalNode.LocalAddresses.Contains(p.Address)).ToArray(); - if (peers.Length > 0) PeersReceived?.Invoke(this, peers); - } - - private void OnFilterAddMessageReceived(FilterAddPayload payload) - { - if (bloom_filter != null) - bloom_filter.Add(payload.Data); - } - - private void OnFilterClearMessageReceived() - { - bloom_filter = null; - } - - private void OnFilterLoadMessageReceived(FilterLoadPayload payload) - { - bloom_filter = new BloomFilter(payload.Filter.Length * 8, payload.K, payload.Tweak, payload.Filter); - } - - private void OnGetAddrMessageReceived() - { - if (!localNode.ServiceEnabled) return; - AddrPayload payload; - lock (localNode.connectedPeers) - { - const int MaxCountToSend = 200; - IEnumerable peers = localNode.connectedPeers.Where(p => p.ListenerEndpoint != null && p.Version != null); - if (localNode.connectedPeers.Count > MaxCountToSend) - { - Random rand = new Random(); - peers = peers.OrderBy(p => rand.Next()); - } - peers = peers.Take(MaxCountToSend); - payload = AddrPayload.Create(peers.Select(p => NetworkAddressWithTime.Create(p.ListenerEndpoint, p.Version.Services, p.Version.Timestamp)).ToArray()); - } - EnqueueMessage("addr", payload); - } - - private void OnGetBlocksMessageReceived(GetBlocksPayload payload) - { - if (!localNode.ServiceEnabled) return; - if (Blockchain.Default == null) return; - UInt256 hash = payload.HashStart.Select(p => Blockchain.Default.GetHeader(p)).Where(p => p != null).OrderBy(p => p.Index).Select(p => p.Hash).FirstOrDefault(); - if (hash == null || hash == payload.HashStop) return; - List hashes = new List(); - do - { - hash = Blockchain.Default.GetNextBlockHash(hash); - if (hash == null) break; - hashes.Add(hash); - } while (hash != payload.HashStop && hashes.Count < 500); - EnqueueMessage("inv", InvPayload.Create(InventoryType.Block, hashes.ToArray())); - } - - private void OnGetDataMessageReceived(InvPayload payload) - { - foreach (UInt256 hash in payload.Hashes.Distinct()) - { - IInventory inventory; - if (!localNode.RelayCache.TryGet(hash, out inventory) && !localNode.ServiceEnabled) - continue; - switch (payload.Type) - { - case InventoryType.TX: - if (inventory == null) - inventory = LocalNode.GetTransaction(hash); - if (inventory == null && Blockchain.Default != null) - inventory = Blockchain.Default.GetTransaction(hash); - if (inventory != null) - EnqueueMessage("tx", inventory); - break; - case InventoryType.Block: - if (inventory == null && Blockchain.Default != null) - inventory = Blockchain.Default.GetBlock(hash); - if (inventory != null) - { - BloomFilter filter = bloom_filter; - if (filter == null) - { - EnqueueMessage("block", inventory); - } - else - { - Block block = (Block)inventory; - BitArray flags = new BitArray(block.Transactions.Select(p => TestFilter(filter, p)).ToArray()); - EnqueueMessage("merkleblock", MerkleBlockPayload.Create(block, flags)); - } - } - break; - case InventoryType.Consensus: - if (inventory != null) - EnqueueMessage("consensus", inventory); - break; - } - } - } - - private void OnGetHeadersMessageReceived(GetBlocksPayload payload) - { - if (!localNode.ServiceEnabled) return; - if (Blockchain.Default == null) return; - UInt256 hash = payload.HashStart.Select(p => Blockchain.Default.GetHeader(p)).Where(p => p != null).OrderBy(p => p.Index).Select(p => p.Hash).FirstOrDefault(); - if (hash == null || hash == payload.HashStop) return; - List
headers = new List
(); - do - { - hash = Blockchain.Default.GetNextBlockHash(hash); - if (hash == null) break; - headers.Add(Blockchain.Default.GetHeader(hash)); - } while (hash != payload.HashStop && headers.Count < 2000); - EnqueueMessage("headers", HeadersPayload.Create(headers)); - } - - private void OnHeadersMessageReceived(HeadersPayload payload) - { - if (Blockchain.Default == null) return; - Blockchain.Default.AddHeaders(payload.Headers); - if (Blockchain.Default.HeaderHeight < Version.StartHeight) - { - EnqueueMessage("getheaders", GetBlocksPayload.Create(Blockchain.Default.CurrentHeaderHash)); - } - } - - private void OnInventoryReceived(IInventory inventory) - { - lock (missions_global) - { - lock (missions) - { - missions_global.Remove(inventory.Hash); - missions.Remove(inventory.Hash); - if (missions.Count == 0) - mission_start = DateTime.Now.AddYears(100); - else - mission_start = DateTime.Now; - } - } - if (inventory is MinerTransaction) return; - InventoryReceived?.Invoke(this, inventory); - } - - private void OnInvMessageReceived(InvPayload payload) - { - if (payload.Type != InventoryType.TX && payload.Type != InventoryType.Block && payload.Type != InventoryType.Consensus) - return; - UInt256[] hashes = payload.Hashes.Distinct().ToArray(); - lock (LocalNode.KnownHashes) - { - hashes = hashes.Where(p => !LocalNode.KnownHashes.Contains(p)).ToArray(); - } - if (hashes.Length == 0) return; - lock (missions_global) - { - lock (missions) - { - if (localNode.GlobalMissionsEnabled) - hashes = hashes.Where(p => !missions_global.Contains(p)).ToArray(); - if (hashes.Length > 0) - { - if (missions.Count == 0) mission_start = DateTime.Now; - missions_global.UnionWith(hashes); - missions.UnionWith(hashes); - } - } - } - if (hashes.Length == 0) return; - EnqueueMessage("getdata", InvPayload.Create(payload.Type, hashes)); - } - - private void OnMemPoolMessageReceived() - { - EnqueueMessage("inv", InvPayload.Create(InventoryType.TX, LocalNode.GetMemoryPool().Select(p => p.Hash).ToArray())); - } - - private void OnMessageReceived(Message message) - { - switch (message.Command) - { - case "addr": - OnAddrMessageReceived(message.Payload.AsSerializable()); - break; - case "block": - OnInventoryReceived(message.Payload.AsSerializable()); - break; - case "consensus": - OnInventoryReceived(message.Payload.AsSerializable()); - break; - case "filteradd": - OnFilterAddMessageReceived(message.Payload.AsSerializable()); - break; - case "filterclear": - OnFilterClearMessageReceived(); - break; - case "filterload": - OnFilterLoadMessageReceived(message.Payload.AsSerializable()); - break; - case "getaddr": - OnGetAddrMessageReceived(); - break; - case "getblocks": - OnGetBlocksMessageReceived(message.Payload.AsSerializable()); - break; - case "getdata": - OnGetDataMessageReceived(message.Payload.AsSerializable()); - break; - case "getheaders": - OnGetHeadersMessageReceived(message.Payload.AsSerializable()); - break; - case "headers": - OnHeadersMessageReceived(message.Payload.AsSerializable()); - break; - case "inv": - OnInvMessageReceived(message.Payload.AsSerializable()); - break; - case "mempool": - OnMemPoolMessageReceived(); - break; - case "tx": - if (message.Payload.Length <= 1024 * 1024) - OnInventoryReceived(Transaction.DeserializeFrom(message.Payload)); - break; - case "verack": - case "version": - Disconnect(true); - break; - case "alert": - case "merkleblock": - case "notfound": - case "ping": - case "pong": - case "reject": - default: - //暂时忽略 - break; - } - } - - protected abstract Task ReceiveMessageAsync(TimeSpan timeout); - - internal bool Relay(IInventory data) - { - if (Version?.Relay != true) return false; - if (data.InventoryType == InventoryType.TX) - { - BloomFilter filter = bloom_filter; - if (filter != null && !TestFilter(filter, (Transaction)data)) - return false; - } - EnqueueMessage("inv", InvPayload.Create(data.InventoryType, data.Hash)); - return true; - } - - internal void Relay(IEnumerable transactions) - { - if (Version?.Relay != true) return; - BloomFilter filter = bloom_filter; - if (filter != null) - transactions = transactions.Where(p => TestFilter(filter, p)); - UInt256[] hashes = transactions.Select(p => p.Hash).ToArray(); - if (hashes.Length == 0) return; - EnqueueMessage("inv", InvPayload.Create(InventoryType.TX, hashes)); - } - - internal void RequestMemoryPool() - { - EnqueueMessage("mempool", null); - } - - internal void RequestPeers() - { - EnqueueMessage("getaddr", null); - } - - protected abstract Task SendMessageAsync(Message message); - - internal async void StartProtocol() - { -#if !NET47 - //There is a bug in .NET Core 2.0 that blocks async method which returns void. - await Task.Yield(); -#endif - if (!await SendMessageAsync(Message.Create("version", VersionPayload.Create(localNode.Port, localNode.Nonce, localNode.UserAgent)))) - return; - Message message = await ReceiveMessageAsync(HalfMinute); - if (message == null) return; - if (message.Command != "version") - { - Disconnect(true); - return; - } - try - { - Version = message.Payload.AsSerializable(); - } - catch (EndOfStreamException) - { - Disconnect(false); - return; - } - catch (FormatException) - { - Disconnect(true); - return; - } - if (Version.Nonce == localNode.Nonce) - { - Disconnect(true); - return; - } - bool isSelf; - lock (localNode.connectedPeers) - { - isSelf = localNode.connectedPeers.Where(p => p != this).Any(p => p.RemoteEndpoint.Address.Equals(RemoteEndpoint.Address) && p.Version?.Nonce == Version.Nonce); - } - if (isSelf) - { - Disconnect(false); - return; - } - if (ListenerEndpoint != null) - { - if (ListenerEndpoint.Port != Version.Port) - { - Disconnect(true); - return; - } - } - else if (Version.Port > 0) - { - ListenerEndpoint = new IPEndPoint(RemoteEndpoint.Address, Version.Port); - } - if (!await SendMessageAsync(Message.Create("verack"))) return; - message = await ReceiveMessageAsync(HalfMinute); - if (message == null) return; - if (message.Command != "verack") - { - Disconnect(true); - return; - } - if (Blockchain.Default?.HeaderHeight < Version.StartHeight) - { - EnqueueMessage("getheaders", GetBlocksPayload.Create(Blockchain.Default.CurrentHeaderHash)); - } - StartSendLoop(); - while (disposed == 0) - { - if (Blockchain.Default != null) - { - if (missions.Count == 0 && Blockchain.Default.Height < Version.StartHeight) - { - EnqueueMessage("getblocks", GetBlocksPayload.Create(Blockchain.Default.CurrentBlockHash)); - } - } - TimeSpan timeout = missions.Count == 0 ? HalfHour : OneMinute; - message = await ReceiveMessageAsync(timeout); - if (message == null) break; - if (DateTime.Now - mission_start > OneMinute - && message.Command != "block" && message.Command != "consensus" && message.Command != "tx") - { - Disconnect(false); - break; - } - try - { - OnMessageReceived(message); - } - catch (EndOfStreamException) - { - Disconnect(false); - break; - } - catch (FormatException) - { - Disconnect(true); - break; - } - } - } - - private async void StartSendLoop() - { -#if !NET47 - //There is a bug in .NET Core 2.0 that blocks async method which returns void. - await Task.Yield(); -#endif - while (disposed == 0) - { - Message message = null; - lock (message_queue_high) - { - if (message_queue_high.Count > 0) - { - message = message_queue_high.Dequeue(); - } - } - if (message == null) - { - lock (message_queue_low) - { - if (message_queue_low.Count > 0) - { - message = message_queue_low.Dequeue(); - } - } - } - if (message == null) - { - for (int i = 0; i < 10 && disposed == 0; i++) - { - Thread.Sleep(100); - } - } - else - { - await SendMessageAsync(message); - } - } - } - - private bool TestFilter(BloomFilter filter, Transaction tx) - { - if (filter.Check(tx.Hash.ToArray())) return true; - if (tx.Outputs.Any(p => filter.Check(p.ScriptHash.ToArray()))) return true; - if (tx.Inputs.Any(p => filter.Check(p.ToArray()))) return true; - if (tx.Scripts.Any(p => filter.Check(p.ScriptHash.ToArray()))) - return true; - if (tx.Type == TransactionType.RegisterTransaction) - { -#pragma warning disable CS0612 - RegisterTransaction asset = (RegisterTransaction)tx; - if (filter.Check(asset.Admin.ToArray())) return true; -#pragma warning restore CS0612 - } - return false; - } - } -} diff --git a/neo/Network/TcpRemoteNode.cs b/neo/Network/TcpRemoteNode.cs deleted file mode 100644 index e93cb02363..0000000000 --- a/neo/Network/TcpRemoteNode.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Neo.IO; -using System; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; - -namespace Neo.Network -{ - internal class TcpRemoteNode : RemoteNode - { - private Socket socket; - private NetworkStream stream; - private bool connected = false; - private int disposed = 0; - - public TcpRemoteNode(LocalNode localNode, IPEndPoint remoteEndpoint) - : base(localNode) - { - this.socket = new Socket(remoteEndpoint.Address.IsIPv4MappedToIPv6 ? AddressFamily.InterNetwork : remoteEndpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - this.ListenerEndpoint = remoteEndpoint; - } - - public TcpRemoteNode(LocalNode localNode, Socket socket) - : base(localNode) - { - this.socket = socket; - OnConnected(); - } - - public async Task ConnectAsync() - { - IPAddress address = ListenerEndpoint.Address; - if (address.IsIPv4MappedToIPv6) - address = address.MapToIPv4(); - try - { - await socket.ConnectAsync(address, ListenerEndpoint.Port); - OnConnected(); - } - catch (SocketException) - { - Disconnect(false); - return false; - } - return true; - } - - public override void Disconnect(bool error) - { - if (Interlocked.Exchange(ref disposed, 1) == 0) - { - if (stream != null) stream.Dispose(); - socket.Dispose(); - base.Disconnect(error); - } - } - - private void OnConnected() - { - IPEndPoint remoteEndpoint = (IPEndPoint)socket.RemoteEndPoint; - RemoteEndpoint = new IPEndPoint(remoteEndpoint.Address.MapToIPv6(), remoteEndpoint.Port); - stream = new NetworkStream(socket); - connected = true; - } - - protected override async Task ReceiveMessageAsync(TimeSpan timeout) - { - CancellationTokenSource source = new CancellationTokenSource(timeout); - //Stream.ReadAsync doesn't support CancellationToken - //see: https://stackoverflow.com/questions/20131434/cancel-networkstream-readasync-using-tcplistener - source.Token.Register(() => Disconnect(false)); - try - { - return await Message.DeserializeFromAsync(stream, source.Token); - } - catch (ArgumentException) { } - catch (ObjectDisposedException) { } - catch (Exception ex) when (ex is FormatException || ex is IOException || ex is OperationCanceledException) - { - Disconnect(false); - } - finally - { - source.Dispose(); - } - return null; - } - - protected override async Task SendMessageAsync(Message message) - { - if (!connected) throw new InvalidOperationException(); - if (disposed > 0) return false; - byte[] buffer = message.ToArray(); - CancellationTokenSource source = new CancellationTokenSource(10000); - //Stream.WriteAsync doesn't support CancellationToken - //see: https://stackoverflow.com/questions/20131434/cancel-networkstream-readasync-using-tcplistener - source.Token.Register(() => Disconnect(false)); - try - { - await stream.WriteAsync(buffer, 0, buffer.Length, source.Token); - return true; - } - catch (ObjectDisposedException) { } - catch (Exception ex) when (ex is IOException || ex is OperationCanceledException) - { - Disconnect(false); - } - finally - { - source.Dispose(); - } - return false; - } - } -} diff --git a/neo/Network/UPnP.cs b/neo/Network/UPnP.cs deleted file mode 100644 index f5e5419c6d..0000000000 --- a/neo/Network/UPnP.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; -using System.Xml; - -namespace Neo.Network -{ - public class UPnP - { - private static string _serviceUrl; - - public static TimeSpan TimeOut { get; set; } = TimeSpan.FromSeconds(3); - - public static async Task DiscoverAsync() - { - Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - s.ReceiveTimeout = (int)TimeOut.TotalMilliseconds; - s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1); - string req = "M-SEARCH * HTTP/1.1\r\n" + - "HOST: 239.255.255.250:1900\r\n" + - "ST:upnp:rootdevice\r\n" + - "MAN:\"ssdp:discover\"\r\n" + - "MX:3\r\n\r\n"; - byte[] data = Encoding.ASCII.GetBytes(req); - IPEndPoint ipe = new IPEndPoint(IPAddress.Broadcast, 1900); - - DateTime start = DateTime.Now; - - s.SendTo(data, ipe); - s.SendTo(data, ipe); - s.SendTo(data, ipe); - - byte[] buffer = new byte[0x1000]; - do - { - int length; - try - { - length = s.Receive(buffer); - } - catch (SocketException) - { - continue; - } - string resp = Encoding.ASCII.GetString(buffer, 0, length).ToLower(); - if (resp.Contains("upnp:rootdevice")) - { - resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9); - resp = resp.Substring(0, resp.IndexOf("\r")).Trim(); - if (!string.IsNullOrEmpty(_serviceUrl = await GetServiceUrlAsync(resp))) - { - return true; - } - } - } while (DateTime.Now - start < TimeOut); - return false; - } - - private static async Task GetServiceUrlAsync(string resp) - { - try - { - XmlDocument desc = new XmlDocument(); - HttpWebRequest request = WebRequest.CreateHttp(resp); - WebResponse response = await request.GetResponseAsync(); - desc.Load(response.GetResponseStream()); - XmlNamespaceManager nsMgr = new XmlNamespaceManager(desc.NameTable); - nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0"); - XmlNode typen = desc.SelectSingleNode("//tns:device/tns:deviceType/text()", nsMgr); - if (!typen.Value.Contains("InternetGatewayDevice")) - return null; - XmlNode node = desc.SelectSingleNode("//tns:service[contains(tns:serviceType,\"WANIPConnection\")]/tns:controlURL/text()", nsMgr); - if (node == null) - return null; - XmlNode eventnode = desc.SelectSingleNode("//tns:service[contains(tns:serviceType,\"WANIPConnection\")]/tns:eventSubURL/text()", nsMgr); - return CombineUrls(resp, node.Value); - } - catch { return null; } - } - - private static string CombineUrls(string resp, string p) - { - int n = resp.IndexOf("://"); - n = resp.IndexOf('/', n + 3); - return resp.Substring(0, n) + p; - } - - public static async Task ForwardPortAsync(int port, ProtocolType protocol, string description) - { - if (string.IsNullOrEmpty(_serviceUrl)) - throw new Exception("No UPnP service available or Discover() has not been called"); - XmlDocument xdoc = await SOAPRequestAsync(_serviceUrl, "" + - "" + port.ToString() + "" + protocol.ToString().ToUpper() + "" + - "" + port.ToString() + "" + (await Dns.GetHostAddressesAsync(Dns.GetHostName())).First(p => p.AddressFamily == AddressFamily.InterNetwork).ToString() + - "1" + description + - "0", "AddPortMapping"); - } - - public static async Task DeleteForwardingRuleAsync(int port, ProtocolType protocol) - { - if (string.IsNullOrEmpty(_serviceUrl)) - throw new Exception("No UPnP service available or Discover() has not been called"); - XmlDocument xdoc = await SOAPRequestAsync(_serviceUrl, - "" + - "" + - "" + - "" + port + "" + - "" + protocol.ToString().ToUpper() + "" + - "", "DeletePortMapping"); - } - - public static async Task GetExternalIPAsync() - { - if (string.IsNullOrEmpty(_serviceUrl)) - throw new Exception("No UPnP service available or Discover() has not been called"); - XmlDocument xdoc = await SOAPRequestAsync(_serviceUrl, "" + - "", "GetExternalIPAddress"); - XmlNamespaceManager nsMgr = new XmlNamespaceManager(xdoc.NameTable); - nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0"); - string IP = xdoc.SelectSingleNode("//NewExternalIPAddress/text()", nsMgr).Value; - return IPAddress.Parse(IP); - } - - private static async Task SOAPRequestAsync(string url, string soap, string function) - { - string req = "" + - "" + - "" + - soap + - "" + - ""; - HttpWebRequest r = WebRequest.CreateHttp(url); - r.Method = "POST"; - byte[] b = Encoding.UTF8.GetBytes(req); - r.Headers["SOAPACTION"] = "\"urn:schemas-upnp-org:service:WANIPConnection:1#" + function + "\""; - r.ContentType = "text/xml; charset=\"utf-8\""; - Stream reqs = await r.GetRequestStreamAsync(); - reqs.Write(b, 0, b.Length); - XmlDocument resp = new XmlDocument(); - WebResponse wres = await r.GetResponseAsync(); - Stream ress = wres.GetResponseStream(); - resp.Load(ress); - return resp; - } - } -} diff --git a/neo/Network/WebSocketRemoteNode.cs b/neo/Network/WebSocketRemoteNode.cs deleted file mode 100644 index 6969aa2391..0000000000 --- a/neo/Network/WebSocketRemoteNode.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Neo.IO; -using System; -using System.IO; -using System.Net; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; - -namespace Neo.Network -{ - internal class WebSocketRemoteNode : RemoteNode - { - private WebSocket socket; - private bool connected = false; - private int disposed = 0; - - public WebSocketRemoteNode(LocalNode localNode, WebSocket socket, IPEndPoint remoteEndpoint) - : base(localNode) - { - this.socket = socket; - this.RemoteEndpoint = new IPEndPoint(remoteEndpoint.Address.MapToIPv6(), remoteEndpoint.Port); - this.connected = true; - } - - public override void Disconnect(bool error) - { - if (Interlocked.Exchange(ref disposed, 1) == 0) - { - socket.Dispose(); - base.Disconnect(error); - } - } - - protected override async Task ReceiveMessageAsync(TimeSpan timeout) - { - CancellationTokenSource source = new CancellationTokenSource(timeout); - try - { - return await Message.DeserializeFromAsync(socket, source.Token); - } - catch (ArgumentException) { } - catch (ObjectDisposedException) { } - catch (Exception ex) when (ex is FormatException || ex is IOException || ex is WebSocketException || ex is OperationCanceledException) - { - Disconnect(false); - } - finally - { - source.Dispose(); - } - return null; - } - - protected override async Task SendMessageAsync(Message message) - { - if (!connected) throw new InvalidOperationException(); - if (disposed > 0) return false; - ArraySegment segment = new ArraySegment(message.ToArray()); - CancellationTokenSource source = new CancellationTokenSource(10000); - try - { - await socket.SendAsync(segment, WebSocketMessageType.Binary, true, source.Token); - return true; - } - catch (ObjectDisposedException) { } - catch (Exception ex) when (ex is WebSocketException || ex is OperationCanceledException) - { - Disconnect(false); - } - finally - { - source.Dispose(); - } - return false; - } - } -} diff --git a/neo/Settings.cs b/neo/Settings.cs deleted file mode 100644 index 41aa8e2950..0000000000 --- a/neo/Settings.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Neo.Core; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Neo -{ - internal class Settings - { - public uint Magic { get; private set; } - public byte AddressVersion { get; private set; } - public int MaxTransactionsPerBlock { get; private set; } - public string[] StandbyValidators { get; private set; } - public string[] SeedList { get; private set; } - public IReadOnlyDictionary SystemFee { get; private set; } - - public static Settings Default { get; private set; } - - static Settings() - { - IConfigurationSection section = new ConfigurationBuilder().AddJsonFile("protocol.json").Build().GetSection("ProtocolConfiguration"); - Default = new Settings(section); - } - - public Settings(IConfigurationSection section) - { - this.Magic = uint.Parse(section.GetSection("Magic").Value); - this.AddressVersion = byte.Parse(section.GetSection("AddressVersion").Value); - this.MaxTransactionsPerBlock = GetValueOrDefault(section.GetSection("MaxTransactionsPerBlock"), 500, p => int.Parse(p)); - this.StandbyValidators = section.GetSection("StandbyValidators").GetChildren().Select(p => p.Value).ToArray(); - this.SeedList = section.GetSection("SeedList").GetChildren().Select(p => p.Value).ToArray(); - this.SystemFee = section.GetSection("SystemFee").GetChildren().ToDictionary(p => (TransactionType)Enum.Parse(typeof(TransactionType), p.Key, true), p => Fixed8.Parse(p.Value)); - } - - public T GetValueOrDefault(IConfigurationSection section, T defaultValue, Func selector) - { - if (section.Value == null) return defaultValue; - return selector(section.Value); - } - } -} diff --git a/neo/SmartContract/ApplicationEngine.cs b/neo/SmartContract/ApplicationEngine.cs deleted file mode 100644 index b04ed4284d..0000000000 --- a/neo/SmartContract/ApplicationEngine.cs +++ /dev/null @@ -1,440 +0,0 @@ -using Neo.Core; -using Neo.IO.Caching; -using Neo.VM; -using Neo.VM.Types; -using System.Numerics; -using System.Text; - -namespace Neo.SmartContract -{ - public class ApplicationEngine : ExecutionEngine - { - #region Limits - /// - /// Set the max size allowed size for BigInteger - /// - private const int MaxSizeForBigInteger = 32; - /// - /// Set the max Stack Size - /// - private const uint MaxStackSize = 2 * 1024; - /// - /// Set Max Item Size - /// - private const uint MaxItemSize = 1024 * 1024; - /// - /// Set Max Invocation Stack Size - /// - private const uint MaxInvocationStackSize = 1024; - /// - /// Set Max Array Size - /// - private const uint MaxArraySize = 1024; - #endregion - - private const long ratio = 100000; - private const long gas_free = 10 * 100000000; - private readonly long gas_amount; - private long gas_consumed = 0; - private readonly bool testMode; - - private readonly CachedScriptTable script_table; - - public TriggerType Trigger { get; } - public Fixed8 GasConsumed => new Fixed8(gas_consumed); - - public ApplicationEngine(TriggerType trigger, IScriptContainer container, IScriptTable table, InteropService service, Fixed8 gas, bool testMode = false) - : base(container, Cryptography.Crypto.Default, table, service) - { - this.gas_amount = gas_free + gas.GetData(); - this.testMode = testMode; - this.Trigger = trigger; - if (table is CachedScriptTable) - { - this.script_table = (CachedScriptTable)table; - } - } - - private bool CheckArraySize(OpCode nextInstruction) - { - switch (nextInstruction) - { - case OpCode.PACK: - case OpCode.NEWARRAY: - case OpCode.NEWSTRUCT: - { - if (EvaluationStack.Count == 0) return false; - int size = (int)EvaluationStack.Peek().GetBigInteger(); - if (size > MaxArraySize) return false; - return true; - } - default: - return true; - } - } - - private bool CheckInvocationStack(OpCode nextInstruction) - { - switch (nextInstruction) - { - case OpCode.CALL: - case OpCode.APPCALL: - if (InvocationStack.Count >= MaxInvocationStackSize) return false; - return true; - default: - return true; - } - } - - private bool CheckItemSize(OpCode nextInstruction) - { - switch (nextInstruction) - { - case OpCode.PUSHDATA4: - { - if (CurrentContext.InstructionPointer + 4 >= CurrentContext.Script.Length) - return false; - uint length = CurrentContext.Script.ToUInt32(CurrentContext.InstructionPointer + 1); - if (length > MaxItemSize) return false; - return true; - } - case OpCode.CAT: - { - if (EvaluationStack.Count < 2) return false; - int length = EvaluationStack.Peek(0).GetByteArray().Length + EvaluationStack.Peek(1).GetByteArray().Length; - if (length > MaxItemSize) return false; - return true; - } - default: - return true; - } - } - - /// - /// Check if the BigInteger is allowed for numeric operations - /// - /// Value - /// Return True if are allowed, otherwise False - private bool CheckBigInteger(BigInteger value) - { - return value == null ? false : - value.ToByteArray().Length <= MaxSizeForBigInteger; - } - - /// - /// Check if the BigInteger is allowed for numeric operations - /// - private bool CheckBigIntegers(OpCode nextInstruction) - { - switch (nextInstruction) - { - case OpCode.INC: - { - BigInteger x = EvaluationStack.Peek().GetBigInteger(); - - if (!CheckBigInteger(x) || !CheckBigInteger(x + 1)) - return false; - - break; - } - case OpCode.DEC: - { - BigInteger x = EvaluationStack.Peek().GetBigInteger(); - - if (!CheckBigInteger(x) || (x.Sign <= 0 && !CheckBigInteger(x - 1))) - return false; - - break; - } - case OpCode.ADD: - { - BigInteger x2 = EvaluationStack.Peek().GetBigInteger(); - BigInteger x1 = EvaluationStack.Peek(1).GetBigInteger(); - - if (!CheckBigInteger(x2) || !CheckBigInteger(x1) || !CheckBigInteger(x1 + x2)) - return false; - - break; - } - case OpCode.SUB: - { - BigInteger x2 = EvaluationStack.Peek().GetBigInteger(); - BigInteger x1 = EvaluationStack.Peek(1).GetBigInteger(); - - if (!CheckBigInteger(x2) || !CheckBigInteger(x1) || !CheckBigInteger(x1 - x2)) - return false; - - break; - } - case OpCode.MUL: - { - BigInteger x2 = EvaluationStack.Peek().GetBigInteger(); - BigInteger x1 = EvaluationStack.Peek(1).GetBigInteger(); - - int lx1 = x1 == null ? 0 : x1.ToByteArray().Length; - - if (lx1 > MaxSizeForBigInteger) - return false; - - int lx2 = x2 == null ? 0 : x2.ToByteArray().Length; - - if ((lx1 + lx2) > MaxSizeForBigInteger) - return false; - - break; - } - case OpCode.DIV: - { - BigInteger x2 = EvaluationStack.Peek().GetBigInteger(); - BigInteger x1 = EvaluationStack.Peek(1).GetBigInteger(); - - if (!CheckBigInteger(x2) || !CheckBigInteger(x1)) - return false; - - break; - } - case OpCode.MOD: - { - BigInteger x2 = EvaluationStack.Peek().GetBigInteger(); - BigInteger x1 = EvaluationStack.Peek(1).GetBigInteger(); - - if (!CheckBigInteger(x2) || !CheckBigInteger(x1)) - return false; - - break; - } - } - - return true; - } - - private bool CheckStackSize(OpCode nextInstruction) - { - int size = 0; - if (nextInstruction <= OpCode.PUSH16) - size = 1; - else - switch (nextInstruction) - { - case OpCode.DEPTH: - case OpCode.DUP: - case OpCode.OVER: - case OpCode.TUCK: - size = 1; - break; - case OpCode.UNPACK: - StackItem item = EvaluationStack.Peek(); - if (item is Array array) - size = array.Count; - else - return false; - break; - } - if (size == 0) return true; - size += EvaluationStack.Count + AltStack.Count; - if (size > MaxStackSize) return false; - return true; - } - - private bool CheckDynamicInvoke(OpCode nextInstruction) - { - if (nextInstruction == OpCode.APPCALL || nextInstruction == OpCode.TAILCALL) - { - for (int i = CurrentContext.InstructionPointer + 1; i < CurrentContext.InstructionPointer + 21; i++) - { - if (CurrentContext.Script[i] != 0) return true; - } - // if we get this far it is a dynamic call - // now look at the current executing script - // to determine if it can do dynamic calls - ContractState contract = script_table.GetContractState(CurrentContext.ScriptHash); - return contract.HasDynamicInvoke; - } - return true; - } - - public new bool Execute() - { - try - { - while (!State.HasFlag(VMState.HALT) && !State.HasFlag(VMState.FAULT)) - { - if (CurrentContext.InstructionPointer < CurrentContext.Script.Length) - { - OpCode nextOpcode = CurrentContext.NextInstruction; - - gas_consumed = checked(gas_consumed + GetPrice(nextOpcode) * ratio); - if (!testMode && gas_consumed > gas_amount) - { - State |= VMState.FAULT; - return false; - } - - if (!CheckItemSize(nextOpcode) || - !CheckStackSize(nextOpcode) || - !CheckArraySize(nextOpcode) || - !CheckInvocationStack(nextOpcode) || - !CheckBigIntegers(nextOpcode) || - !CheckDynamicInvoke(nextOpcode)) - { - State |= VMState.FAULT; - return false; - } - } - StepInto(); - } - } - catch - { - State |= VMState.FAULT; - return false; - } - return !State.HasFlag(VMState.FAULT); - } - - protected virtual long GetPrice(OpCode nextInstruction) - { - if (nextInstruction <= OpCode.PUSH16) return 0; - switch (nextInstruction) - { - case OpCode.NOP: - return 0; - case OpCode.APPCALL: - case OpCode.TAILCALL: - return 10; - case OpCode.SYSCALL: - return GetPriceForSysCall(); - case OpCode.SHA1: - case OpCode.SHA256: - return 10; - case OpCode.HASH160: - case OpCode.HASH256: - return 20; - case OpCode.CHECKSIG: - return 100; - case OpCode.CHECKMULTISIG: - { - if (EvaluationStack.Count == 0) return 1; - int n = (int)EvaluationStack.Peek().GetBigInteger(); - if (n < 1) return 1; - return 100 * n; - } - default: return 1; - } - } - - protected virtual long GetPriceForSysCall() - { - if (CurrentContext.InstructionPointer >= CurrentContext.Script.Length - 3) - return 1; - byte length = CurrentContext.Script[CurrentContext.InstructionPointer + 1]; - if (CurrentContext.InstructionPointer > CurrentContext.Script.Length - length - 2) - return 1; - string api_name = Encoding.ASCII.GetString(CurrentContext.Script, CurrentContext.InstructionPointer + 2, length); - switch (api_name) - { - case "Neo.Runtime.CheckWitness": - case "AntShares.Runtime.CheckWitness": - return 200; - case "Neo.Blockchain.GetHeader": - case "AntShares.Blockchain.GetHeader": - return 100; - case "Neo.Blockchain.GetBlock": - case "AntShares.Blockchain.GetBlock": - return 200; - case "Neo.Blockchain.GetTransaction": - case "AntShares.Blockchain.GetTransaction": - return 100; - case "Neo.Blockchain.GetAccount": - case "AntShares.Blockchain.GetAccount": - return 100; - case "Neo.Blockchain.GetValidators": - case "AntShares.Blockchain.GetValidators": - return 200; - case "Neo.Blockchain.GetAsset": - case "AntShares.Blockchain.GetAsset": - return 100; - case "Neo.Blockchain.GetContract": - case "AntShares.Blockchain.GetContract": - return 100; - case "Neo.Transaction.GetReferences": - case "AntShares.Transaction.GetReferences": - case "Neo.Transaction.GetUnspentCoins": - return 200; - case "Neo.Account.SetVotes": - case "AntShares.Account.SetVotes": - return 1000; - case "Neo.Validator.Register": - case "AntShares.Validator.Register": - return 1000L * 100000000L / ratio; - case "Neo.Asset.Create": - case "AntShares.Asset.Create": - return 5000L * 100000000L / ratio; - case "Neo.Asset.Renew": - case "AntShares.Asset.Renew": - return (byte)EvaluationStack.Peek(1).GetBigInteger() * 5000L * 100000000L / ratio; - case "Neo.Contract.Create": - case "Neo.Contract.Migrate": - case "AntShares.Contract.Create": - case "AntShares.Contract.Migrate": - long fee = 100L; - - ContractPropertyState contract_properties = (ContractPropertyState)(byte)EvaluationStack.Peek(3).GetBigInteger(); - - if (contract_properties.HasFlag(ContractPropertyState.HasStorage)) - { - fee += 400L; - } - if (contract_properties.HasFlag(ContractPropertyState.HasDynamicInvoke)) - { - fee += 500L; - } - return fee * 100000000L / ratio; - case "Neo.Storage.Get": - case "AntShares.Storage.Get": - return 100; - case "Neo.Storage.Put": - case "AntShares.Storage.Put": - return ((EvaluationStack.Peek(1).GetByteArray().Length + EvaluationStack.Peek(2).GetByteArray().Length - 1) / 1024 + 1) * 1000; - case "Neo.Storage.Delete": - case "AntShares.Storage.Delete": - return 100; - default: - return 1; - } - } - - public static ApplicationEngine Run(byte[] script, IScriptContainer container = null, Block persisting_block = null) - { - if (persisting_block == null) - persisting_block = new Block - { - Version = 0, - PrevHash = Blockchain.Default.CurrentBlockHash, - MerkleRoot = new UInt256(), - Timestamp = Blockchain.Default.GetHeader(Blockchain.Default.Height).Timestamp + Blockchain.SecondsPerBlock, - Index = Blockchain.Default.Height + 1, - ConsensusData = 0, - NextConsensus = Blockchain.Default.GetHeader(Blockchain.Default.Height).NextConsensus, - Script = new Witness - { - InvocationScript = new byte[0], - VerificationScript = new byte[0] - }, - Transactions = new Transaction[0] - }; - DataCache accounts = Blockchain.Default.GetStates(); - DataCache assets = Blockchain.Default.GetStates(); - DataCache contracts = Blockchain.Default.GetStates(); - DataCache storages = Blockchain.Default.GetStates(); - CachedScriptTable script_table = new CachedScriptTable(contracts); - using (StateMachine service = new StateMachine(persisting_block, accounts, assets, contracts, storages)) - { - ApplicationEngine engine = new ApplicationEngine(TriggerType.Application, container, script_table, service, Fixed8.Zero, true); - engine.LoadScript(script, false); - engine.Execute(); - return engine; - } - } - } -} diff --git a/neo/SmartContract/CachedScriptTable.cs b/neo/SmartContract/CachedScriptTable.cs deleted file mode 100644 index ae28cb8665..0000000000 --- a/neo/SmartContract/CachedScriptTable.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Neo.Core; -using Neo.IO.Caching; -using Neo.VM; - -namespace Neo.SmartContract -{ - internal class CachedScriptTable : IScriptTable - { - private DataCache contracts; - - public CachedScriptTable(DataCache contracts) - { - this.contracts = contracts; - } - - byte[] IScriptTable.GetScript(byte[] script_hash) - { - return contracts[new UInt160(script_hash)].Script; - } - - public ContractState GetContractState(byte[] script_hash) - { - return contracts[new UInt160(script_hash)]; - } - } -} diff --git a/neo/SmartContract/Contract.cs b/neo/SmartContract/Contract.cs deleted file mode 100644 index 8c3997e953..0000000000 --- a/neo/SmartContract/Contract.cs +++ /dev/null @@ -1,157 +0,0 @@ -using Neo.Core; -using Neo.Cryptography.ECC; -using Neo.VM; -using Neo.Wallets; -using System; -using System.Linq; - -namespace Neo.SmartContract -{ - public class Contract - { - public byte[] Script; - public ContractParameterType[] ParameterList; - - private string _address; - /// - /// 合约地址 - /// - public string Address - { - get - { - if (_address == null) - { - _address = Wallet.ToAddress(ScriptHash); - } - return _address; - } - } - - public virtual bool IsStandard - { - get - { - if (Script.Length != 35) return false; - if (Script[0] != 33 || Script[34] != (byte)OpCode.CHECKSIG) - return false; - return true; - } - } - - private UInt160 _scriptHash; - public virtual UInt160 ScriptHash - { - get - { - if (_scriptHash == null) - { - _scriptHash = Script.ToScriptHash(); - } - return _scriptHash; - } - } - - public static Contract Create(ContractParameterType[] parameterList, byte[] redeemScript) - { - return new Contract - { - Script = redeemScript, - ParameterList = parameterList - }; - } - - public static Contract CreateMultiSigContract(int m, params ECPoint[] publicKeys) - { - return new Contract - { - Script = CreateMultiSigRedeemScript(m, publicKeys), - ParameterList = Enumerable.Repeat(ContractParameterType.Signature, m).ToArray() - }; - } - - public static byte[] CreateMultiSigRedeemScript(int m, params ECPoint[] publicKeys) - { - if (!(1 <= m && m <= publicKeys.Length && publicKeys.Length <= 1024)) - throw new ArgumentException(); - using (ScriptBuilder sb = new ScriptBuilder()) - { - sb.EmitPush(m); - foreach (ECPoint publicKey in publicKeys.OrderBy(p => p)) - { - sb.EmitPush(publicKey.EncodePoint(true)); - } - sb.EmitPush(publicKeys.Length); - sb.Emit(OpCode.CHECKMULTISIG); - return sb.ToArray(); - } - } - - public static Contract CreateSignatureContract(ECPoint publicKey) - { - return new Contract - { - Script = CreateSignatureRedeemScript(publicKey), - ParameterList = new[] { ContractParameterType.Signature } - }; - } - - public static byte[] CreateSignatureRedeemScript(ECPoint publicKey) - { - using (ScriptBuilder sb = new ScriptBuilder()) - { - sb.EmitPush(publicKey.EncodePoint(true)); - sb.Emit(OpCode.CHECKSIG); - return sb.ToArray(); - } - } - - public virtual bool IsMultiSigContract() - { - int m, n = 0; - int i = 0; - if (Script.Length < 37) return false; - if (Script[i] > (byte)OpCode.PUSH16) return false; - if (Script[i] < (byte)OpCode.PUSH1 && Script[i] != 1 && Script[i] != 2) return false; - switch (Script[i]) - { - case 1: - m = Script[++i]; - ++i; - break; - case 2: - m = Script.ToUInt16(++i); - i += 2; - break; - default: - m = Script[i++] - 80; - break; - } - if (m < 1 || m > 1024) return false; - while (Script[i] == 33) - { - i += 34; - if (Script.Length <= i) return false; - ++n; - } - if (n < m || n > 1024) return false; - switch (Script[i]) - { - case 1: - if (n != Script[++i]) return false; - ++i; - break; - case 2: - if (n != Script.ToUInt16(++i)) return false; - i += 2; - break; - default: - if (n != Script[i++] - 80) return false; - break; - } - if (Script[i++] != (byte)OpCode.CHECKMULTISIG) return false; - if (Script.Length != i) return false; - return true; - } - } -} diff --git a/neo/SmartContract/ContractParameter.cs b/neo/SmartContract/ContractParameter.cs deleted file mode 100644 index 800def6ce7..0000000000 --- a/neo/SmartContract/ContractParameter.cs +++ /dev/null @@ -1,184 +0,0 @@ -using Neo.Cryptography.ECC; -using Neo.IO.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Text; - -namespace Neo.SmartContract -{ - public class ContractParameter - { - public ContractParameterType Type; - public object Value; - - public ContractParameter() { } - - public ContractParameter(ContractParameterType type) - { - this.Type = type; - switch (type) - { - case ContractParameterType.Signature: - this.Value = new byte[64]; - break; - case ContractParameterType.Boolean: - this.Value = false; - break; - case ContractParameterType.Integer: - this.Value = 0; - break; - case ContractParameterType.Hash160: - this.Value = new UInt160(); - break; - case ContractParameterType.Hash256: - this.Value = new UInt256(); - break; - case ContractParameterType.ByteArray: - this.Value = new byte[0]; - break; - case ContractParameterType.PublicKey: - this.Value = ECCurve.Secp256r1.G; - break; - case ContractParameterType.String: - this.Value = ""; - break; - case ContractParameterType.Array: - this.Value = new List(); - break; - default: - throw new ArgumentException(); - } - } - - public static ContractParameter FromJson(JObject json) - { - ContractParameter parameter = new ContractParameter - { - Type = json["type"].AsEnum() - }; - JObject value = json["value"]; - if (value != null) - switch (parameter.Type) - { - case ContractParameterType.Signature: - case ContractParameterType.ByteArray: - parameter.Value = json["value"].AsString().HexToBytes(); - break; - case ContractParameterType.Boolean: - parameter.Value = json["value"].AsBoolean(); - break; - case ContractParameterType.Integer: - parameter.Value = BigInteger.Parse(json["value"].AsString()); - break; - case ContractParameterType.Hash160: - parameter.Value = UInt160.Parse(json["value"].AsString()); - break; - case ContractParameterType.Hash256: - parameter.Value = UInt256.Parse(json["value"].AsString()); - break; - case ContractParameterType.PublicKey: - parameter.Value = ECPoint.Parse(json["value"].AsString(), ECCurve.Secp256r1); - break; - case ContractParameterType.String: - parameter.Value = json["value"].AsString(); - break; - case ContractParameterType.Array: - parameter.Value = ((JArray)json["value"]).Select(p => FromJson(p)).ToArray(); - break; - default: - throw new ArgumentException(); - } - return parameter; - } - - public void SetValue(string text) - { - switch (Type) - { - case ContractParameterType.Signature: - byte[] signature = text.HexToBytes(); - if (signature.Length != 64) throw new FormatException(); - Value = signature; - break; - case ContractParameterType.Boolean: - Value = string.Equals(text, bool.TrueString, StringComparison.OrdinalIgnoreCase); - break; - case ContractParameterType.Integer: - Value = BigInteger.Parse(text); - break; - case ContractParameterType.Hash160: - Value = UInt160.Parse(text); - break; - case ContractParameterType.Hash256: - Value = UInt256.Parse(text); - break; - case ContractParameterType.ByteArray: - Value = text.HexToBytes(); - break; - case ContractParameterType.PublicKey: - Value = ECPoint.Parse(text, ECCurve.Secp256r1); - break; - case ContractParameterType.String: - Value = text; - break; - default: - throw new ArgumentException(); - } - } - - public JObject ToJson() - { - JObject json = new JObject(); - json["type"] = Type; - if (Value != null) - switch (Type) - { - case ContractParameterType.Signature: - case ContractParameterType.ByteArray: - json["value"] = ((byte[])Value).ToHexString(); - break; - case ContractParameterType.Boolean: - json["value"] = (bool)Value; - break; - case ContractParameterType.Integer: - case ContractParameterType.Hash160: - case ContractParameterType.Hash256: - case ContractParameterType.PublicKey: - case ContractParameterType.String: - json["value"] = Value.ToString(); - break; - case ContractParameterType.Array: - json["value"] = new JArray(((IList)Value).Select(p => p.ToJson())); - break; - } - return json; - } - - public override string ToString() - { - switch (Value) - { - case null: - return "(null)"; - case byte[] data: - return data.ToHexString(); - case IList data: - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (ContractParameter item in data) - { - sb.Append(item); - sb.Append(", "); - } - if (data.Count > 0) - sb.Length -= 2; - sb.Append(']'); - return sb.ToString(); - default: - return Value.ToString(); - } - } - } -} diff --git a/neo/SmartContract/ContractParameterType.cs b/neo/SmartContract/ContractParameterType.cs deleted file mode 100644 index 12b2b0534e..0000000000 --- a/neo/SmartContract/ContractParameterType.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Neo.SmartContract -{ - /// - /// 表示智能合约的参数类型 - /// - public enum ContractParameterType : byte - { - /// - /// 签名 - /// - Signature = 0x00, - Boolean = 0x01, - /// - /// 整数 - /// - Integer = 0x02, - /// - /// 160位散列值 - /// - Hash160 = 0x03, - /// - /// 256位散列值 - /// - Hash256 = 0x04, - /// - /// 字节数组 - /// - ByteArray = 0x05, - PublicKey = 0x06, - String = 0x07, - - Array = 0x10, - - InteropInterface = 0xf0, - - Void = 0xff - } -} diff --git a/neo/SmartContract/ContractParametersContext.cs b/neo/SmartContract/ContractParametersContext.cs deleted file mode 100644 index 350faf85a8..0000000000 --- a/neo/SmartContract/ContractParametersContext.cs +++ /dev/null @@ -1,259 +0,0 @@ -using Neo.Core; -using Neo.Cryptography.ECC; -using Neo.IO.Json; -using Neo.VM; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace Neo.SmartContract -{ - public class ContractParametersContext - { - private class ContextItem - { - public byte[] Script; - public ContractParameter[] Parameters; - public Dictionary Signatures; - - private ContextItem() { } - - public ContextItem(Contract contract) - { - this.Script = contract.Script; - this.Parameters = contract.ParameterList.Select(p => new ContractParameter { Type = p }).ToArray(); - } - - public static ContextItem FromJson(JObject json) - { - return new ContextItem - { - Script = json["script"]?.AsString().HexToBytes(), - Parameters = ((JArray)json["parameters"]).Select(p => ContractParameter.FromJson(p)).ToArray(), - Signatures = json["signatures"]?.Properties.Select(p => new - { - PublicKey = ECPoint.Parse(p.Key, ECCurve.Secp256r1), - Signature = p.Value.AsString().HexToBytes() - }).ToDictionary(p => p.PublicKey, p => p.Signature) - }; - } - - public JObject ToJson() - { - JObject json = new JObject(); - if (Script != null) - json["script"] = Script.ToHexString(); - json["parameters"] = new JArray(Parameters.Select(p => p.ToJson())); - if (Signatures != null) - { - json["signatures"] = new JObject(); - foreach (var signature in Signatures) - json["signatures"][signature.Key.ToString()] = signature.Value.ToHexString(); - } - return json; - } - } - - public readonly IVerifiable Verifiable; - private readonly Dictionary ContextItems; - - public bool Completed - { - get - { - if (ContextItems.Count < ScriptHashes.Count) - return false; - return ContextItems.Values.All(p => p != null && p.Parameters.All(q => q.Value != null)); - } - } - - private UInt160[] _ScriptHashes = null; - public IReadOnlyList ScriptHashes - { - get - { - if (_ScriptHashes == null) - { - _ScriptHashes = Verifiable.GetScriptHashesForVerifying(); - } - return _ScriptHashes; - } - } - - public ContractParametersContext(IVerifiable verifiable) - { - this.Verifiable = verifiable; - this.ContextItems = new Dictionary(); - } - - public bool Add(Contract contract, int index, object parameter) - { - ContextItem item = CreateItem(contract); - if (item == null) return false; - item.Parameters[index].Value = parameter; - return true; - } - - public bool AddSignature(Contract contract, ECPoint pubkey, byte[] signature) - { - if (contract.IsMultiSigContract()) - { - ContextItem item = CreateItem(contract); - if (item == null) return false; - if (item.Parameters.All(p => p.Value != null)) return false; - if (item.Signatures == null) - item.Signatures = new Dictionary(); - else if (item.Signatures.ContainsKey(pubkey)) - return false; - List points = new List(); - { - int i = 0; - switch (contract.Script[i++]) - { - case 1: - ++i; - break; - case 2: - i += 2; - break; - } - while (contract.Script[i++] == 33) - { - points.Add(ECPoint.DecodePoint(contract.Script.Skip(i).Take(33).ToArray(), ECCurve.Secp256r1)); - i += 33; - } - } - if (!points.Contains(pubkey)) return false; - item.Signatures.Add(pubkey, signature); - if (item.Signatures.Count == contract.ParameterList.Length) - { - Dictionary dic = points.Select((p, i) => new - { - PublicKey = p, - Index = i - }).ToDictionary(p => p.PublicKey, p => p.Index); - byte[][] sigs = item.Signatures.Select(p => new - { - Signature = p.Value, - Index = dic[p.Key] - }).OrderByDescending(p => p.Index).Select(p => p.Signature).ToArray(); - for (int i = 0; i < sigs.Length; i++) - if (!Add(contract, i, sigs[i])) - throw new InvalidOperationException(); - item.Signatures = null; - } - return true; - } - else - { - int index = -1; - for (int i = 0; i < contract.ParameterList.Length; i++) - if (contract.ParameterList[i] == ContractParameterType.Signature) - if (index >= 0) - throw new NotSupportedException(); - else - index = i; - - if(index == -1) { - // unable to find ContractParameterType.Signature in contract.ParameterList - // return now to prevent array index out of bounds exception - return false; - } - return Add(contract, index, signature); - } - } - - private ContextItem CreateItem(Contract contract) - { - if (ContextItems.TryGetValue(contract.ScriptHash, out ContextItem item)) - return item; - if (!ScriptHashes.Contains(contract.ScriptHash)) - return null; - item = new ContextItem(contract); - ContextItems.Add(contract.ScriptHash, item); - return item; - } - - public static ContractParametersContext FromJson(JObject json) - { - IVerifiable verifiable = typeof(ContractParametersContext).GetTypeInfo().Assembly.CreateInstance(json["type"].AsString()) as IVerifiable; - if (verifiable == null) throw new FormatException(); - using (MemoryStream ms = new MemoryStream(json["hex"].AsString().HexToBytes(), false)) - using (BinaryReader reader = new BinaryReader(ms, Encoding.UTF8)) - { - verifiable.DeserializeUnsigned(reader); - } - ContractParametersContext context = new ContractParametersContext(verifiable); - foreach (var property in json["items"].Properties) - { - context.ContextItems.Add(UInt160.Parse(property.Key), ContextItem.FromJson(property.Value)); - } - return context; - } - - public ContractParameter GetParameter(UInt160 scriptHash, int index) - { - return GetParameters(scriptHash)?[index]; - } - - public IReadOnlyList GetParameters(UInt160 scriptHash) - { - if (!ContextItems.TryGetValue(scriptHash, out ContextItem item)) - return null; - return item.Parameters; - } - - public Witness[] GetScripts() - { - if (!Completed) throw new InvalidOperationException(); - Witness[] scripts = new Witness[ScriptHashes.Count]; - for (int i = 0; i < ScriptHashes.Count; i++) - { - ContextItem item = ContextItems[ScriptHashes[i]]; - using (ScriptBuilder sb = new ScriptBuilder()) - { - foreach (ContractParameter parameter in item.Parameters.Reverse()) - { - sb.EmitPush(parameter); - } - scripts[i] = new Witness - { - InvocationScript = sb.ToArray(), - VerificationScript = item.Script ?? new byte[0] - }; - } - } - return scripts; - } - - public static ContractParametersContext Parse(string value) - { - return FromJson(JObject.Parse(value)); - } - - public JObject ToJson() - { - JObject json = new JObject(); - json["type"] = Verifiable.GetType().FullName; - using (MemoryStream ms = new MemoryStream()) - using (BinaryWriter writer = new BinaryWriter(ms, Encoding.UTF8)) - { - Verifiable.SerializeUnsigned(writer); - writer.Flush(); - json["hex"] = ms.ToArray().ToHexString(); - } - json["items"] = new JObject(); - foreach (var item in ContextItems) - json["items"][item.Key.ToString()] = item.Value.ToJson(); - return json; - } - - public override string ToString() - { - return ToJson().ToString(); - } - } -} diff --git a/neo/SmartContract/Iterator.cs b/neo/SmartContract/Iterator.cs deleted file mode 100644 index ad78c27bff..0000000000 --- a/neo/SmartContract/Iterator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Neo.VM; -using System; - -namespace Neo.SmartContract -{ - internal abstract class Iterator : IDisposable, IInteropInterface - { - public abstract void Dispose(); - public abstract StackItem Key(); - public abstract bool Next(); - public abstract StackItem Value(); - } -} diff --git a/neo/SmartContract/LogEventArgs.cs b/neo/SmartContract/LogEventArgs.cs deleted file mode 100644 index aba62be290..0000000000 --- a/neo/SmartContract/LogEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Neo.VM; -using System; - -namespace Neo.SmartContract -{ - public class LogEventArgs : EventArgs - { - public IScriptContainer ScriptContainer { get; } - public UInt160 ScriptHash { get; } - public string Message { get; } - - public LogEventArgs(IScriptContainer container, UInt160 script_hash, string message) - { - this.ScriptContainer = container; - this.ScriptHash = script_hash; - this.Message = message; - } - } -} diff --git a/neo/SmartContract/NotifyEventArgs.cs b/neo/SmartContract/NotifyEventArgs.cs deleted file mode 100644 index e40238d2af..0000000000 --- a/neo/SmartContract/NotifyEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Neo.VM; -using System; - -namespace Neo.SmartContract -{ - public class NotifyEventArgs : EventArgs - { - public IScriptContainer ScriptContainer { get; } - public UInt160 ScriptHash { get; } - public StackItem State { get; } - - public NotifyEventArgs(IScriptContainer container, UInt160 script_hash, StackItem state) - { - this.ScriptContainer = container; - this.ScriptHash = script_hash; - this.State = state; - } - } -} diff --git a/neo/SmartContract/StackItemType.cs b/neo/SmartContract/StackItemType.cs deleted file mode 100644 index 576f7c1d62..0000000000 --- a/neo/SmartContract/StackItemType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Neo.SmartContract -{ - internal enum StackItemType : byte - { - ByteArray = 0x00, - Boolean = 0x01, - Integer = 0x02, - InteropInterface = 0x40, - Array = 0x80, - Struct = 0x81, - } -} diff --git a/neo/SmartContract/StateMachine.cs b/neo/SmartContract/StateMachine.cs deleted file mode 100644 index a21a9624b8..0000000000 --- a/neo/SmartContract/StateMachine.cs +++ /dev/null @@ -1,297 +0,0 @@ -using Neo.Core; -using Neo.Cryptography.ECC; -using Neo.IO.Caching; -using Neo.VM; -using Neo.VM.Types; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Neo.SmartContract -{ - public class StateMachine : StateReader - { - private readonly Block persisting_block; - private readonly DataCache accounts; - private readonly DataCache assets; - private readonly DataCache contracts; - private readonly DataCache storages; - - private Dictionary contracts_created = new Dictionary(); - - protected override DataCache Accounts => accounts; - protected override DataCache Assets => assets; - protected override DataCache Contracts => contracts; - protected override DataCache Storages => storages; - - public StateMachine(Block persisting_block, DataCache accounts, DataCache assets, DataCache contracts, DataCache storages) - { - this.persisting_block = persisting_block; - this.accounts = accounts.CreateSnapshot(); - this.assets = assets.CreateSnapshot(); - this.contracts = contracts.CreateSnapshot(); - this.storages = storages.CreateSnapshot(); - Register("Neo.Asset.Create", Asset_Create); - Register("Neo.Asset.Renew", Asset_Renew); - Register("Neo.Contract.Create", Contract_Create); - Register("Neo.Contract.Migrate", Contract_Migrate); - Register("Neo.Contract.GetStorageContext", Contract_GetStorageContext); - Register("Neo.Contract.Destroy", Contract_Destroy); - Register("Neo.Storage.Put", Storage_Put); - Register("Neo.Storage.Delete", Storage_Delete); - #region Old AntShares APIs - Register("AntShares.Asset.Create", Asset_Create); - Register("AntShares.Asset.Renew", Asset_Renew); - Register("AntShares.Contract.Create", Contract_Create); - Register("AntShares.Contract.Migrate", Contract_Migrate); - Register("AntShares.Contract.GetStorageContext", Contract_GetStorageContext); - Register("AntShares.Contract.Destroy", Contract_Destroy); - Register("AntShares.Storage.Put", Storage_Put); - Register("AntShares.Storage.Delete", Storage_Delete); - #endregion - } - - public void Commit() - { - accounts.Commit(); - assets.Commit(); - contracts.Commit(); - storages.Commit(); - } - - protected override bool Runtime_GetTime(ExecutionEngine engine) - { - engine.EvaluationStack.Push(persisting_block.Timestamp); - return true; - } - - private bool Asset_Create(ExecutionEngine engine) - { - InvocationTransaction tx = (InvocationTransaction)engine.ScriptContainer; - AssetType asset_type = (AssetType)(byte)engine.EvaluationStack.Pop().GetBigInteger(); - if (!Enum.IsDefined(typeof(AssetType), asset_type) || asset_type == AssetType.CreditFlag || asset_type == AssetType.DutyFlag || asset_type == AssetType.GoverningToken || asset_type == AssetType.UtilityToken) - return false; - if (engine.EvaluationStack.Peek().GetByteArray().Length > 1024) - return false; - string name = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - Fixed8 amount = new Fixed8((long)engine.EvaluationStack.Pop().GetBigInteger()); - if (amount == Fixed8.Zero || amount < -Fixed8.Satoshi) return false; - if (asset_type == AssetType.Invoice && amount != -Fixed8.Satoshi) - return false; - byte precision = (byte)engine.EvaluationStack.Pop().GetBigInteger(); - if (precision > 8) return false; - if (asset_type == AssetType.Share && precision != 0) return false; - if (amount != -Fixed8.Satoshi && amount.GetData() % (long)Math.Pow(10, 8 - precision) != 0) - return false; - ECPoint owner = ECPoint.DecodePoint(engine.EvaluationStack.Pop().GetByteArray(), ECCurve.Secp256r1); - if (owner.IsInfinity) return false; - if (!CheckWitness(engine, owner)) - return false; - UInt160 admin = new UInt160(engine.EvaluationStack.Pop().GetByteArray()); - UInt160 issuer = new UInt160(engine.EvaluationStack.Pop().GetByteArray()); - AssetState asset = assets.GetOrAdd(tx.Hash, () => new AssetState - { - AssetId = tx.Hash, - AssetType = asset_type, - Name = name, - Amount = amount, - Available = Fixed8.Zero, - Precision = precision, - Fee = Fixed8.Zero, - FeeAddress = new UInt160(), - Owner = owner, - Admin = admin, - Issuer = issuer, - Expiration = Blockchain.Default.Height + 1 + 2000000, - IsFrozen = false - }); - engine.EvaluationStack.Push(StackItem.FromInterface(asset)); - return true; - } - - private bool Asset_Renew(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AssetState asset = _interface.GetInterface(); - if (asset == null) return false; - byte years = (byte)engine.EvaluationStack.Pop().GetBigInteger(); - asset = assets.GetAndChange(asset.AssetId); - if (asset.Expiration < Blockchain.Default.Height + 1) - asset.Expiration = Blockchain.Default.Height + 1; - try - { - asset.Expiration = checked(asset.Expiration + years * 2000000u); - } - catch (OverflowException) - { - asset.Expiration = uint.MaxValue; - } - engine.EvaluationStack.Push(asset.Expiration); - return true; - } - return false; - } - - private bool Contract_Create(ExecutionEngine engine) - { - byte[] script = engine.EvaluationStack.Pop().GetByteArray(); - if (script.Length > 1024 * 1024) return false; - ContractParameterType[] parameter_list = engine.EvaluationStack.Pop().GetByteArray().Select(p => (ContractParameterType)p).ToArray(); - if (parameter_list.Length > 252) return false; - ContractParameterType return_type = (ContractParameterType)(byte)engine.EvaluationStack.Pop().GetBigInteger(); - ContractPropertyState contract_properties = (ContractPropertyState)(byte)engine.EvaluationStack.Pop().GetBigInteger(); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 252) return false; - string name = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 252) return false; - string version = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 252) return false; - string author = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 252) return false; - string email = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 65536) return false; - string description = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - UInt160 hash = script.ToScriptHash(); - ContractState contract = contracts.TryGet(hash); - if (contract == null) - { - contract = new ContractState - { - Script = script, - ParameterList = parameter_list, - ReturnType = return_type, - ContractProperties = contract_properties, - Name = name, - CodeVersion = version, - Author = author, - Email = email, - Description = description - }; - contracts.Add(hash, contract); - contracts_created.Add(hash, new UInt160(engine.CurrentContext.ScriptHash)); - } - engine.EvaluationStack.Push(StackItem.FromInterface(contract)); - return true; - } - - private bool Contract_Migrate(ExecutionEngine engine) - { - byte[] script = engine.EvaluationStack.Pop().GetByteArray(); - if (script.Length > 1024 * 1024) return false; - ContractParameterType[] parameter_list = engine.EvaluationStack.Pop().GetByteArray().Select(p => (ContractParameterType)p).ToArray(); - if (parameter_list.Length > 252) return false; - ContractParameterType return_type = (ContractParameterType)(byte)engine.EvaluationStack.Pop().GetBigInteger(); - ContractPropertyState contract_properties = (ContractPropertyState)(byte)engine.EvaluationStack.Pop().GetBigInteger(); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 252) return false; - string name = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 252) return false; - string version = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 252) return false; - string author = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 252) return false; - string email = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - if (engine.EvaluationStack.Peek().GetByteArray().Length > 65536) return false; - string description = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - UInt160 hash = script.ToScriptHash(); - ContractState contract = contracts.TryGet(hash); - if (contract == null) - { - contract = new ContractState - { - Script = script, - ParameterList = parameter_list, - ReturnType = return_type, - ContractProperties = contract_properties, - Name = name, - CodeVersion = version, - Author = author, - Email = email, - Description = description - }; - contracts.Add(hash, contract); - contracts_created.Add(hash, new UInt160(engine.CurrentContext.ScriptHash)); - if (contract.HasStorage) - { - foreach (var pair in storages.Find(engine.CurrentContext.ScriptHash).ToArray()) - { - storages.Add(new StorageKey - { - ScriptHash = hash, - Key = pair.Key.Key - }, new StorageItem - { - Value = pair.Value.Value - }); - } - } - } - engine.EvaluationStack.Push(StackItem.FromInterface(contract)); - return Contract_Destroy(engine); - } - - private bool Contract_GetStorageContext(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - ContractState contract = _interface.GetInterface(); - if (!contracts_created.TryGetValue(contract.ScriptHash, out UInt160 created)) return false; - if (!created.Equals(new UInt160(engine.CurrentContext.ScriptHash))) return false; - engine.EvaluationStack.Push(StackItem.FromInterface(new StorageContext - { - ScriptHash = contract.ScriptHash - })); - return true; - } - return false; - } - - private bool Contract_Destroy(ExecutionEngine engine) - { - UInt160 hash = new UInt160(engine.CurrentContext.ScriptHash); - ContractState contract = contracts.TryGet(hash); - if (contract == null) return true; - contracts.Delete(hash); - if (contract.HasStorage) - foreach (var pair in storages.Find(hash.ToArray())) - storages.Delete(pair.Key); - return true; - } - - private bool Storage_Put(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - StorageContext context = _interface.GetInterface(); - if (!CheckStorageContext(context)) return false; - byte[] key = engine.EvaluationStack.Pop().GetByteArray(); - if (key.Length > 1024) return false; - byte[] value = engine.EvaluationStack.Pop().GetByteArray(); - storages.GetAndChange(new StorageKey - { - ScriptHash = context.ScriptHash, - Key = key - }, () => new StorageItem()).Value = value; - return true; - } - return false; - } - - private bool Storage_Delete(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - StorageContext context = _interface.GetInterface(); - if (!CheckStorageContext(context)) return false; - byte[] key = engine.EvaluationStack.Pop().GetByteArray(); - storages.Delete(new StorageKey - { - ScriptHash = context.ScriptHash, - Key = key - }); - return true; - } - return false; - } - } -} diff --git a/neo/SmartContract/StateReader.cs b/neo/SmartContract/StateReader.cs deleted file mode 100644 index 5ae79fc227..0000000000 --- a/neo/SmartContract/StateReader.cs +++ /dev/null @@ -1,993 +0,0 @@ -using Neo.Core; -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.IO.Caching; -using Neo.VM; -using Neo.VM.Types; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Text; -using VMArray = Neo.VM.Types.Array; -using VMBoolean = Neo.VM.Types.Boolean; - -namespace Neo.SmartContract -{ - public class StateReader : InteropService, IDisposable - { - public static event EventHandler Notify; - public static event EventHandler Log; - - private readonly List notifications = new List(); - private readonly List disposables = new List(); - - public IReadOnlyList Notifications => notifications; - - private DataCache _accounts; - protected virtual DataCache Accounts - { - get - { - if (_accounts == null) - _accounts = Blockchain.Default.GetStates(); - return _accounts; - } - } - - private DataCache _assets; - protected virtual DataCache Assets - { - get - { - if (_assets == null) - _assets = Blockchain.Default.GetStates(); - return _assets; - } - } - - private DataCache _contracts; - protected virtual DataCache Contracts - { - get - { - if (_contracts == null) - _contracts = Blockchain.Default.GetStates(); - return _contracts; - } - } - - private DataCache _storages; - protected virtual DataCache Storages - { - get - { - if (_storages == null) - _storages = Blockchain.Default.GetStates(); - return _storages; - } - } - - public StateReader() - { - Register("Neo.Runtime.GetTrigger", Runtime_GetTrigger); - Register("Neo.Runtime.CheckWitness", Runtime_CheckWitness); - Register("Neo.Runtime.Notify", Runtime_Notify); - Register("Neo.Runtime.Log", Runtime_Log); - Register("Neo.Runtime.GetTime", Runtime_GetTime); - Register("Neo.Runtime.Serialize", Runtime_Serialize); - Register("Neo.Runtime.Deserialize", Runtime_Deserialize); - Register("Neo.Blockchain.GetHeight", Blockchain_GetHeight); - Register("Neo.Blockchain.GetHeader", Blockchain_GetHeader); - Register("Neo.Blockchain.GetBlock", Blockchain_GetBlock); - Register("Neo.Blockchain.GetTransaction", Blockchain_GetTransaction); - Register("Neo.Blockchain.GetAccount", Blockchain_GetAccount); - Register("Neo.Blockchain.GetValidators", Blockchain_GetValidators); - Register("Neo.Blockchain.GetAsset", Blockchain_GetAsset); - Register("Neo.Blockchain.GetContract", Blockchain_GetContract); - Register("Neo.Header.GetIndex", Header_GetIndex); - Register("Neo.Header.GetHash", Header_GetHash); - Register("Neo.Header.GetVersion", Header_GetVersion); - Register("Neo.Header.GetPrevHash", Header_GetPrevHash); - Register("Neo.Header.GetMerkleRoot", Header_GetMerkleRoot); - Register("Neo.Header.GetTimestamp", Header_GetTimestamp); - Register("Neo.Header.GetConsensusData", Header_GetConsensusData); - Register("Neo.Header.GetNextConsensus", Header_GetNextConsensus); - Register("Neo.Block.GetTransactionCount", Block_GetTransactionCount); - Register("Neo.Block.GetTransactions", Block_GetTransactions); - Register("Neo.Block.GetTransaction", Block_GetTransaction); - Register("Neo.Transaction.GetHash", Transaction_GetHash); - Register("Neo.Transaction.GetType", Transaction_GetType); - Register("Neo.Transaction.GetAttributes", Transaction_GetAttributes); - Register("Neo.Transaction.GetInputs", Transaction_GetInputs); - Register("Neo.Transaction.GetOutputs", Transaction_GetOutputs); - Register("Neo.Transaction.GetReferences", Transaction_GetReferences); - Register("Neo.Transaction.GetUnspentCoins", Transaction_GetUnspentCoins); - Register("Neo.InvocationTransaction.GetScript", InvocationTransaction_GetScript); - Register("Neo.Attribute.GetUsage", Attribute_GetUsage); - Register("Neo.Attribute.GetData", Attribute_GetData); - Register("Neo.Input.GetHash", Input_GetHash); - Register("Neo.Input.GetIndex", Input_GetIndex); - Register("Neo.Output.GetAssetId", Output_GetAssetId); - Register("Neo.Output.GetValue", Output_GetValue); - Register("Neo.Output.GetScriptHash", Output_GetScriptHash); - Register("Neo.Account.GetScriptHash", Account_GetScriptHash); - Register("Neo.Account.GetVotes", Account_GetVotes); - Register("Neo.Account.GetBalance", Account_GetBalance); - Register("Neo.Asset.GetAssetId", Asset_GetAssetId); - Register("Neo.Asset.GetAssetType", Asset_GetAssetType); - Register("Neo.Asset.GetAmount", Asset_GetAmount); - Register("Neo.Asset.GetAvailable", Asset_GetAvailable); - Register("Neo.Asset.GetPrecision", Asset_GetPrecision); - Register("Neo.Asset.GetOwner", Asset_GetOwner); - Register("Neo.Asset.GetAdmin", Asset_GetAdmin); - Register("Neo.Asset.GetIssuer", Asset_GetIssuer); - Register("Neo.Contract.GetScript", Contract_GetScript); - Register("Neo.Storage.GetContext", Storage_GetContext); - Register("Neo.Storage.Get", Storage_Get); - Register("Neo.Storage.Find", Storage_Find); - Register("Neo.Iterator.Next", Iterator_Next); - Register("Neo.Iterator.Key", Iterator_Key); - Register("Neo.Iterator.Value", Iterator_Value); - #region Old AntShares APIs - Register("AntShares.Runtime.CheckWitness", Runtime_CheckWitness); - Register("AntShares.Runtime.Notify", Runtime_Notify); - Register("AntShares.Runtime.Log", Runtime_Log); - Register("AntShares.Blockchain.GetHeight", Blockchain_GetHeight); - Register("AntShares.Blockchain.GetHeader", Blockchain_GetHeader); - Register("AntShares.Blockchain.GetBlock", Blockchain_GetBlock); - Register("AntShares.Blockchain.GetTransaction", Blockchain_GetTransaction); - Register("AntShares.Blockchain.GetAccount", Blockchain_GetAccount); - Register("AntShares.Blockchain.GetValidators", Blockchain_GetValidators); - Register("AntShares.Blockchain.GetAsset", Blockchain_GetAsset); - Register("AntShares.Blockchain.GetContract", Blockchain_GetContract); - Register("AntShares.Header.GetHash", Header_GetHash); - Register("AntShares.Header.GetVersion", Header_GetVersion); - Register("AntShares.Header.GetPrevHash", Header_GetPrevHash); - Register("AntShares.Header.GetMerkleRoot", Header_GetMerkleRoot); - Register("AntShares.Header.GetTimestamp", Header_GetTimestamp); - Register("AntShares.Header.GetConsensusData", Header_GetConsensusData); - Register("AntShares.Header.GetNextConsensus", Header_GetNextConsensus); - Register("AntShares.Block.GetTransactionCount", Block_GetTransactionCount); - Register("AntShares.Block.GetTransactions", Block_GetTransactions); - Register("AntShares.Block.GetTransaction", Block_GetTransaction); - Register("AntShares.Transaction.GetHash", Transaction_GetHash); - Register("AntShares.Transaction.GetType", Transaction_GetType); - Register("AntShares.Transaction.GetAttributes", Transaction_GetAttributes); - Register("AntShares.Transaction.GetInputs", Transaction_GetInputs); - Register("AntShares.Transaction.GetOutputs", Transaction_GetOutputs); - Register("AntShares.Transaction.GetReferences", Transaction_GetReferences); - Register("AntShares.Attribute.GetUsage", Attribute_GetUsage); - Register("AntShares.Attribute.GetData", Attribute_GetData); - Register("AntShares.Input.GetHash", Input_GetHash); - Register("AntShares.Input.GetIndex", Input_GetIndex); - Register("AntShares.Output.GetAssetId", Output_GetAssetId); - Register("AntShares.Output.GetValue", Output_GetValue); - Register("AntShares.Output.GetScriptHash", Output_GetScriptHash); - Register("AntShares.Account.GetScriptHash", Account_GetScriptHash); - Register("AntShares.Account.GetVotes", Account_GetVotes); - Register("AntShares.Account.GetBalance", Account_GetBalance); - Register("AntShares.Asset.GetAssetId", Asset_GetAssetId); - Register("AntShares.Asset.GetAssetType", Asset_GetAssetType); - Register("AntShares.Asset.GetAmount", Asset_GetAmount); - Register("AntShares.Asset.GetAvailable", Asset_GetAvailable); - Register("AntShares.Asset.GetPrecision", Asset_GetPrecision); - Register("AntShares.Asset.GetOwner", Asset_GetOwner); - Register("AntShares.Asset.GetAdmin", Asset_GetAdmin); - Register("AntShares.Asset.GetIssuer", Asset_GetIssuer); - Register("AntShares.Contract.GetScript", Contract_GetScript); - Register("AntShares.Storage.GetContext", Storage_GetContext); - Register("AntShares.Storage.Get", Storage_Get); - #endregion - } - - internal bool CheckStorageContext(StorageContext context) - { - ContractState contract = Contracts.TryGet(context.ScriptHash); - if (contract == null) return false; - if (!contract.HasStorage) return false; - return true; - } - - public void Dispose() - { - foreach (IDisposable disposable in disposables) - disposable.Dispose(); - disposables.Clear(); - } - - protected virtual bool Runtime_GetTrigger(ExecutionEngine engine) - { - ApplicationEngine app_engine = (ApplicationEngine)engine; - engine.EvaluationStack.Push((int)app_engine.Trigger); - return true; - } - - protected bool CheckWitness(ExecutionEngine engine, UInt160 hash) - { - IVerifiable container = (IVerifiable)engine.ScriptContainer; - UInt160[] _hashes_for_verifying = container.GetScriptHashesForVerifying(); - return _hashes_for_verifying.Contains(hash); - } - - protected bool CheckWitness(ExecutionEngine engine, ECPoint pubkey) - { - return CheckWitness(engine, Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash()); - } - - protected virtual bool Runtime_CheckWitness(ExecutionEngine engine) - { - byte[] hashOrPubkey = engine.EvaluationStack.Pop().GetByteArray(); - bool result; - if (hashOrPubkey.Length == 20) - result = CheckWitness(engine, new UInt160(hashOrPubkey)); - else if (hashOrPubkey.Length == 33) - result = CheckWitness(engine, ECPoint.DecodePoint(hashOrPubkey, ECCurve.Secp256r1)); - else - return false; - engine.EvaluationStack.Push(result); - return true; - } - - protected virtual bool Runtime_Notify(ExecutionEngine engine) - { - StackItem state = engine.EvaluationStack.Pop(); - NotifyEventArgs notification = new NotifyEventArgs(engine.ScriptContainer, new UInt160(engine.CurrentContext.ScriptHash), state); - Notify?.Invoke(this, notification); - notifications.Add(notification); - return true; - } - - protected virtual bool Runtime_Log(ExecutionEngine engine) - { - string message = Encoding.UTF8.GetString(engine.EvaluationStack.Pop().GetByteArray()); - Log?.Invoke(this, new LogEventArgs(engine.ScriptContainer, new UInt160(engine.CurrentContext.ScriptHash), message)); - return true; - } - - protected virtual bool Runtime_GetTime(ExecutionEngine engine) - { - BlockBase header = Blockchain.Default?.GetHeader(Blockchain.Default.Height); - if (header == null) header = Blockchain.GenesisBlock; - engine.EvaluationStack.Push(header.Timestamp + Blockchain.SecondsPerBlock); - return true; - } - - private void SerializeStackItem(StackItem item, BinaryWriter writer) - { - switch (item) - { - case ByteArray _: - writer.Write((byte)StackItemType.ByteArray); - writer.WriteVarBytes(item.GetByteArray()); - break; - case VMBoolean _: - writer.Write((byte)StackItemType.Boolean); - writer.Write(item.GetBoolean()); - break; - case Integer _: - writer.Write((byte)StackItemType.Integer); - writer.WriteVarBytes(item.GetByteArray()); - break; - case InteropInterface _: - throw new NotSupportedException(); - case VMArray array: - if (array is Struct) - writer.Write((byte)StackItemType.Struct); - else - writer.Write((byte)StackItemType.Array); - writer.WriteVarInt(array.Count); - foreach (StackItem subitem in array) - SerializeStackItem(subitem, writer); - break; - } - } - - protected virtual bool Runtime_Serialize(ExecutionEngine engine) - { - using (MemoryStream ms = new MemoryStream()) - using (BinaryWriter writer = new BinaryWriter(ms)) - { - try - { - SerializeStackItem(engine.EvaluationStack.Pop(), writer); - } - catch (NotSupportedException) - { - return false; - } - writer.Flush(); - engine.EvaluationStack.Push(ms.ToArray()); - } - return true; - } - - private StackItem DeserializeStackItem(BinaryReader reader) - { - StackItemType type = (StackItemType)reader.ReadByte(); - switch (type) - { - case StackItemType.ByteArray: - return new ByteArray(reader.ReadVarBytes()); - case StackItemType.Boolean: - return new VMBoolean(reader.ReadBoolean()); - case StackItemType.Integer: - return new Integer(new BigInteger(reader.ReadVarBytes())); - case StackItemType.Array: - case StackItemType.Struct: - VMArray array = type == StackItemType.Struct ? new Struct() : new VMArray(); - ulong count = reader.ReadVarInt(); - while (count-- > 0) - array.Add(DeserializeStackItem(reader)); - return array; - default: - return null; - } - } - - protected virtual bool Runtime_Deserialize(ExecutionEngine engine) - { - byte[] data = engine.EvaluationStack.Pop().GetByteArray(); - using (MemoryStream ms = new MemoryStream(data, false)) - using (BinaryReader reader = new BinaryReader(ms)) - { - StackItem item = DeserializeStackItem(reader); - if (item == null) return false; - engine.EvaluationStack.Push(item); - } - return true; - } - - protected virtual bool Blockchain_GetHeight(ExecutionEngine engine) - { - if (Blockchain.Default == null) - engine.EvaluationStack.Push(0); - else - engine.EvaluationStack.Push(Blockchain.Default.Height); - return true; - } - - protected virtual bool Blockchain_GetHeader(ExecutionEngine engine) - { - byte[] data = engine.EvaluationStack.Pop().GetByteArray(); - Header header; - if (data.Length <= 5) - { - uint height = (uint)new BigInteger(data); - if (Blockchain.Default != null) - header = Blockchain.Default.GetHeader(height); - else if (height == 0) - header = Blockchain.GenesisBlock.Header; - else - header = null; - } - else if (data.Length == 32) - { - UInt256 hash = new UInt256(data); - if (Blockchain.Default != null) - header = Blockchain.Default.GetHeader(hash); - else if (hash == Blockchain.GenesisBlock.Hash) - header = Blockchain.GenesisBlock.Header; - else - header = null; - } - else - { - return false; - } - engine.EvaluationStack.Push(StackItem.FromInterface(header)); - return true; - } - - protected virtual bool Blockchain_GetBlock(ExecutionEngine engine) - { - byte[] data = engine.EvaluationStack.Pop().GetByteArray(); - Block block; - if (data.Length <= 5) - { - uint height = (uint)new BigInteger(data); - if (Blockchain.Default != null) - block = Blockchain.Default.GetBlock(height); - else if (height == 0) - block = Blockchain.GenesisBlock; - else - block = null; - } - else if (data.Length == 32) - { - UInt256 hash = new UInt256(data); - if (Blockchain.Default != null) - block = Blockchain.Default.GetBlock(hash); - else if (hash == Blockchain.GenesisBlock.Hash) - block = Blockchain.GenesisBlock; - else - block = null; - } - else - { - return false; - } - engine.EvaluationStack.Push(StackItem.FromInterface(block)); - return true; - } - - protected virtual bool Blockchain_GetTransaction(ExecutionEngine engine) - { - byte[] hash = engine.EvaluationStack.Pop().GetByteArray(); - Transaction tx = Blockchain.Default?.GetTransaction(new UInt256(hash)); - engine.EvaluationStack.Push(StackItem.FromInterface(tx)); - return true; - } - - protected virtual bool Blockchain_GetAccount(ExecutionEngine engine) - { - UInt160 hash = new UInt160(engine.EvaluationStack.Pop().GetByteArray()); - AccountState account = Accounts.GetOrAdd(hash, () => new AccountState(hash)); - engine.EvaluationStack.Push(StackItem.FromInterface(account)); - return true; - } - - protected virtual bool Blockchain_GetValidators(ExecutionEngine engine) - { - ECPoint[] validators = Blockchain.Default.GetValidators(); - engine.EvaluationStack.Push(validators.Select(p => (StackItem)p.EncodePoint(true)).ToArray()); - return true; - } - - protected virtual bool Blockchain_GetAsset(ExecutionEngine engine) - { - UInt256 hash = new UInt256(engine.EvaluationStack.Pop().GetByteArray()); - AssetState asset = Assets.TryGet(hash); - if (asset == null) return false; - engine.EvaluationStack.Push(StackItem.FromInterface(asset)); - return true; - } - - protected virtual bool Blockchain_GetContract(ExecutionEngine engine) - { - UInt160 hash = new UInt160(engine.EvaluationStack.Pop().GetByteArray()); - ContractState contract = Contracts.TryGet(hash); - if (contract == null) return false; - engine.EvaluationStack.Push(StackItem.FromInterface(contract)); - return true; - } - - protected virtual bool Header_GetIndex(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - BlockBase header = _interface.GetInterface(); - if (header == null) return false; - engine.EvaluationStack.Push(header.Index); - return true; - } - return false; - } - - protected virtual bool Header_GetHash(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - BlockBase header = _interface.GetInterface(); - if (header == null) return false; - engine.EvaluationStack.Push(header.Hash.ToArray()); - return true; - } - return false; - } - - protected virtual bool Header_GetVersion(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - BlockBase header = _interface.GetInterface(); - if (header == null) return false; - engine.EvaluationStack.Push(header.Version); - return true; - } - return false; - } - - protected virtual bool Header_GetPrevHash(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - BlockBase header = _interface.GetInterface(); - if (header == null) return false; - engine.EvaluationStack.Push(header.PrevHash.ToArray()); - return true; - } - return false; - } - - protected virtual bool Header_GetMerkleRoot(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - BlockBase header = _interface.GetInterface(); - if (header == null) return false; - engine.EvaluationStack.Push(header.MerkleRoot.ToArray()); - return true; - } - return false; - } - - protected virtual bool Header_GetTimestamp(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - BlockBase header = _interface.GetInterface(); - if (header == null) return false; - engine.EvaluationStack.Push(header.Timestamp); - return true; - } - return false; - } - - protected virtual bool Header_GetConsensusData(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - BlockBase header = _interface.GetInterface(); - if (header == null) return false; - engine.EvaluationStack.Push(header.ConsensusData); - return true; - } - return false; - } - - protected virtual bool Header_GetNextConsensus(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - BlockBase header = _interface.GetInterface(); - if (header == null) return false; - engine.EvaluationStack.Push(header.NextConsensus.ToArray()); - return true; - } - return false; - } - - protected virtual bool Block_GetTransactionCount(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Block block = _interface.GetInterface(); - if (block == null) return false; - engine.EvaluationStack.Push(block.Transactions.Length); - return true; - } - return false; - } - - protected virtual bool Block_GetTransactions(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Block block = _interface.GetInterface(); - if (block == null) return false; - engine.EvaluationStack.Push(block.Transactions.Select(p => StackItem.FromInterface(p)).ToArray()); - return true; - } - return false; - } - - protected virtual bool Block_GetTransaction(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Block block = _interface.GetInterface(); - int index = (int)engine.EvaluationStack.Pop().GetBigInteger(); - if (block == null) return false; - if (index < 0 || index >= block.Transactions.Length) return false; - Transaction tx = block.Transactions[index]; - engine.EvaluationStack.Push(StackItem.FromInterface(tx)); - return true; - } - return false; - } - - protected virtual bool Transaction_GetHash(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Transaction tx = _interface.GetInterface(); - if (tx == null) return false; - engine.EvaluationStack.Push(tx.Hash.ToArray()); - return true; - } - return false; - } - - protected virtual bool Transaction_GetType(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Transaction tx = _interface.GetInterface(); - if (tx == null) return false; - engine.EvaluationStack.Push((int)tx.Type); - return true; - } - return false; - } - - protected virtual bool Transaction_GetAttributes(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Transaction tx = _interface.GetInterface(); - if (tx == null) return false; - engine.EvaluationStack.Push(tx.Attributes.Select(p => StackItem.FromInterface(p)).ToArray()); - return true; - } - return false; - } - - protected virtual bool Transaction_GetInputs(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Transaction tx = _interface.GetInterface(); - if (tx == null) return false; - engine.EvaluationStack.Push(tx.Inputs.Select(p => StackItem.FromInterface(p)).ToArray()); - return true; - } - return false; - } - - protected virtual bool Transaction_GetOutputs(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Transaction tx = _interface.GetInterface(); - if (tx == null) return false; - engine.EvaluationStack.Push(tx.Outputs.Select(p => StackItem.FromInterface(p)).ToArray()); - return true; - } - return false; - } - - protected virtual bool Transaction_GetReferences(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Transaction tx = _interface.GetInterface(); - if (tx == null) return false; - engine.EvaluationStack.Push(tx.Inputs.Select(p => StackItem.FromInterface(tx.References[p])).ToArray()); - return true; - } - return false; - } - - protected virtual bool Transaction_GetUnspentCoins(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Transaction tx = _interface.GetInterface(); - if (tx == null) return false; - engine.EvaluationStack.Push(Blockchain.Default.GetUnspent(tx.Hash).Select(p => StackItem.FromInterface(p)).ToArray()); - return true; - } - return false; - } - - protected virtual bool InvocationTransaction_GetScript(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - InvocationTransaction tx = _interface.GetInterface(); - if (tx == null) return false; - engine.EvaluationStack.Push(tx.Script); - return true; - } - return false; - } - - protected virtual bool Attribute_GetUsage(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - TransactionAttribute attr = _interface.GetInterface(); - if (attr == null) return false; - engine.EvaluationStack.Push((int)attr.Usage); - return true; - } - return false; - } - - protected virtual bool Attribute_GetData(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - TransactionAttribute attr = _interface.GetInterface(); - if (attr == null) return false; - engine.EvaluationStack.Push(attr.Data); - return true; - } - return false; - } - - protected virtual bool Input_GetHash(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - CoinReference input = _interface.GetInterface(); - if (input == null) return false; - engine.EvaluationStack.Push(input.PrevHash.ToArray()); - return true; - } - return false; - } - - protected virtual bool Input_GetIndex(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - CoinReference input = _interface.GetInterface(); - if (input == null) return false; - engine.EvaluationStack.Push((int)input.PrevIndex); - return true; - } - return false; - } - - protected virtual bool Output_GetAssetId(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - TransactionOutput output = _interface.GetInterface(); - if (output == null) return false; - engine.EvaluationStack.Push(output.AssetId.ToArray()); - return true; - } - return false; - } - - protected virtual bool Output_GetValue(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - TransactionOutput output = _interface.GetInterface(); - if (output == null) return false; - engine.EvaluationStack.Push(output.Value.GetData()); - return true; - } - return false; - } - - protected virtual bool Output_GetScriptHash(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - TransactionOutput output = _interface.GetInterface(); - if (output == null) return false; - engine.EvaluationStack.Push(output.ScriptHash.ToArray()); - return true; - } - return false; - } - - protected virtual bool Account_GetScriptHash(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AccountState account = _interface.GetInterface(); - if (account == null) return false; - engine.EvaluationStack.Push(account.ScriptHash.ToArray()); - return true; - } - return false; - } - - protected virtual bool Account_GetVotes(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AccountState account = _interface.GetInterface(); - if (account == null) return false; - engine.EvaluationStack.Push(account.Votes.Select(p => (StackItem)p.EncodePoint(true)).ToArray()); - return true; - } - return false; - } - - protected virtual bool Account_GetBalance(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AccountState account = _interface.GetInterface(); - UInt256 asset_id = new UInt256(engine.EvaluationStack.Pop().GetByteArray()); - if (account == null) return false; - Fixed8 balance = account.Balances.TryGetValue(asset_id, out Fixed8 value) ? value : Fixed8.Zero; - engine.EvaluationStack.Push(balance.GetData()); - return true; - } - return false; - } - - protected virtual bool Asset_GetAssetId(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AssetState asset = _interface.GetInterface(); - if (asset == null) return false; - engine.EvaluationStack.Push(asset.AssetId.ToArray()); - return true; - } - return false; - } - - protected virtual bool Asset_GetAssetType(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AssetState asset = _interface.GetInterface(); - if (asset == null) return false; - engine.EvaluationStack.Push((int)asset.AssetType); - return true; - } - return false; - } - - protected virtual bool Asset_GetAmount(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AssetState asset = _interface.GetInterface(); - if (asset == null) return false; - engine.EvaluationStack.Push(asset.Amount.GetData()); - return true; - } - return false; - } - - protected virtual bool Asset_GetAvailable(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AssetState asset = _interface.GetInterface(); - if (asset == null) return false; - engine.EvaluationStack.Push(asset.Available.GetData()); - return true; - } - return false; - } - - protected virtual bool Asset_GetPrecision(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AssetState asset = _interface.GetInterface(); - if (asset == null) return false; - engine.EvaluationStack.Push((int)asset.Precision); - return true; - } - return false; - } - - protected virtual bool Asset_GetOwner(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AssetState asset = _interface.GetInterface(); - if (asset == null) return false; - engine.EvaluationStack.Push(asset.Owner.EncodePoint(true)); - return true; - } - return false; - } - - protected virtual bool Asset_GetAdmin(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AssetState asset = _interface.GetInterface(); - if (asset == null) return false; - engine.EvaluationStack.Push(asset.Admin.ToArray()); - return true; - } - return false; - } - - protected virtual bool Asset_GetIssuer(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - AssetState asset = _interface.GetInterface(); - if (asset == null) return false; - engine.EvaluationStack.Push(asset.Issuer.ToArray()); - return true; - } - return false; - } - - protected virtual bool Contract_GetScript(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - ContractState contract = _interface.GetInterface(); - if (contract == null) return false; - engine.EvaluationStack.Push(contract.Script); - return true; - } - return false; - } - - protected virtual bool Storage_GetContext(ExecutionEngine engine) - { - engine.EvaluationStack.Push(StackItem.FromInterface(new StorageContext - { - ScriptHash = new UInt160(engine.CurrentContext.ScriptHash) - })); - return true; - } - - protected virtual bool Storage_Get(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - StorageContext context = _interface.GetInterface(); - if (!CheckStorageContext(context)) return false; - byte[] key = engine.EvaluationStack.Pop().GetByteArray(); - StorageItem item = Storages.TryGet(new StorageKey - { - ScriptHash = context.ScriptHash, - Key = key - }); - engine.EvaluationStack.Push(item?.Value ?? new byte[0]); - return true; - } - return false; - } - - protected virtual bool Storage_Find(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - StorageContext context = _interface.GetInterface(); - if (!CheckStorageContext(context)) return false; - byte[] prefix = engine.EvaluationStack.Pop().GetByteArray(); - prefix = context.ScriptHash.ToArray().Concat(prefix).ToArray(); - StorageIterator iterator = new StorageIterator(Storages.Find(prefix).GetEnumerator()); - engine.EvaluationStack.Push(StackItem.FromInterface(iterator)); - disposables.Add(iterator); - return true; - } - return false; - } - - protected virtual bool Iterator_Next(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Iterator iterator = _interface.GetInterface(); - engine.EvaluationStack.Push(iterator.Next()); - return true; - } - return false; - } - - protected virtual bool Iterator_Key(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Iterator iterator = _interface.GetInterface(); - engine.EvaluationStack.Push(iterator.Key()); - return true; - } - return false; - } - - protected virtual bool Iterator_Value(ExecutionEngine engine) - { - if (engine.EvaluationStack.Pop() is InteropInterface _interface) - { - Iterator iterator = _interface.GetInterface(); - engine.EvaluationStack.Push(iterator.Value()); - return true; - } - return false; - } - } -} diff --git a/neo/SmartContract/StorageContext.cs b/neo/SmartContract/StorageContext.cs deleted file mode 100644 index 637811bdfe..0000000000 --- a/neo/SmartContract/StorageContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Neo.VM; - -namespace Neo.SmartContract -{ - internal class StorageContext : IInteropInterface - { - public UInt160 ScriptHash; - - public byte[] ToArray() - { - return ScriptHash.ToArray(); - } - } -} diff --git a/neo/SmartContract/StorageIterator.cs b/neo/SmartContract/StorageIterator.cs deleted file mode 100644 index 0aa5d4873d..0000000000 --- a/neo/SmartContract/StorageIterator.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Neo.Core; -using Neo.VM; -using System.Collections.Generic; - -namespace Neo.SmartContract -{ - internal class StorageIterator : Iterator - { - private readonly IEnumerator> enumerator; - - public StorageIterator(IEnumerator> enumerator) - { - this.enumerator = enumerator; - } - - public override void Dispose() - { - enumerator.Dispose(); - } - - public override StackItem Key() - { - return enumerator.Current.Key.Key; - } - - public override bool Next() - { - return enumerator.MoveNext(); - } - - public override StackItem Value() - { - return enumerator.Current.Value.Value; - } - } -} diff --git a/neo/SmartContract/TriggerType.cs b/neo/SmartContract/TriggerType.cs deleted file mode 100644 index c39441f392..0000000000 --- a/neo/SmartContract/TriggerType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Neo.SmartContract -{ - public enum TriggerType : byte - { - Verification = 0x00, - Application = 0x10 - } -} diff --git a/neo/UInt160.cs b/neo/UInt160.cs deleted file mode 100644 index 47e0a810d0..0000000000 --- a/neo/UInt160.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; - -namespace Neo -{ - public class UInt160 : UIntBase, IComparable, IEquatable - { - public static readonly UInt160 Zero = new UInt160(); - - public UInt160() - : this(null) - { - } - - public UInt160(byte[] value) - : base(20, value) - { - } - - public int CompareTo(UInt160 other) - { - byte[] x = ToArray(); - byte[] y = other.ToArray(); - for (int i = x.Length - 1; i >= 0; i--) - { - if (x[i] > y[i]) - return 1; - if (x[i] < y[i]) - return -1; - } - return 0; - } - - bool IEquatable.Equals(UInt160 other) - { - return Equals(other); - } - - public static new UInt160 Parse(string value) - { - if (value == null) - throw new ArgumentNullException(); - if (value.StartsWith("0x")) - value = value.Substring(2); - if (value.Length != 40) - throw new FormatException(); - return new UInt160(value.HexToBytes().Reverse().ToArray()); - } - - public static bool TryParse(string s, out UInt160 result) - { - if (s == null) - { - result = null; - return false; - } - if (s.StartsWith("0x")) - s = s.Substring(2); - if (s.Length != 40) - { - result = null; - return false; - } - byte[] data = new byte[20]; - for (int i = 0; i < 20; i++) - if (!byte.TryParse(s.Substring(i * 2, 2), NumberStyles.AllowHexSpecifier, null, out data[i])) - { - result = null; - return false; - } - result = new UInt160(data.Reverse().ToArray()); - return true; - } - - public static bool operator >(UInt160 left, UInt160 right) - { - return left.CompareTo(right) > 0; - } - - public static bool operator >=(UInt160 left, UInt160 right) - { - return left.CompareTo(right) >= 0; - } - - public static bool operator <(UInt160 left, UInt160 right) - { - return left.CompareTo(right) < 0; - } - - public static bool operator <=(UInt160 left, UInt160 right) - { - return left.CompareTo(right) <= 0; - } - } -} diff --git a/neo/UInt256.cs b/neo/UInt256.cs deleted file mode 100644 index 21b92a6ff6..0000000000 --- a/neo/UInt256.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; - -namespace Neo -{ - public class UInt256 : UIntBase, IComparable, IEquatable - { - public static readonly UInt256 Zero = new UInt256(); - - public UInt256() - : this(null) - { - } - - public UInt256(byte[] value) - : base(32, value) - { - } - - public int CompareTo(UInt256 other) - { - byte[] x = ToArray(); - byte[] y = other.ToArray(); - for (int i = x.Length - 1; i >= 0; i--) - { - if (x[i] > y[i]) - return 1; - if (x[i] < y[i]) - return -1; - } - return 0; - } - - bool IEquatable.Equals(UInt256 other) - { - return Equals(other); - } - - public static new UInt256 Parse(string s) - { - if (s == null) - throw new ArgumentNullException(); - if (s.StartsWith("0x")) - s = s.Substring(2); - if (s.Length != 64) - throw new FormatException(); - return new UInt256(s.HexToBytes().Reverse().ToArray()); - } - - public static bool TryParse(string s, out UInt256 result) - { - if (s == null) - { - result = null; - return false; - } - if (s.StartsWith("0x")) - s = s.Substring(2); - if (s.Length != 64) - { - result = null; - return false; - } - byte[] data = new byte[32]; - for (int i = 0; i < 32; i++) - if (!byte.TryParse(s.Substring(i * 2, 2), NumberStyles.AllowHexSpecifier, null, out data[i])) - { - result = null; - return false; - } - result = new UInt256(data.Reverse().ToArray()); - return true; - } - - public static bool operator >(UInt256 left, UInt256 right) - { - return left.CompareTo(right) > 0; - } - - public static bool operator >=(UInt256 left, UInt256 right) - { - return left.CompareTo(right) >= 0; - } - - public static bool operator <(UInt256 left, UInt256 right) - { - return left.CompareTo(right) < 0; - } - - public static bool operator <=(UInt256 left, UInt256 right) - { - return left.CompareTo(right) <= 0; - } - } -} diff --git a/neo/UIntBase.cs b/neo/UIntBase.cs deleted file mode 100644 index 1d92a64c37..0000000000 --- a/neo/UIntBase.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Neo.IO; -using System; -using System.IO; -using System.Linq; - -namespace Neo -{ - public abstract class UIntBase : IEquatable, ISerializable - { - private byte[] data_bytes; - - public int Size => data_bytes.Length; - - protected UIntBase(int bytes, byte[] value) - { - if (value == null) - { - this.data_bytes = new byte[bytes]; - return; - } - if (value.Length != bytes) - throw new ArgumentException(); - this.data_bytes = value; - } - - void ISerializable.Deserialize(BinaryReader reader) - { - reader.Read(data_bytes, 0, data_bytes.Length); - } - - public bool Equals(UIntBase other) - { - if (ReferenceEquals(other, null)) - return false; - if (ReferenceEquals(this, other)) - return true; - if (data_bytes.Length != other.data_bytes.Length) - return false; - return data_bytes.SequenceEqual(other.data_bytes); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(obj, null)) - return false; - if (!(obj is UIntBase)) - return false; - return this.Equals((UIntBase)obj); - } - - public override int GetHashCode() - { - return data_bytes.ToInt32(0); - } - - public static UIntBase Parse(string s) - { - if (s.Length == 40 || s.Length == 42) - return UInt160.Parse(s); - else if (s.Length == 64 || s.Length == 66) - return UInt256.Parse(s); - else - throw new FormatException(); - } - - void ISerializable.Serialize(BinaryWriter writer) - { - writer.Write(data_bytes); - } - - public byte[] ToArray() - { - return data_bytes; - } - - /// - /// 转为16进制字符串 - /// - /// 返回16进制字符串 - public override string ToString() - { - return "0x" + data_bytes.Reverse().ToHexString(); - } - - public static bool TryParse(string s, out T result) where T : UIntBase - { - int size; - if (typeof(T) == typeof(UInt160)) - size = 20; - else if (typeof(T) == typeof(UInt256)) - size = 32; - else if (s.Length == 40 || s.Length == 42) - size = 20; - else if (s.Length == 64 || s.Length == 66) - size = 32; - else - size = 0; - if (size == 20) - { - if (UInt160.TryParse(s, out UInt160 r)) - { - result = (T)(UIntBase)r; - return true; - } - } - else if (size == 32) - { - if (UInt256.TryParse(s, out UInt256 r)) - { - result = (T)(UIntBase)r; - return true; - } - } - result = null; - return false; - } - - public static bool operator ==(UIntBase left, UIntBase right) - { - if (ReferenceEquals(left, right)) - return true; - if (ReferenceEquals(left, null) || ReferenceEquals(right, null)) - return false; - return left.Equals(right); - } - - public static bool operator !=(UIntBase left, UIntBase right) - { - return !(left == right); - } - } -} diff --git a/neo/VM/Helper.cs b/neo/VM/Helper.cs deleted file mode 100644 index f0bb2b4220..0000000000 --- a/neo/VM/Helper.cs +++ /dev/null @@ -1,211 +0,0 @@ -using Neo.Cryptography.ECC; -using Neo.IO; -using Neo.SmartContract; -using Neo.VM.Types; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using VMArray = Neo.VM.Types.Array; -using VMBoolean = Neo.VM.Types.Boolean; - -namespace Neo.VM -{ - public static class Helper - { - public static ScriptBuilder Emit(this ScriptBuilder sb, params OpCode[] ops) - { - foreach (OpCode op in ops) - sb.Emit(op); - return sb; - } - - public static ScriptBuilder EmitAppCall(this ScriptBuilder sb, UInt160 scriptHash, bool useTailCall = false) - { - return sb.EmitAppCall(scriptHash.ToArray(), useTailCall); - } - - public static ScriptBuilder EmitAppCall(this ScriptBuilder sb, UInt160 scriptHash, params ContractParameter[] parameters) - { - for (int i = parameters.Length - 1; i >= 0; i--) - sb.EmitPush(parameters[i]); - return sb.EmitAppCall(scriptHash); - } - - public static ScriptBuilder EmitAppCall(this ScriptBuilder sb, UInt160 scriptHash, string operation) - { - sb.EmitPush(false); - sb.EmitPush(operation); - sb.EmitAppCall(scriptHash); - return sb; - } - - public static ScriptBuilder EmitAppCall(this ScriptBuilder sb, UInt160 scriptHash, string operation, params ContractParameter[] args) - { - for (int i = args.Length - 1; i >= 0; i--) - sb.EmitPush(args[i]); - sb.EmitPush(args.Length); - sb.Emit(OpCode.PACK); - sb.EmitPush(operation); - sb.EmitAppCall(scriptHash); - return sb; - } - - public static ScriptBuilder EmitAppCall(this ScriptBuilder sb, UInt160 scriptHash, string operation, params object[] args) - { - for (int i = args.Length - 1; i >= 0; i--) - sb.EmitPush(args[i]); - sb.EmitPush(args.Length); - sb.Emit(OpCode.PACK); - sb.EmitPush(operation); - sb.EmitAppCall(scriptHash); - return sb; - } - - public static ScriptBuilder EmitPush(this ScriptBuilder sb, ISerializable data) - { - return sb.EmitPush(data.ToArray()); - } - - public static ScriptBuilder EmitPush(this ScriptBuilder sb, ContractParameter parameter) - { - switch (parameter.Type) - { - case ContractParameterType.Signature: - case ContractParameterType.ByteArray: - sb.EmitPush((byte[])parameter.Value); - break; - case ContractParameterType.Boolean: - sb.EmitPush((bool)parameter.Value); - break; - case ContractParameterType.Integer: - if (parameter.Value is BigInteger bi) - sb.EmitPush(bi); - else - sb.EmitPush((BigInteger)typeof(BigInteger).GetConstructor(new[] { parameter.Value.GetType() }).Invoke(new[] { parameter.Value })); - break; - case ContractParameterType.Hash160: - sb.EmitPush((UInt160)parameter.Value); - break; - case ContractParameterType.Hash256: - sb.EmitPush((UInt256)parameter.Value); - break; - case ContractParameterType.PublicKey: - sb.EmitPush((ECPoint)parameter.Value); - break; - case ContractParameterType.String: - sb.EmitPush((string)parameter.Value); - break; - case ContractParameterType.Array: - { - IList parameters = (IList)parameter.Value; - for (int i = parameters.Count - 1; i >= 0; i--) - sb.EmitPush(parameters[i]); - sb.EmitPush(parameters.Count); - sb.Emit(OpCode.PACK); - } - break; - default: - throw new ArgumentException(); - } - return sb; - } - - public static ScriptBuilder EmitPush(this ScriptBuilder sb, object obj) - { - switch (obj) - { - case bool data: - sb.EmitPush(data); - break; - case byte[] data: - sb.EmitPush(data); - break; - case string data: - sb.EmitPush(data); - break; - case BigInteger data: - sb.EmitPush(data); - break; - case ISerializable data: - sb.EmitPush(data); - break; - case sbyte data: - sb.EmitPush(data); - break; - case byte data: - sb.EmitPush(data); - break; - case short data: - sb.EmitPush(data); - break; - case ushort data: - sb.EmitPush(data); - break; - case int data: - sb.EmitPush(data); - break; - case uint data: - sb.EmitPush(data); - break; - case long data: - sb.EmitPush(data); - break; - case ulong data: - sb.EmitPush(data); - break; - case Enum data: - sb.EmitPush(BigInteger.Parse(data.ToString("d"))); - break; - default: - throw new ArgumentException(); - } - return sb; - } - - public static ScriptBuilder EmitSysCall(this ScriptBuilder sb, string api, params object[] args) - { - for (int i = args.Length - 1; i >= 0; i--) - EmitPush(sb, args[i]); - return sb.EmitSysCall(api); - } - - public static ContractParameter ToParameter(this StackItem item) - { - switch (item) - { - case VMArray array: - return new ContractParameter - { - Type = ContractParameterType.Array, - Value = array.Select(p => p.ToParameter()).ToArray() - }; - case VMBoolean _: - return new ContractParameter - { - Type = ContractParameterType.Boolean, - Value = item.GetBoolean() - }; - case ByteArray _: - return new ContractParameter - { - Type = ContractParameterType.ByteArray, - Value = item.GetByteArray() - }; - case Integer _: - return new ContractParameter - { - Type = ContractParameterType.Integer, - Value = item.GetBigInteger() - }; - case InteropInterface _: - return new ContractParameter - { - Type = ContractParameterType.InteropInterface - }; - default: - throw new ArgumentException(); - } - } - } -} diff --git a/neo/Wallets/AssetDescriptor.cs b/neo/Wallets/AssetDescriptor.cs deleted file mode 100644 index fcbfdc6db3..0000000000 --- a/neo/Wallets/AssetDescriptor.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Neo.Core; -using Neo.SmartContract; -using Neo.VM; -using System; - -namespace Neo.Wallets -{ - public class AssetDescriptor - { - public UIntBase AssetId; - public string AssetName; - public byte Decimals; - - public AssetDescriptor(UIntBase asset_id) - { - if (asset_id is UInt160 asset_id_160) - { - byte[] script; - using (ScriptBuilder sb = new ScriptBuilder()) - { - sb.EmitAppCall(asset_id_160, "decimals"); - sb.EmitAppCall(asset_id_160, "name"); - script = sb.ToArray(); - } - ApplicationEngine engine = ApplicationEngine.Run(script); - if (engine.State.HasFlag(VMState.FAULT)) throw new ArgumentException(); - this.AssetId = asset_id; - this.AssetName = engine.EvaluationStack.Pop().GetString(); - this.Decimals = (byte)engine.EvaluationStack.Pop().GetBigInteger(); - } - else - { - AssetState state = Blockchain.Default.GetAssetState((UInt256)asset_id); - this.AssetId = state.AssetId; - this.AssetName = state.GetName(); - this.Decimals = state.Precision; - } - } - - public override string ToString() - { - return AssetName; - } - } -} diff --git a/neo/Wallets/BalanceEventArgs.cs b/neo/Wallets/BalanceEventArgs.cs deleted file mode 100644 index e728363b1a..0000000000 --- a/neo/Wallets/BalanceEventArgs.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Neo.Core; -using System; - -namespace Neo.Wallets -{ - public class BalanceEventArgs : EventArgs - { - public Transaction Transaction; - public UInt160[] RelatedAccounts; - public uint? Height; - public uint Time; - } -} diff --git a/neo/Wallets/Coin.cs b/neo/Wallets/Coin.cs deleted file mode 100644 index a313468a51..0000000000 --- a/neo/Wallets/Coin.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Neo.Core; -using System; - -namespace Neo.Wallets -{ - public class Coin : IEquatable - { - public CoinReference Reference; - public TransactionOutput Output; - public CoinState State; - - private string _address = null; - public string Address - { - get - { - if (_address == null) - { - _address = Wallet.ToAddress(Output.ScriptHash); - } - return _address; - } - } - - public bool Equals(Coin other) - { - if (ReferenceEquals(this, other)) return true; - if (ReferenceEquals(null, other)) return false; - return Reference.Equals(other.Reference); - } - - public override bool Equals(object obj) - { - return Equals(obj as Coin); - } - - public override int GetHashCode() - { - return Reference.GetHashCode(); - } - } -} diff --git a/neo/Wallets/DataEntryPrefix.cs b/neo/Wallets/DataEntryPrefix.cs deleted file mode 100644 index f5a77c8a58..0000000000 --- a/neo/Wallets/DataEntryPrefix.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Neo.Wallets -{ - internal static class DataEntryPrefix - { - public const byte ST_Coin = 0x44; - public const byte ST_Transaction = 0x48; - - public const byte IX_Group = 0x80; - public const byte IX_Accounts = 0x81; - - public const byte SYS_Version = 0xf0; - } -} diff --git a/neo/Wallets/KeyPair.cs b/neo/Wallets/KeyPair.cs deleted file mode 100644 index 94b07116e1..0000000000 --- a/neo/Wallets/KeyPair.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Neo.Core; -using Neo.Cryptography; -using Neo.SmartContract; -using System; -using System.Linq; -using System.Security.Cryptography; -using System.Text; - -namespace Neo.Wallets -{ - public class KeyPair : IEquatable - { - public readonly byte[] PrivateKey; - public readonly Cryptography.ECC.ECPoint PublicKey; - - public UInt160 PublicKeyHash => PublicKey.EncodePoint(true).ToScriptHash(); - - public KeyPair(byte[] privateKey) - { - if (privateKey.Length != 32 && privateKey.Length != 96 && privateKey.Length != 104) - throw new ArgumentException(); - this.PrivateKey = new byte[32]; - Buffer.BlockCopy(privateKey, privateKey.Length - 32, PrivateKey, 0, 32); - if (privateKey.Length == 32) - { - this.PublicKey = Cryptography.ECC.ECCurve.Secp256r1.G * privateKey; - } - else - { - this.PublicKey = Cryptography.ECC.ECPoint.FromBytes(privateKey, Cryptography.ECC.ECCurve.Secp256r1); - } -#if NET47 - ProtectedMemory.Protect(PrivateKey, MemoryProtectionScope.SameProcess); -#endif - } - - public IDisposable Decrypt() - { -#if NET47 - return new ProtectedMemoryContext(PrivateKey, MemoryProtectionScope.SameProcess); -#else - return new System.IO.MemoryStream(0); -#endif - } - - public bool Equals(KeyPair other) - { - if (ReferenceEquals(this, other)) return true; - if (ReferenceEquals(null, other)) return false; - return PublicKey.Equals(other.PublicKey); - } - - public override bool Equals(object obj) - { - return Equals(obj as KeyPair); - } - - public string Export() - { - using (Decrypt()) - { - byte[] data = new byte[34]; - data[0] = 0x80; - Buffer.BlockCopy(PrivateKey, 0, data, 1, 32); - data[33] = 0x01; - string wif = data.Base58CheckEncode(); - Array.Clear(data, 0, data.Length); - return wif; - } - } - - public string Export(string passphrase, int N = 16384, int r = 8, int p = 8) - { - using (Decrypt()) - { - UInt160 script_hash = Contract.CreateSignatureRedeemScript(PublicKey).ToScriptHash(); - string address = Wallet.ToAddress(script_hash); - byte[] addresshash = Encoding.ASCII.GetBytes(address).Sha256().Sha256().Take(4).ToArray(); - byte[] derivedkey = SCrypt.DeriveKey(Encoding.UTF8.GetBytes(passphrase), addresshash, N, r, p, 64); - byte[] derivedhalf1 = derivedkey.Take(32).ToArray(); - byte[] derivedhalf2 = derivedkey.Skip(32).ToArray(); - byte[] encryptedkey = XOR(PrivateKey, derivedhalf1).AES256Encrypt(derivedhalf2); - byte[] buffer = new byte[39]; - buffer[0] = 0x01; - buffer[1] = 0x42; - buffer[2] = 0xe0; - Buffer.BlockCopy(addresshash, 0, buffer, 3, addresshash.Length); - Buffer.BlockCopy(encryptedkey, 0, buffer, 7, encryptedkey.Length); - return buffer.Base58CheckEncode(); - } - } - - public override int GetHashCode() - { - return PublicKey.GetHashCode(); - } - - public override string ToString() - { - return PublicKey.ToString(); - } - - private static byte[] XOR(byte[] x, byte[] y) - { - if (x.Length != y.Length) throw new ArgumentException(); - return x.Zip(y, (a, b) => (byte)(a ^ b)).ToArray(); - } - } -} diff --git a/neo/Wallets/TransferOutput.cs b/neo/Wallets/TransferOutput.cs deleted file mode 100644 index 04200ecc85..0000000000 --- a/neo/Wallets/TransferOutput.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Neo.Core; -using System; - -namespace Neo.Wallets -{ - public class TransferOutput - { - public UIntBase AssetId; - public BigDecimal Value; - public UInt160 ScriptHash; - - public bool IsGlobalAsset => AssetId.Size == 32; - - public TransactionOutput ToTxOutput() - { - if (AssetId is UInt256 asset_id) - return new TransactionOutput - { - AssetId = asset_id, - Value = Value.ToFixed8(), - ScriptHash = ScriptHash - }; - throw new NotSupportedException(); - } - } -} diff --git a/neo/Wallets/Wallet.cs b/neo/Wallets/Wallet.cs deleted file mode 100644 index e5ed1a1efa..0000000000 --- a/neo/Wallets/Wallet.cs +++ /dev/null @@ -1,418 +0,0 @@ -using Neo.Core; -using Neo.Cryptography; -using Neo.SmartContract; -using Neo.VM; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using ECPoint = Neo.Cryptography.ECC.ECPoint; -using VMArray = Neo.VM.Types.Array; - -namespace Neo.Wallets -{ - public abstract class Wallet - { - public abstract event EventHandler BalanceChanged; - - private static readonly Random rand = new Random(); - - public abstract string Name { get; } - public abstract Version Version { get; } - public abstract uint WalletHeight { get; } - - public abstract void ApplyTransaction(Transaction tx); - public abstract bool Contains(UInt160 scriptHash); - public abstract WalletAccount CreateAccount(byte[] privateKey); - public abstract WalletAccount CreateAccount(Contract contract, KeyPair key = null); - public abstract WalletAccount CreateAccount(UInt160 scriptHash); - public abstract bool DeleteAccount(UInt160 scriptHash); - public abstract WalletAccount GetAccount(UInt160 scriptHash); - public abstract IEnumerable GetAccounts(); - public abstract IEnumerable GetCoins(IEnumerable accounts); - public abstract IEnumerable GetTransactions(); - - public WalletAccount CreateAccount() - { - byte[] privateKey = new byte[32]; - using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) - { - rng.GetBytes(privateKey); - } - WalletAccount account = CreateAccount(privateKey); - Array.Clear(privateKey, 0, privateKey.Length); - return account; - } - - public WalletAccount CreateAccount(Contract contract, byte[] privateKey) - { - if (privateKey == null) return CreateAccount(contract); - return CreateAccount(contract, new KeyPair(privateKey)); - } - - public IEnumerable FindUnspentCoins(params UInt160[] from) - { - IEnumerable accounts = from.Length > 0 ? from : GetAccounts().Where(p => !p.Lock && !p.WatchOnly).Select(p => p.ScriptHash); - return GetCoins(accounts).Where(p => p.State.HasFlag(CoinState.Confirmed) && !p.State.HasFlag(CoinState.Spent) && !p.State.HasFlag(CoinState.Frozen)); - } - - public virtual Coin[] FindUnspentCoins(UInt256 asset_id, Fixed8 amount, params UInt160[] from) - { - return FindUnspentCoins(FindUnspentCoins(from), asset_id, amount); - } - - protected static Coin[] FindUnspentCoins(IEnumerable unspents, UInt256 asset_id, Fixed8 amount) - { - Coin[] unspents_asset = unspents.Where(p => p.Output.AssetId == asset_id).ToArray(); - Fixed8 sum = unspents_asset.Sum(p => p.Output.Value); - if (sum < amount) return null; - if (sum == amount) return unspents_asset; - Coin[] unspents_ordered = unspents_asset.OrderByDescending(p => p.Output.Value).ToArray(); - int i = 0; - while (unspents_ordered[i].Output.Value <= amount) - amount -= unspents_ordered[i++].Output.Value; - if (amount == Fixed8.Zero) - return unspents_ordered.Take(i).ToArray(); - else - return unspents_ordered.Take(i).Concat(new[] { unspents_ordered.Last(p => p.Output.Value >= amount) }).ToArray(); - } - - public WalletAccount GetAccount(ECPoint pubkey) - { - return GetAccount(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash()); - } - - public Fixed8 GetAvailable(UInt256 asset_id) - { - return FindUnspentCoins().Where(p => p.Output.AssetId.Equals(asset_id)).Sum(p => p.Output.Value); - } - - public BigDecimal GetAvailable(UIntBase asset_id) - { - if (asset_id is UInt160 asset_id_160) - { - byte[] script; - using (ScriptBuilder sb = new ScriptBuilder()) - { - foreach (UInt160 account in GetAccounts().Where(p => !p.WatchOnly).Select(p => p.ScriptHash)) - sb.EmitAppCall(asset_id_160, "balanceOf", account); - sb.Emit(OpCode.DEPTH, OpCode.PACK); - sb.EmitAppCall(asset_id_160, "decimals"); - script = sb.ToArray(); - } - ApplicationEngine engine = ApplicationEngine.Run(script); - byte decimals = (byte)engine.EvaluationStack.Pop().GetBigInteger(); - BigInteger amount = ((VMArray)engine.EvaluationStack.Pop()).Aggregate(BigInteger.Zero, (x, y) => x + y.GetBigInteger()); - return new BigDecimal(amount, decimals); - } - else - { - return new BigDecimal(GetAvailable((UInt256)asset_id).GetData(), 8); - } - } - - public Fixed8 GetBalance(UInt256 asset_id) - { - return GetCoins(GetAccounts().Select(p => p.ScriptHash)).Where(p => !p.State.HasFlag(CoinState.Spent) && p.Output.AssetId.Equals(asset_id)).Sum(p => p.Output.Value); - } - - public virtual UInt160 GetChangeAddress() - { - WalletAccount[] accounts = GetAccounts().ToArray(); - WalletAccount account = accounts.FirstOrDefault(p => p.IsDefault); - if (account == null) - account = accounts.FirstOrDefault(p => p.Contract?.IsStandard == true); - if (account == null) - account = accounts.FirstOrDefault(p => !p.WatchOnly); - if (account == null) - account = accounts.FirstOrDefault(); - return account?.ScriptHash; - } - - public IEnumerable GetCoins() - { - return GetCoins(GetAccounts().Select(p => p.ScriptHash)); - } - - public static byte[] GetPrivateKeyFromNEP2(string nep2, string passphrase, int N = 16384, int r = 8, int p = 8) - { - if (nep2 == null) throw new ArgumentNullException(nameof(nep2)); - if (passphrase == null) throw new ArgumentNullException(nameof(passphrase)); - byte[] data = nep2.Base58CheckDecode(); - if (data.Length != 39 || data[0] != 0x01 || data[1] != 0x42 || data[2] != 0xe0) - throw new FormatException(); - byte[] addresshash = new byte[4]; - Buffer.BlockCopy(data, 3, addresshash, 0, 4); - byte[] derivedkey = SCrypt.DeriveKey(Encoding.UTF8.GetBytes(passphrase), addresshash, N, r, p, 64); - byte[] derivedhalf1 = derivedkey.Take(32).ToArray(); - byte[] derivedhalf2 = derivedkey.Skip(32).ToArray(); - byte[] encryptedkey = new byte[32]; - Buffer.BlockCopy(data, 7, encryptedkey, 0, 32); - byte[] prikey = XOR(encryptedkey.AES256Decrypt(derivedhalf2), derivedhalf1); - Cryptography.ECC.ECPoint pubkey = Cryptography.ECC.ECCurve.Secp256r1.G * prikey; - UInt160 script_hash = Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash(); - string address = ToAddress(script_hash); - if (!Encoding.ASCII.GetBytes(address).Sha256().Sha256().Take(4).SequenceEqual(addresshash)) - throw new FormatException(); - return prikey; - } - - public static byte[] GetPrivateKeyFromWIF(string wif) - { - if (wif == null) throw new ArgumentNullException(); - byte[] data = wif.Base58CheckDecode(); - if (data.Length != 34 || data[0] != 0x80 || data[33] != 0x01) - throw new FormatException(); - byte[] privateKey = new byte[32]; - Buffer.BlockCopy(data, 1, privateKey, 0, privateKey.Length); - Array.Clear(data, 0, data.Length); - return privateKey; - } - - public IEnumerable GetUnclaimedCoins() - { - IEnumerable accounts = GetAccounts().Where(p => !p.Lock && !p.WatchOnly).Select(p => p.ScriptHash); - IEnumerable coins = GetCoins(accounts); - coins = coins.Where(p => p.Output.AssetId.Equals(Blockchain.GoverningToken.Hash)); - coins = coins.Where(p => p.State.HasFlag(CoinState.Confirmed) && p.State.HasFlag(CoinState.Spent)); - coins = coins.Where(p => !p.State.HasFlag(CoinState.Claimed) && !p.State.HasFlag(CoinState.Frozen)); - return coins; - } - - public virtual WalletAccount Import(X509Certificate2 cert) - { - byte[] privateKey; - using (ECDsa ecdsa = cert.GetECDsaPrivateKey()) - { - privateKey = ecdsa.ExportParameters(true).D; - } - WalletAccount account = CreateAccount(privateKey); - Array.Clear(privateKey, 0, privateKey.Length); - return account; - } - - public virtual WalletAccount Import(string wif) - { - byte[] privateKey = GetPrivateKeyFromWIF(wif); - WalletAccount account = CreateAccount(privateKey); - Array.Clear(privateKey, 0, privateKey.Length); - return account; - } - - public virtual WalletAccount Import(string nep2, string passphrase) - { - byte[] privateKey = GetPrivateKeyFromNEP2(nep2, passphrase); - WalletAccount account = CreateAccount(privateKey); - Array.Clear(privateKey, 0, privateKey.Length); - return account; - } - - public T MakeTransaction(T tx, UInt160 from = null, UInt160 change_address = null, Fixed8 fee = default(Fixed8)) where T : Transaction - { - if (tx.Outputs == null) tx.Outputs = new TransactionOutput[0]; - if (tx.Attributes == null) tx.Attributes = new TransactionAttribute[0]; - fee += tx.SystemFee; - var pay_total = (typeof(T) == typeof(IssueTransaction) ? new TransactionOutput[0] : tx.Outputs).GroupBy(p => p.AssetId, (k, g) => new - { - AssetId = k, - Value = g.Sum(p => p.Value) - }).ToDictionary(p => p.AssetId); - if (fee > Fixed8.Zero) - { - if (pay_total.ContainsKey(Blockchain.UtilityToken.Hash)) - { - pay_total[Blockchain.UtilityToken.Hash] = new - { - AssetId = Blockchain.UtilityToken.Hash, - Value = pay_total[Blockchain.UtilityToken.Hash].Value + fee - }; - } - else - { - pay_total.Add(Blockchain.UtilityToken.Hash, new - { - AssetId = Blockchain.UtilityToken.Hash, - Value = fee - }); - } - } - var pay_coins = pay_total.Select(p => new - { - AssetId = p.Key, - Unspents = from == null ? FindUnspentCoins(p.Key, p.Value.Value) : FindUnspentCoins(p.Key, p.Value.Value, from) - }).ToDictionary(p => p.AssetId); - if (pay_coins.Any(p => p.Value.Unspents == null)) return null; - var input_sum = pay_coins.Values.ToDictionary(p => p.AssetId, p => new - { - p.AssetId, - Value = p.Unspents.Sum(q => q.Output.Value) - }); - if (change_address == null) change_address = GetChangeAddress(); - List outputs_new = new List(tx.Outputs); - foreach (UInt256 asset_id in input_sum.Keys) - { - if (input_sum[asset_id].Value > pay_total[asset_id].Value) - { - outputs_new.Add(new TransactionOutput - { - AssetId = asset_id, - Value = input_sum[asset_id].Value - pay_total[asset_id].Value, - ScriptHash = change_address - }); - } - } - tx.Inputs = pay_coins.Values.SelectMany(p => p.Unspents).Select(p => p.Reference).ToArray(); - tx.Outputs = outputs_new.ToArray(); - return tx; - } - - public Transaction MakeTransaction(List attributes, IEnumerable outputs, UInt160 from = null, UInt160 change_address = null, Fixed8 fee = default(Fixed8)) - { - var cOutputs = outputs.Where(p => !p.IsGlobalAsset).GroupBy(p => new - { - AssetId = (UInt160)p.AssetId, - Account = p.ScriptHash - }, (k, g) => new - { - k.AssetId, - Value = g.Aggregate(BigInteger.Zero, (x, y) => x + y.Value.Value), - k.Account - }).ToArray(); - Transaction tx; - if (attributes == null) attributes = new List(); - if (cOutputs.Length == 0) - { - tx = new ContractTransaction(); - } - else - { - UInt160[] accounts = from == null ? GetAccounts().Where(p => !p.Lock && !p.WatchOnly).Select(p => p.ScriptHash).ToArray() : new[] { from }; - HashSet sAttributes = new HashSet(); - using (ScriptBuilder sb = new ScriptBuilder()) - { - foreach (var output in cOutputs) - { - byte[] script; - using (ScriptBuilder sb2 = new ScriptBuilder()) - { - foreach (UInt160 account in accounts) - sb2.EmitAppCall(output.AssetId, "balanceOf", account); - sb2.Emit(OpCode.DEPTH, OpCode.PACK); - script = sb2.ToArray(); - } - ApplicationEngine engine = ApplicationEngine.Run(script); - if (engine.State.HasFlag(VMState.FAULT)) return null; - var balances = ((IEnumerable)(VMArray)engine.EvaluationStack.Pop()).Reverse().Zip(accounts, (i, a) => new - { - Account = a, - Value = i.GetBigInteger() - }).ToArray(); - BigInteger sum = balances.Aggregate(BigInteger.Zero, (x, y) => x + y.Value); - if (sum < output.Value) return null; - if (sum != output.Value) - { - balances = balances.OrderByDescending(p => p.Value).ToArray(); - BigInteger amount = output.Value; - int i = 0; - while (balances[i].Value <= amount) - amount -= balances[i++].Value; - if (amount == BigInteger.Zero) - balances = balances.Take(i).ToArray(); - else - balances = balances.Take(i).Concat(new[] { balances.Last(p => p.Value >= amount) }).ToArray(); - sum = balances.Aggregate(BigInteger.Zero, (x, y) => x + y.Value); - } - sAttributes.UnionWith(balances.Select(p => p.Account)); - for (int i = 0; i < balances.Length; i++) - { - BigInteger value = balances[i].Value; - if (i == 0) - { - BigInteger change = sum - output.Value; - if (change > 0) value -= change; - } - sb.EmitAppCall(output.AssetId, "transfer", balances[i].Account, output.Account, value); - sb.Emit(OpCode.THROWIFNOT); - } - } - byte[] nonce = new byte[8]; - rand.NextBytes(nonce); - sb.Emit(OpCode.RET, nonce); - tx = new InvocationTransaction - { - Version = 1, - Script = sb.ToArray() - }; - } - attributes.AddRange(sAttributes.Select(p => new TransactionAttribute - { - Usage = TransactionAttributeUsage.Script, - Data = p.ToArray() - })); - } - tx.Attributes = attributes.ToArray(); - tx.Inputs = new CoinReference[0]; - tx.Outputs = outputs.Where(p => p.IsGlobalAsset).Select(p => p.ToTxOutput()).ToArray(); - tx.Scripts = new Witness[0]; - if (tx is InvocationTransaction itx) - { - ApplicationEngine engine = ApplicationEngine.Run(itx.Script, itx); - if (engine.State.HasFlag(VMState.FAULT)) return null; - tx = new InvocationTransaction - { - Version = itx.Version, - Script = itx.Script, - Gas = InvocationTransaction.GetGas(engine.GasConsumed), - Attributes = itx.Attributes, - Inputs = itx.Inputs, - Outputs = itx.Outputs - }; - } - tx = MakeTransaction(tx, from, change_address, fee); - return tx; - } - - public bool Sign(ContractParametersContext context) - { - bool fSuccess = false; - foreach (UInt160 scriptHash in context.ScriptHashes) - { - WalletAccount account = GetAccount(scriptHash); - if (account?.HasKey != true) continue; - KeyPair key = account.GetKey(); - byte[] signature = context.Verifiable.Sign(key); - fSuccess |= context.AddSignature(account.Contract, key.PublicKey, signature); - } - return fSuccess; - } - - public static string ToAddress(UInt160 scriptHash) - { - byte[] data = new byte[21]; - data[0] = Settings.Default.AddressVersion; - Buffer.BlockCopy(scriptHash.ToArray(), 0, data, 1, 20); - return data.Base58CheckEncode(); - } - - public static UInt160 ToScriptHash(string address) - { - byte[] data = address.Base58CheckDecode(); - if (data.Length != 21) - throw new FormatException(); - if (data[0] != Settings.Default.AddressVersion) - throw new FormatException(); - return new UInt160(data.Skip(1).ToArray()); - } - - public abstract bool VerifyPassword(string password); - - private static byte[] XOR(byte[] x, byte[] y) - { - if (x.Length != y.Length) throw new ArgumentException(); - return x.Zip(y, (a, b) => (byte)(a ^ b)).ToArray(); - } - } -} diff --git a/neo/Wallets/WalletAccount.cs b/neo/Wallets/WalletAccount.cs deleted file mode 100644 index b81bde740d..0000000000 --- a/neo/Wallets/WalletAccount.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Neo.SmartContract; - -namespace Neo.Wallets -{ - public abstract class WalletAccount - { - public readonly UInt160 ScriptHash; - public string Label; - public bool IsDefault; - public bool Lock; - public Contract Contract; - - public string Address => Wallet.ToAddress(ScriptHash); - public abstract bool HasKey { get; } - public bool WatchOnly => Contract == null; - - public abstract KeyPair GetKey(); - - protected WalletAccount(UInt160 scriptHash) - { - this.ScriptHash = scriptHash; - } - } -} diff --git a/neo/Wallets/WalletIndexer.cs b/neo/Wallets/WalletIndexer.cs deleted file mode 100644 index 0c6396093e..0000000000 --- a/neo/Wallets/WalletIndexer.cs +++ /dev/null @@ -1,362 +0,0 @@ -using Neo.Core; -using Neo.IO; -using Neo.IO.Data.LevelDB; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Security.Cryptography; -using System.Threading; - -namespace Neo.Wallets -{ - public static class WalletIndexer - { - public static event EventHandler BalanceChanged; - - private static readonly Dictionary> indexes = new Dictionary>(); - private static readonly Dictionary> accounts_tracked = new Dictionary>(); - private static readonly Dictionary coins_tracked = new Dictionary(); - - private static readonly DB db; - private static readonly object SyncRoot = new object(); - - public static uint IndexHeight - { - get - { - lock (SyncRoot) - { - if (indexes.Count == 0) return 0; - return indexes.Keys.Min(); - } - } - } - - static WalletIndexer() - { - string path = $"Index_{Settings.Default.Magic:X8}"; - Directory.CreateDirectory(path); - db = DB.Open(path, new Options { CreateIfMissing = true }); - if (db.TryGet(ReadOptions.Default, SliceBuilder.Begin(DataEntryPrefix.SYS_Version), out Slice value) && Version.TryParse(value.ToString(), out Version version) && version >= Version.Parse("2.5.4")) - { - ReadOptions options = new ReadOptions { FillCache = false }; - foreach (var group in db.Find(options, SliceBuilder.Begin(DataEntryPrefix.IX_Group), (k, v) => new - { - Height = k.ToUInt32(1), - Id = v.ToArray() - })) - { - UInt160[] accounts = db.Get(options, SliceBuilder.Begin(DataEntryPrefix.IX_Accounts).Add(group.Id)).ToArray().AsSerializableArray(); - indexes.Add(group.Height, new HashSet(accounts)); - foreach (UInt160 account in accounts) - accounts_tracked.Add(account, new HashSet()); - } - foreach (Coin coin in db.Find(options, SliceBuilder.Begin(DataEntryPrefix.ST_Coin), (k, v) => new Coin - { - Reference = k.ToArray().Skip(1).ToArray().AsSerializable(), - Output = v.ToArray().AsSerializable(), - State = (CoinState)v.ToArray()[60] - })) - { - accounts_tracked[coin.Output.ScriptHash].Add(coin.Reference); - coins_tracked.Add(coin.Reference, coin); - } - } - else - { - WriteBatch batch = new WriteBatch(); - ReadOptions options = new ReadOptions { FillCache = false }; - using (Iterator it = db.NewIterator(options)) - { - for (it.SeekToFirst(); it.Valid(); it.Next()) - { - batch.Delete(it.Key()); - } - } - batch.Put(SliceBuilder.Begin(DataEntryPrefix.SYS_Version), Assembly.GetExecutingAssembly().GetName().Version.ToString()); - db.Write(WriteOptions.Default, batch); - } - Thread thread = new Thread(ProcessBlocks) - { - IsBackground = true, - Name = $"{nameof(WalletIndexer)}.{nameof(ProcessBlocks)}" - }; - thread.Start(); - } - - public static IEnumerable GetCoins(IEnumerable accounts) - { - lock (SyncRoot) - { - foreach (UInt160 account in accounts) - foreach (CoinReference reference in accounts_tracked[account]) - yield return coins_tracked[reference]; - } - } - - private static byte[] GetGroupId() - { - byte[] groupId = new byte[32]; - using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) - { - rng.GetBytes(groupId); - } - return groupId; - } - - public static IEnumerable GetTransactions(IEnumerable accounts) - { - ReadOptions options = new ReadOptions { FillCache = false }; - using (options.Snapshot = db.GetSnapshot()) - { - IEnumerable results = Enumerable.Empty(); - foreach (UInt160 account in accounts) - results = results.Union(db.Find(options, SliceBuilder.Begin(DataEntryPrefix.ST_Transaction).Add(account), (k, v) => new UInt256(k.ToArray().Skip(21).ToArray()))); - foreach (UInt256 hash in results) - yield return hash; - } - } - - private static void ProcessBlock(Block block, HashSet accounts, WriteBatch batch) - { - foreach (Transaction tx in block.Transactions) - { - HashSet accounts_changed = new HashSet(); - for (ushort index = 0; index < tx.Outputs.Length; index++) - { - TransactionOutput output = tx.Outputs[index]; - if (accounts_tracked.ContainsKey(output.ScriptHash)) - { - CoinReference reference = new CoinReference - { - PrevHash = tx.Hash, - PrevIndex = index - }; - if (coins_tracked.TryGetValue(reference, out Coin coin)) - { - coin.State |= CoinState.Confirmed; - } - else - { - accounts_tracked[output.ScriptHash].Add(reference); - coins_tracked.Add(reference, coin = new Coin - { - Reference = reference, - Output = output, - State = CoinState.Confirmed - }); - } - batch.Put(SliceBuilder.Begin(DataEntryPrefix.ST_Coin).Add(reference), SliceBuilder.Begin().Add(output).Add((byte)coin.State)); - accounts_changed.Add(output.ScriptHash); - } - } - foreach (CoinReference input in tx.Inputs) - { - if (coins_tracked.TryGetValue(input, out Coin coin)) - { - if (coin.Output.AssetId.Equals(Blockchain.GoverningToken.Hash)) - { - coin.State |= CoinState.Spent | CoinState.Confirmed; - batch.Put(SliceBuilder.Begin(DataEntryPrefix.ST_Coin).Add(input), SliceBuilder.Begin().Add(coin.Output).Add((byte)coin.State)); - } - else - { - accounts_tracked[coin.Output.ScriptHash].Remove(input); - coins_tracked.Remove(input); - batch.Delete(DataEntryPrefix.ST_Coin, input); - } - accounts_changed.Add(coin.Output.ScriptHash); - } - } - if (tx is ClaimTransaction ctx) - { - foreach (CoinReference claim in ctx.Claims) - { - if (coins_tracked.TryGetValue(claim, out Coin coin)) - { - accounts_tracked[coin.Output.ScriptHash].Remove(claim); - coins_tracked.Remove(claim); - batch.Delete(DataEntryPrefix.ST_Coin, claim); - accounts_changed.Add(coin.Output.ScriptHash); - } - } - } - if (accounts_changed.Count > 0) - { - foreach (UInt160 account in accounts_changed) - batch.Put(SliceBuilder.Begin(DataEntryPrefix.ST_Transaction).Add(account).Add(tx.Hash), false); - BalanceChanged?.Invoke(null, new BalanceEventArgs - { - Transaction = tx, - RelatedAccounts = accounts_changed.ToArray(), - Height = block.Index, - Time = block.Timestamp - }); - } - } - } - - private static void ProcessBlocks() - { - bool need_sleep = false; - for (; ; ) - { - if (need_sleep) - { - Thread.Sleep(2000); - need_sleep = false; - } - try - { - lock (SyncRoot) - { - if (indexes.Count == 0) - { - need_sleep = true; - continue; - } - uint height = indexes.Keys.Min(); - Block block = Blockchain.Default?.GetBlock(height); - if (block == null) - { - need_sleep = true; - continue; - } - WriteBatch batch = new WriteBatch(); - HashSet accounts = indexes[height]; - ProcessBlock(block, accounts, batch); - ReadOptions options = ReadOptions.Default; - byte[] groupId = db.Get(options, SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height)).ToArray(); - indexes.Remove(height); - batch.Delete(SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height)); - height++; - if (indexes.TryGetValue(height, out HashSet accounts_next)) - { - accounts_next.UnionWith(accounts); - groupId = db.Get(options, SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height)).ToArray(); - batch.Put(SliceBuilder.Begin(DataEntryPrefix.IX_Accounts).Add(groupId), accounts_next.ToArray().ToByteArray()); - } - else - { - indexes.Add(height, accounts); - batch.Put(SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height), groupId); - } - db.Write(WriteOptions.Default, batch); - } - } - catch when (Blockchain.Default == null || Blockchain.Default.IsDisposed || db.IsDisposed) - { - return; - } - } - } - - public static void RebuildIndex() - { - lock (SyncRoot) - { - WriteBatch batch = new WriteBatch(); - ReadOptions options = new ReadOptions { FillCache = false }; - foreach (uint height in indexes.Keys) - { - byte[] groupId = db.Get(options, SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height)).ToArray(); - batch.Delete(SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height)); - batch.Delete(SliceBuilder.Begin(DataEntryPrefix.IX_Accounts).Add(groupId)); - } - indexes.Clear(); - if (accounts_tracked.Count > 0) - { - indexes[0] = new HashSet(accounts_tracked.Keys); - byte[] groupId = GetGroupId(); - batch.Put(SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(0u), groupId); - batch.Put(SliceBuilder.Begin(DataEntryPrefix.IX_Accounts).Add(groupId), accounts_tracked.Keys.ToArray().ToByteArray()); - foreach (HashSet coins in accounts_tracked.Values) - coins.Clear(); - } - foreach (CoinReference reference in coins_tracked.Keys) - batch.Delete(DataEntryPrefix.ST_Coin, reference); - coins_tracked.Clear(); - foreach (Slice key in db.Find(options, SliceBuilder.Begin(DataEntryPrefix.ST_Transaction), (k, v) => k)) - batch.Delete(key); - db.Write(WriteOptions.Default, batch); - } - } - - public static void RegisterAccounts(IEnumerable accounts, uint height = 0) - { - lock (SyncRoot) - { - bool index_exists = indexes.TryGetValue(height, out HashSet index); - if (!index_exists) index = new HashSet(); - foreach (UInt160 account in accounts) - if (!accounts_tracked.ContainsKey(account)) - { - index.Add(account); - accounts_tracked.Add(account, new HashSet()); - } - if (index.Count > 0) - { - WriteBatch batch = new WriteBatch(); - byte[] groupId; - if (!index_exists) - { - indexes.Add(height, index); - groupId = GetGroupId(); - batch.Put(SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height), groupId); - } - else - { - groupId = db.Get(ReadOptions.Default, SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height)).ToArray(); - } - batch.Put(SliceBuilder.Begin(DataEntryPrefix.IX_Accounts).Add(groupId), index.ToArray().ToByteArray()); - db.Write(WriteOptions.Default, batch); - } - } - } - - public static void UnregisterAccounts(IEnumerable accounts) - { - lock (SyncRoot) - { - WriteBatch batch = new WriteBatch(); - ReadOptions options = new ReadOptions { FillCache = false }; - foreach (UInt160 account in accounts) - { - if (accounts_tracked.TryGetValue(account, out HashSet references)) - { - foreach (uint height in indexes.Keys.ToArray()) - { - HashSet index = indexes[height]; - if (index.Remove(account)) - { - byte[] groupId = db.Get(options, SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height)).ToArray(); - if (index.Count == 0) - { - indexes.Remove(height); - batch.Delete(SliceBuilder.Begin(DataEntryPrefix.IX_Group).Add(height)); - batch.Delete(SliceBuilder.Begin(DataEntryPrefix.IX_Accounts).Add(groupId)); - } - else - { - batch.Put(SliceBuilder.Begin(DataEntryPrefix.IX_Accounts).Add(groupId), index.ToArray().ToByteArray()); - } - break; - } - } - accounts_tracked.Remove(account); - foreach (CoinReference reference in references) - { - batch.Delete(DataEntryPrefix.ST_Coin, reference); - coins_tracked.Remove(reference); - } - foreach (Slice key in db.Find(options, SliceBuilder.Begin(DataEntryPrefix.ST_Transaction).Add(account), (k, v) => k)) - batch.Delete(key); - } - } - db.Write(WriteOptions.Default, batch); - } - } - } -} diff --git a/neo/neo.csproj b/neo/neo.csproj deleted file mode 100644 index 004d9db2f2..0000000000 --- a/neo/neo.csproj +++ /dev/null @@ -1,53 +0,0 @@ - - - - 2015-2017 The Neo Project - Neo - 2.7.2 - The Neo Project - netstandard2.0;net47 - true - Neo - Neo - NEO;AntShares;Blockchain;Smart Contract - https://github.com/neo-project/neo - git - https://github.com/neo-project/neo.git - Neo - The Neo Project - Neo - - - - none - False - - - - - PreserveNewest - content - true - - - - - - - - - - - - - - - - - - - 1.1.6.13 - - - - diff --git a/neo/policy.json b/neo/policy.json deleted file mode 100644 index 1f20084c31..0000000000 --- a/neo/policy.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "PolicyConfiguration": { - "PolicyLevel": "AllowAll", - "List": [] - } -} diff --git a/neo/protocol.json b/neo/protocol.json deleted file mode 100644 index 195d69d302..0000000000 --- a/neo/protocol.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "ProtocolConfiguration": { - "Magic": 7630401, - "AddressVersion": 23, - "MaxTransactionsPerBlock": 500, - "StandbyValidators": [ - "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", - "02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", - "03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", - "02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", - "024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", - "02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", - "02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70" - ], - "SeedList": [ - "seed1.neo.org:10333", - "seed2.neo.org:10333", - "seed3.neo.org:10333", - "seed4.neo.org:10333", - "seed5.neo.org:10333" - ], - "SystemFee": { - "EnrollmentTransaction": 1000, - "IssueTransaction": 500, - "PublishTransaction": 500, - "RegisterTransaction": 10000 - } - } -} diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000000..fd68f2ca48 --- /dev/null +++ b/nuget.config @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/pkgs/.gitkeep b/pkgs/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000000..d5d2f20440 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + true + + + + 2015-2025 The Neo Project + The Neo Project + true + $(MSBuildProjectName) + neo.png + https://github.com/neo-project/neo + MIT + README.md + git + https://github.com/neo-project/neo.git + The Neo Project + true + 4.0.0 + net10.0 + $(PackageId) + enable + enable + + + + + + + + diff --git a/src/Neo.Extensions/AssemblyExtensions.cs b/src/Neo.Extensions/AssemblyExtensions.cs new file mode 100644 index 0000000000..c03383b439 --- /dev/null +++ b/src/Neo.Extensions/AssemblyExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// AssemblyExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Reflection; + +namespace Neo; + +public static class AssemblyExtensions +{ + public static string GetVersion(this Assembly assembly) + { + return assembly.GetName().Version!.ToString(3); + } +} diff --git a/src/Neo.Extensions/BigIntegerExtensions.cs b/src/Neo.Extensions/BigIntegerExtensions.cs new file mode 100644 index 0000000000..51e3e9c479 --- /dev/null +++ b/src/Neo.Extensions/BigIntegerExtensions.cs @@ -0,0 +1,165 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BigIntegerExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; +using System.Runtime.CompilerServices; + +namespace Neo; + +public static class BigIntegerExtensions +{ + /// + /// Performs integer division with ceiling (rounding up). + /// Example: 10 / 3 = 4 instead of 3. + /// + /// The dividend. + /// The divisor (must be nonzero). + /// The result of division rounded up. + /// Thrown when divisor is zero. + public static BigInteger DivideCeiling(this BigInteger dividend, BigInteger divisor) + { + // If it's 0, it will automatically throw DivideByZeroException + var v = divisor > 0 ? + BigInteger.DivRem(dividend, divisor, out var r) : + BigInteger.DivRem(-dividend, -divisor, out r); + + if (r > 0) + return v + BigInteger.One; + + return v; + } + + /// + /// Finds the lowest set bit in the specified value. If value is zero, returns -1. + /// + /// The value to find the lowest set bit in. The value.GetBitLength cannot greater than 2Gib. + /// The lowest set bit in the specified value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetLowestSetBit(this BigInteger value) + { + if (value.Sign == 0) return -1; // special case for zero. TrailingZeroCount returns 32 in standard library. + + return (int)BigInteger.TrailingZeroCount(value); + } + + /// + /// Computes the remainder of the division of the specified value by the modulus. + /// It's different from the `%` operator(see `BigInteger.Remainder`) if the dividend is negative. + /// It always returns a non-negative value even if the dividend is negative. + /// + /// The value to compute the remainder of(i.e. dividend). + /// The modulus(i.e. divisor). + /// The remainder of the division of the specified value by the modulus. + /// Thrown when the divisor is zero. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BigInteger Mod(this BigInteger x, BigInteger y) + { + x %= y; + if (x.Sign < 0) + x += y; + return x; + } + + /// + /// Computes the modular inverse of the specified value. + /// + /// The value to find the modular inverse of. + /// The modulus. + /// The modular inverse of the specified value. + /// Thrown when the value or modulus is out of range. + /// + /// Thrown when no modular inverse exists for the given inputs. i.e. when the value and modulus are not coprime. + /// + public static BigInteger ModInverse(this BigInteger value, BigInteger modulus) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + ArgumentOutOfRangeException.ThrowIfLessThan(modulus, 2); + + BigInteger r = value, oldR = modulus, s = 1, oldS = 0; + while (r > 0) + { + var q = oldR / r; + (oldR, r) = (r, oldR % r); + (oldS, s) = (s, oldS - q * s); + } + var result = oldS % modulus; + if (result < 0) result += modulus; + + if (!(value * result % modulus).IsOne) + throw new ArithmeticException("No modular inverse exists for the given inputs."); + return result; + } + + /// + /// Tests whether the specified bit is set in the specified value. + /// If the value is negative and index exceeds the bit length, it returns true. + /// If the value is positive and index exceeds the bit length, it returns false. + /// If index is negative, it returns false always. + /// NOTE: the `value` is represented in sign-magnitude format, + /// so it's different from the bit value in two's complement format(int, long). + /// + /// The value to test. + /// The index of the bit to test. + /// True if the specified bit is set in the specified value, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TestBit(this BigInteger value, int index) + { + return !(value & (BigInteger.One << index)).IsZero; + } + + /// + /// Finds the sum of the specified integers. + /// + /// The specified integers. + /// The sum of the integers. + public static BigInteger Sum(this IEnumerable source) + { + var sum = BigInteger.Zero; + foreach (var bi in source) sum += bi; + return sum; + } + + /// + /// Converts a to byte array in little-endian and eliminates all the leading zeros. + /// If the value is zero, it returns an empty byte array. + /// + /// The to convert. + /// The converted byte array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] ToByteArrayStandard(this BigInteger value) + { + if (value.IsZero) return []; + return value.ToByteArray(); + } + + /// + /// Computes the square root of the specified value. + /// + /// The value to compute the square root of. + /// The square root of the specified value. + /// Thrown when the value is negative. + public static BigInteger Sqrt(this BigInteger value) + { + if (value < 0) throw new InvalidOperationException($"value {value} can not be negative for '{nameof(Sqrt)}'."); + if (value.IsZero) return BigInteger.Zero; + if (value < 4) return BigInteger.One; + + var z = value; + var x = BigInteger.One << (int)(((value - 1).GetBitLength() + 1) >> 1); + while (x < z) + { + z = x; + x = (value / x + x) / 2; + } + + return z; + } +} diff --git a/src/Neo.Extensions/ByteArrayComparer.cs b/src/Neo.Extensions/ByteArrayComparer.cs new file mode 100644 index 0000000000..f5ec4c087f --- /dev/null +++ b/src/Neo.Extensions/ByteArrayComparer.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ByteArrayComparer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Runtime.CompilerServices; + +namespace Neo; + +/// +/// Defines methods to support the comparison of two []. +/// +public class ByteArrayComparer : IComparer +{ + public static readonly ByteArrayComparer Default = new(1); + public static readonly ByteArrayComparer Reverse = new(-1); + + private readonly int _direction; + + private ByteArrayComparer(int direction) + { + _direction = direction; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Compare(byte[]? x, byte[]? y) + { + if (ReferenceEquals(x, y)) return 0; + + if (x is null) // y must not be null + return -y!.Length * _direction; + + if (y is null) // x must not be null + return x.Length * _direction; + + // Note: if "SequenceCompareTo" is "int.MinValue * -1", it + // will overflow "int.MaxValue". Seeing how "int.MinValue * -1" + // value would be "int.MaxValue + 1" + return unchecked(x.AsSpan().SequenceCompareTo(y.AsSpan()) * _direction); + } +} diff --git a/src/Neo.Extensions/ByteArrayEqualityComparer.cs b/src/Neo.Extensions/ByteArrayEqualityComparer.cs new file mode 100644 index 0000000000..9770c18865 --- /dev/null +++ b/src/Neo.Extensions/ByteArrayEqualityComparer.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2015-2025 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. + +namespace Neo; + +public class ByteArrayEqualityComparer : IEqualityComparer +{ + public static readonly ByteArrayEqualityComparer Default = new(); + + public bool Equals(byte[]? x, byte[]? y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null || x.Length != y.Length) return false; + + return x.AsSpan().SequenceEqual(y.AsSpan()); + } + + public int GetHashCode(byte[] obj) + { + return obj.XxHash3_32(); + } +} diff --git a/src/Neo.Extensions/ByteExtensions.cs b/src/Neo.Extensions/ByteExtensions.cs new file mode 100644 index 0000000000..d191a7fec9 --- /dev/null +++ b/src/Neo.Extensions/ByteExtensions.cs @@ -0,0 +1,150 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ByteExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Hashing; +using System.Runtime.CompilerServices; + +namespace Neo; + +public static class ByteExtensions +{ + private const int DefaultXxHash3Seed = 40343; + private const string s_hexChars = "0123456789abcdef"; + + /// + /// Computes the 32-bit hash value for the specified byte array using the xxhash3 algorithm. + /// + /// The input to compute the hash code for. + /// The seed used by the xxhash3 algorithm. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int XxHash3_32(this ReadOnlySpan value, long seed = DefaultXxHash3Seed) + { + return HashCode.Combine(XxHash3.HashToUInt64(value, seed)); + } + + /// + /// Computes the 32-bit hash value for the specified byte array using the xxhash3 algorithm. + /// + /// The input to compute the hash code for. + /// The seed used by the xxhash3 algorithm. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int XxHash3_32(this byte[] value, long seed = DefaultXxHash3Seed) + { + return XxHash3_32(value.AsSpan(), seed); + } + + /// + /// Converts a byte array to hex . + /// + /// The byte array to convert. + /// The converted hex . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToHexString(this byte[]? value) + { + ArgumentNullException.ThrowIfNull(value); + + return Convert.ToHexStringLower(value); + } + + /// + /// Converts a byte array to hex . + /// + /// The byte array to convert. + /// The converted hex . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToHexString(this ReadOnlyMemory value) + { + return Convert.ToHexStringLower(value.Span); + } + + /// + /// Converts a byte array to hex . + /// + /// The byte array to convert. + /// The converted hex . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToHexString(this Memory value) + { + return Convert.ToHexStringLower(value.Span); + } + + /// + /// Converts a byte array to hex . + /// + /// The byte array to convert. + /// Indicates whether it should be converted in the reversed byte order. + /// The converted hex . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToHexString(this byte[]? value, bool reverse = false) + { + ArgumentNullException.ThrowIfNull(value); + + return ToHexString(value.AsSpan(), reverse); + } + + /// + /// Converts a byte span to hex . + /// + /// The byte array to convert. + /// Indicates whether it should be converted in the reversed byte order. + /// The converted hex . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToHexString(this ReadOnlySpan value, bool reverse = false) + { + if (!reverse) return ToHexString(value); + + return string.Create(value.Length * 2, value, (span, bytes) => + { + for (var i = 0; i < bytes.Length; i++) + { + var b = bytes[bytes.Length - i - 1]; + span[i * 2] = s_hexChars[b >> 4]; + span[i * 2 + 1] = s_hexChars[b & 0xF]; + } + }); + } + + /// + /// Converts a byte array to hex . + /// + /// The byte array to convert. + /// The converted hex . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToHexString(this ReadOnlySpan value) + { + return Convert.ToHexStringLower(value); + } + + /// + /// Converts a byte array to a read-only span. + /// + /// The byte array to convert. + /// The converted read-only span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan AsReadOnlySpan(this byte[] value) => value; + + /// + /// All bytes are zero or not in a byte array + /// + /// The byte array + /// false if all bytes are zero, true otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool NotZero(this ReadOnlySpan x) + { + return x.IndexOfAnyExcept((byte)0) >= 0; + } +} diff --git a/src/Neo.Extensions/Collections/CollectionExtensions.cs b/src/Neo.Extensions/Collections/CollectionExtensions.cs new file mode 100644 index 0000000000..8b539b94c8 --- /dev/null +++ b/src/Neo.Extensions/Collections/CollectionExtensions.cs @@ -0,0 +1,117 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CollectionExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Collections; + +public static class CollectionExtensions +{ + /// + /// Removes the key-value pairs from the dictionary that match the specified predicate. + /// + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// The dictionary to remove key-value pairs from. + /// The predicate to match key-value pairs. + /// An action to perform after each key-value pair is removed. + public static void RemoveWhere( + this IDictionary dict, + Func, bool> predicate, + Action>? afterRemoved = null) + { + var items = new List>(); + foreach (var item in dict) // avoid linq + { + if (predicate(item)) + items.Add(item); + } + + foreach (var item in items) + { + if (dict.Remove(item.Key)) + afterRemoved?.Invoke(item); + } + } + + /// + /// Chunks the source collection into chunks of the specified size. + /// For example, if the source collection is [1, 2, 3, 4, 5] and the chunk size is 3, the result will be [[1, 2, 3], [4, 5]]. + /// + /// The type of the elements in the collection. + /// The collection to chunk. + /// The size of each chunk. + /// An enumerable of arrays, each containing a chunk of the source collection. + /// Thrown when the source collection is null. + /// Thrown when the chunk size is less than or equal to 0. + public static IEnumerable Chunk(this IReadOnlyCollection? source, int chunkSize) + { + ArgumentNullException.ThrowIfNull(source); + + if (chunkSize <= 0) + throw new ArgumentOutOfRangeException(nameof(chunkSize), "Chunk size must > 0."); + + using IEnumerator enumerator = source.GetEnumerator(); + for (var remain = source.Count; remain > 0;) + { + var chunk = new T[Math.Min(remain, chunkSize)]; + for (var i = 0; i < chunk.Length; i++) + { + if (!enumerator.MoveNext()) // Additional checks + throw new InvalidOperationException("unexpected end of sequence"); + chunk[i] = enumerator.Current; + } + + remain -= chunk.Length; + yield return chunk; + } + } + + /// + /// Randomly samples a specified number of elements from the collection using reservoir sampling algorithm. + /// This method ensures each element has an equal probability of being selected, regardless of the collection size. + /// If the count is greater than the collection size, the entire collection will be returned. + /// + /// The type of the elements in the collection. + /// The collection to sample from. + /// The number of elements to sample. + /// An array containing the randomly sampled elements. + /// Thrown when the collection is null. + /// Thrown when the count is less than 0 + [return: NotNull] + public static T[] Sample(this IReadOnlyCollection collection, int count) + { + ArgumentNullException.ThrowIfNull(collection); + ArgumentOutOfRangeException.ThrowIfLessThan(count, 0, nameof(count)); + + if (count == 0) return []; + + var reservoir = new T[Math.Min(count, collection.Count)]; + var currentIndex = 0; + foreach (var item in collection) + { + if (currentIndex < reservoir.Length) + { + reservoir[currentIndex] = item; + } + else + { + var randomIndex = RandomNumberFactory.NextInt32(0, currentIndex + 1); + if (randomIndex < reservoir.Length) reservoir[randomIndex] = item; + } + currentIndex++; + } + + return reservoir; + } +} diff --git a/src/Neo.Extensions/Collections/HashSetExtensions.cs b/src/Neo.Extensions/Collections/HashSetExtensions.cs new file mode 100644 index 0000000000..dfd7e318cf --- /dev/null +++ b/src/Neo.Extensions/Collections/HashSetExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// HashSetExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Collections; + +public static class HashSetExtensions +{ + public static void Remove(this HashSet set, ISet other) + { + if (set.Count > other.Count) + { + set.ExceptWith(other); + } + else + { + set.RemoveWhere(other.Contains); + } + } + + public static void Remove(this HashSet set, ICollection other) + where T : IEquatable + { + if (set.Count > other.Count) + { + set.ExceptWith(other); + } + else + { + set.RemoveWhere(other.Contains); + } + } + + public static void Remove(this HashSet set, IReadOnlyDictionary other) + { + if (set.Count > other.Count) + { + set.ExceptWith(other.Keys); + } + else + { + set.RemoveWhere(other.ContainsKey); + } + } +} diff --git a/src/Neo.Extensions/DateTimeExtensions.cs b/src/Neo.Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000000..b5bc788f35 --- /dev/null +++ b/src/Neo.Extensions/DateTimeExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// DateTimeExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +public static class DateTimeExtensions +{ + /// + /// Converts a to timestamp. + /// + /// The to convert. + /// The converted timestamp. + public static uint ToTimestamp(this DateTime time) + { + return (uint)new DateTimeOffset(time.ToUniversalTime()).ToUnixTimeSeconds(); + } + + /// + /// Converts a to timestamp in milliseconds. + /// + /// The to convert. + /// The converted timestamp. + public static ulong ToTimestampMS(this DateTime time) + { + return (ulong)new DateTimeOffset(time.ToUniversalTime()).ToUnixTimeMilliseconds(); + } +} diff --git a/src/Neo.Extensions/Exceptions/TryCatchExtensions.cs b/src/Neo.Extensions/Exceptions/TryCatchExtensions.cs new file mode 100644 index 0000000000..c1295841e2 --- /dev/null +++ b/src/Neo.Extensions/Exceptions/TryCatchExtensions.cs @@ -0,0 +1,139 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TryCatchExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Exceptions; + +internal static class TryCatchExtensions +{ + public static TSource TryCatch(this TSource obj, Action action) + where TSource : class? + { + try + { + action(obj); + } + catch + { + } + + return obj; + } + + public static TSource TryCatch(this TSource obj, Action action, Action? onError = default) + where TSource : notnull + where TException : Exception + { + try + { + action(obj); + } + catch (TException ex) + { + onError?.Invoke(obj, ex); + } + + return obj; + } + + public static TResult TryCatch(this TSource obj, Func func, Func? onError = default) + where TSource : notnull + where TException : Exception + { + try + { + return func(obj); + } + catch (TException ex) + { + if (onError == null) throw; + return onError(obj, ex); + } + } + + public static TSource TryCatchThrow(this TSource obj, Action action) + where TSource : class? + where TException : Exception + { + try + { + action(obj); + + return obj; + } + catch (TException) + { + throw; + } + } + + public static TResult TryCatchThrow(this TSource obj, Func func) + where TSource : notnull + where TException : Exception + { + try + { + return func(obj); + } + catch (TException) + { + throw; + } + } + + public static TSource TryCatchThrow(this TSource obj, Action action, string? errorMessage = default) + where TSource : class? + where TException : Exception, new() + { + try + { + action(obj); + + return obj; + } + catch (TException innerException) + { + if (string.IsNullOrEmpty(errorMessage)) + throw; + else + { + if (Activator.CreateInstance(typeof(TException), errorMessage, innerException) is not TException ex) + throw; + else + throw ex; + } + + } + } + + public static TResult? TryCatchThrow(this TSource obj, Func func, string? errorMessage = default) + where TSource : class? + where TException : Exception + where TResult : class? + { + try + { + return func(obj); + } + catch (TException innerException) + { + if (string.IsNullOrEmpty(errorMessage)) + throw; + else + { + if (Activator.CreateInstance(typeof(TException), errorMessage, innerException) is not TException ex) + throw; + else + throw ex; + } + + } + } +} diff --git a/src/Neo.Extensions/Factories/RandomNumberFactory.cs b/src/Neo.Extensions/Factories/RandomNumberFactory.cs new file mode 100644 index 0000000000..66f2121cfe --- /dev/null +++ b/src/Neo.Extensions/Factories/RandomNumberFactory.cs @@ -0,0 +1,267 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// RandomNumberFactory.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Buffers.Binary; +using System.Numerics; +using System.Security.Cryptography; + +namespace Neo.Factories; + +public static class RandomNumberFactory +{ + public static sbyte NextSByte() => + NextSByte(0, sbyte.MaxValue); + + public static sbyte NextSByte(sbyte maxValue) + { + if (maxValue < 0) + throw new ArgumentOutOfRangeException(nameof(maxValue)); + + return NextSByte(0, maxValue); + } + + public static sbyte NextSByte(sbyte minValue, sbyte maxValue) + { + if (minValue == maxValue) return maxValue; + + if (minValue > maxValue) + throw new ArgumentOutOfRangeException(nameof(minValue)); + + return (sbyte)(NextUInt32((uint)(maxValue - minValue)) + minValue); + } + + public static byte NextByte() => + NextByte(0, byte.MaxValue); + + public static byte NextByte(byte maxValue) => + NextByte(0, maxValue); + + public static byte NextByte(byte minValue, byte maxValue) + { + if (minValue == maxValue) return maxValue; + + if (minValue > maxValue) + throw new ArgumentOutOfRangeException(nameof(minValue)); + + return (byte)(NextUInt32((uint)(maxValue - minValue)) + minValue); + } + + public static short NextInt16() => + NextInt16(0, short.MaxValue); + + public static short NextInt16(short maxValue) + { + if (maxValue < 0) + throw new ArgumentOutOfRangeException(nameof(maxValue)); + + return NextInt16(0, maxValue); + } + + public static short NextInt16(short minValue, short maxValue) + { + if (minValue == maxValue) return maxValue; + + if (minValue > maxValue) + throw new ArgumentOutOfRangeException(nameof(minValue)); + + return (short)(NextUInt32((uint)(maxValue - minValue)) + minValue); + } + + public static ushort NextUInt16() => + NextUInt16(0, ushort.MaxValue); + + public static ushort NextUInt16(ushort maxValue) => + NextUInt16(0, maxValue); + + public static ushort NextUInt16(ushort minValue, ushort maxValue) + { + if (minValue == maxValue) return maxValue; + + if (minValue > maxValue) + throw new ArgumentOutOfRangeException(nameof(minValue)); + + return (ushort)(NextUInt32((uint)(maxValue - minValue)) + minValue); + } + + public static int NextInt32() => + NextInt32(0, int.MaxValue); + + public static int NextInt32(int maxValue) + { + ArgumentOutOfRangeException.ThrowIfNegative(maxValue); + + return NextInt32(0, maxValue); + } + + public static int NextInt32(int minValue, int maxValue) + { + if (minValue == maxValue) return maxValue; + + ArgumentOutOfRangeException.ThrowIfGreaterThan(minValue, maxValue); + + return (int)NextUInt32((uint)(maxValue - minValue)) + minValue; + } + + public static uint NextUInt32() + { + Span longBytes = stackalloc byte[4]; + RandomNumberGenerator.Fill(longBytes); + return BinaryPrimitives.ReadUInt32LittleEndian(longBytes); + } + + public static uint NextUInt32(uint maxValue) + { + var randomProduct = (ulong)maxValue * NextUInt32(); + var lowPart = (uint)randomProduct; + + if (lowPart < maxValue) + { + var remainder = (0u - maxValue) % maxValue; + + while (lowPart < remainder) + { + randomProduct = (ulong)maxValue * NextUInt32(); + lowPart = (uint)randomProduct; + } + } + + return (uint)(randomProduct >> 32); + } + + public static uint NextUInt32(uint minValue, uint maxValue) + { + if (minValue == maxValue) return maxValue; + + ArgumentOutOfRangeException.ThrowIfGreaterThan(minValue, maxValue); + + return NextUInt32(maxValue - minValue) + minValue; + } + + public static long NextInt64() => + NextInt64(0L, long.MaxValue); + + public static long NextInt64(long maxValue) + { + return NextInt64(0L, maxValue); + } + + public static long NextInt64(long minValue, long maxValue) + { + if (minValue == maxValue) return maxValue; + + ArgumentOutOfRangeException.ThrowIfGreaterThan(minValue, maxValue); + + return (long)NextUInt64((ulong)(maxValue - minValue)) + minValue; + } + + public static ulong NextUInt64() + { + Span longBytes = stackalloc byte[8]; + RandomNumberGenerator.Fill(longBytes); + return BinaryPrimitives.ReadUInt64LittleEndian(longBytes); + } + + public static ulong NextUInt64(ulong maxValue) + { + var randomProduct = Math.BigMul(maxValue, NextUInt64(), out var lowPart); + + if (lowPart < maxValue) + { + var remainder = (0ul - maxValue) % maxValue; + + while (lowPart < remainder) + { + randomProduct = Math.BigMul(maxValue, NextUInt64(), out lowPart); + } + } + + return randomProduct; + } + + public static ulong NextUInt64(ulong minValue, ulong maxValue) + { + if (minValue == maxValue) return maxValue; + + ArgumentOutOfRangeException.ThrowIfGreaterThan(minValue, maxValue); + + return NextUInt64(maxValue - minValue) + minValue; + } + + public static BigInteger NextBigInteger(BigInteger minValue, BigInteger maxValue) + { + if (minValue == maxValue) return maxValue; + + ArgumentOutOfRangeException.ThrowIfGreaterThan(minValue, maxValue); + + return NextBigInteger(maxValue - minValue) + minValue; + } + + public static BigInteger NextBigInteger(BigInteger maxValue) + { + if (maxValue.Sign < 0) + throw new ArgumentOutOfRangeException(nameof(maxValue)); + + if (maxValue == 0 || maxValue == 1) + return BigInteger.Zero; + + var maxValueBits = maxValue.GetByteCount() * 8; + var maxMaxValue = BigInteger.One << maxValueBits; + + var randomProduct = maxValue * NextBigInteger(maxValueBits); + var lowPart = randomProduct % maxMaxValue; + + if (lowPart < maxValue) + { + var threshold = (maxMaxValue - maxValue) % maxValue; + + while (lowPart < threshold) + { + randomProduct = maxValue * NextBigInteger(maxValueBits); + lowPart = randomProduct % maxMaxValue; + } + } + + return randomProduct >> maxValueBits; + } + + public static BigInteger NextBigInteger(int sizeInBits) + { + if (sizeInBits < 0) + throw new ArgumentException("sizeInBits must be non-negative."); + + if (sizeInBits == 0) + return BigInteger.Zero; + + Span b = stackalloc byte[sizeInBits / 8 + 1]; + RandomNumberGenerator.Fill(b); + + if (sizeInBits % 8 == 0) + b[^1] = 0; + else + b[^1] &= (byte)((1 << sizeInBits % 8) - 1); + + return new BigInteger(b); + } + + public static byte[] NextBytes(int length, bool cryptography = false) + { + ArgumentOutOfRangeException.ThrowIfLessThan(length, 0, nameof(length)); + + var bytes = new byte[length]; + + if (cryptography) + RandomNumberGenerator.Fill(bytes); + else + Random.Shared.NextBytes(bytes); + + return bytes; + } +} diff --git a/src/Neo.Extensions/IntegerExtensions.cs b/src/Neo.Extensions/IntegerExtensions.cs new file mode 100644 index 0000000000..c66df19e99 --- /dev/null +++ b/src/Neo.Extensions/IntegerExtensions.cs @@ -0,0 +1,58 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IntegerExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Runtime.CompilerServices; + +namespace Neo; + +public static class IntegerExtensions +{ + /// + /// Gets the size of variable-length of the data. + /// + /// The length of the data. + /// The size of variable-length of the data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte GetVarSize(this int value) => ((long)value).GetVarSize(); + + /// + /// Gets the size of variable-length of the data. + /// + /// The length of the data. + /// The size of variable-length of the data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte GetVarSize(this ushort value) => ((long)value).GetVarSize(); + + /// + /// Gets the size of variable-length of the data. + /// + /// The length of the data. + /// The size of variable-length of the data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte GetVarSize(this uint value) => ((long)value).GetVarSize(); + + /// + /// Gets the size of variable-length of the data. + /// + /// The length of the data. + /// The size of variable-length of the data. + public static byte GetVarSize(this long value) + { + if (value < 0xFD) + return sizeof(byte); + else if (value <= ushort.MaxValue) + return sizeof(byte) + sizeof(ushort); + else if (value <= uint.MaxValue) + return sizeof(byte) + sizeof(uint); + else + return sizeof(byte) + sizeof(ulong); + } +} diff --git a/src/Neo.Extensions/LogLevel.cs b/src/Neo.Extensions/LogLevel.cs new file mode 100644 index 0000000000..f42151df05 --- /dev/null +++ b/src/Neo.Extensions/LogLevel.cs @@ -0,0 +1,45 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// LogLevel.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using static Akka.Event.LogLevel; + +namespace Neo; + +/// +/// Represents the level of logs. +/// +public enum LogLevel : byte +{ + /// + /// The debug log level. + /// + Debug = DebugLevel, + + /// + /// The information log level. + /// + Info = InfoLevel, + + /// + /// The warning log level. + /// + Warning = WarningLevel, + + /// + /// The error log level. + /// + Error = ErrorLevel, + + /// + /// The fatal log level. + /// + Fatal = Error + 1 +} diff --git a/src/Neo.Extensions/Logs.cs b/src/Neo.Extensions/Logs.cs new file mode 100644 index 0000000000..e6e70c5326 --- /dev/null +++ b/src/Neo.Extensions/Logs.cs @@ -0,0 +1,84 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Logs.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Serilog; +using Serilog.Events; +using System; +using System.Collections.Concurrent; +using System.IO; + +namespace Neo; + +public static class Logs +{ + private static string? s_logDirectory; + + private static readonly ConcurrentDictionary s_loggers = new(); + + private static readonly ILogger s_noopLogger = new LoggerConfiguration().CreateLogger(); + + /// + /// The directory where the logs are stored. If not set, the logs will be disabled. + /// It only can be set once on startup. + /// + public static string? LogDirectory + { + get => s_logDirectory; + set + { + if (s_logDirectory is not null) // cannot be changed after setup + throw new InvalidOperationException("LogDirectory is already set"); + s_logDirectory = value; + } + } + + /// + /// Get a logger for the given source. If the log directory is not set, a no-op logger will be returned. + /// + /// The source of the log. + /// A logger for the given source. + public static ILogger GetLogger(string source) + { + return (LogDirectory is null) ? s_noopLogger : s_loggers.GetOrAdd(source, CreateLogger); + } + + private static ILogger CreateLogger(string source) + { + if (LogDirectory is null) return s_noopLogger; + + foreach (var ch in Path.GetInvalidFileNameChars()) + { + source = source.Replace(ch, '-'); + } + + return new LoggerConfiguration() + .WriteTo.File( + path: Path.Combine(LogDirectory, source, "log-.txt"), + fileSizeLimitBytes: 100 * 1024 * 1024, // 100 MiB + rollOnFileSizeLimit: true, + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 30 // about 1 month + ) + .CreateLogger(); + } + + public static ILogger ConsoleLogger => new LoggerConfiguration().WriteTo.Console().CreateLogger(); + + public static LogEventLevel ToLogEventLevel(this LogLevel level) => level switch + { + LogLevel.Debug => LogEventLevel.Debug, + LogLevel.Info => LogEventLevel.Information, + LogLevel.Warning => LogEventLevel.Warning, + LogLevel.Error => LogEventLevel.Error, + LogLevel.Fatal => LogEventLevel.Fatal, + _ => LogEventLevel.Information, + }; +} diff --git a/src/Neo.Extensions/Neo.Extensions.csproj b/src/Neo.Extensions/Neo.Extensions.csproj new file mode 100644 index 0000000000..f3c45c3bec --- /dev/null +++ b/src/Neo.Extensions/Neo.Extensions.csproj @@ -0,0 +1,25 @@ + + + + Neo + true + NEO;Blockchain;Extensions + + + + + + + + + + + + + + + + + + + diff --git a/src/Neo.Extensions/Network/IpAddressExtensions.cs b/src/Neo.Extensions/Network/IpAddressExtensions.cs new file mode 100644 index 0000000000..fcb4b47389 --- /dev/null +++ b/src/Neo.Extensions/Network/IpAddressExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IpAddressExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Net; + +namespace Neo.Network; + +public static class IpAddressExtensions +{ + /// + /// Checks if address is IPv4 Mapped to IPv6 format, if so, Map to IPv4. + /// Otherwise, return current address. + /// + public static IPAddress UnMap(this IPAddress address) + { + if (address.IsIPv4MappedToIPv6) + address = address.MapToIPv4(); + return address; + } + + /// + /// Checks if IPEndPoint is IPv4 Mapped to IPv6 format, if so, unmap to IPv4. + /// Otherwise, return current endpoint. + /// + public static IPEndPoint UnMap(this IPEndPoint endPoint) + { + if (!endPoint.Address.IsIPv4MappedToIPv6) + return endPoint; + return new IPEndPoint(endPoint.Address.UnMap(), endPoint.Port); + } +} diff --git a/src/Neo.Extensions/SecureStringExtensions.cs b/src/Neo.Extensions/SecureStringExtensions.cs new file mode 100644 index 0000000000..b666bd09be --- /dev/null +++ b/src/Neo.Extensions/SecureStringExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// SecureStringExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.InteropServices; +using System.Security; + +namespace Neo; + +public static class SecureStringExtensions +{ + public static string GetClearText(this SecureString secureString) + { + var unmanagedStringPtr = IntPtr.Zero; + + try + { + unmanagedStringPtr = Marshal.SecureStringToGlobalAllocUnicode(secureString); + return Marshal.PtrToStringUni(unmanagedStringPtr)!; + } + finally + { + Marshal.ZeroFreeGlobalAllocUnicode(unmanagedStringPtr); + } + } + + public static SecureString ToSecureString(this string value, bool asReadOnly = true) + { + unsafe + { + fixed (char* passwordChars = value) + { + var securePasswordString = new SecureString(passwordChars, value.Length); + + if (asReadOnly) + securePasswordString.MakeReadOnly(); + return securePasswordString; + } + } + } +} diff --git a/src/Neo.Extensions/StringExtensions.cs b/src/Neo.Extensions/StringExtensions.cs new file mode 100644 index 0000000000..c91c850f59 --- /dev/null +++ b/src/Neo.Extensions/StringExtensions.cs @@ -0,0 +1,362 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StringExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Neo; + +public static class StringExtensions +{ + /// + /// A strict UTF8 encoding + /// + internal static Encoding StrictUTF8 { get; } + + static StringExtensions() + { + StrictUTF8 = (Encoding)Encoding.UTF8.Clone(); + StrictUTF8.DecoderFallback = DecoderFallback.ExceptionFallback; + StrictUTF8.EncoderFallback = EncoderFallback.ExceptionFallback; + } + + /// + /// Converts a byte span to a strict UTF8 string. + /// + /// The byte span to convert. + /// The converted string. + /// True if the conversion is successful, otherwise false. + public static bool TryToStrictUtf8String(this ReadOnlySpan bytes, [NotNullWhen(true)] out string? value) + { + try + { + value = StrictUTF8.GetString(bytes); + return true; + } + catch + { + value = default; + return false; + } + } + + /// + /// Converts a byte span to a strict UTF8 string. + /// + /// The byte span to convert. + /// The converted string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToStrictUtf8String(this ReadOnlySpan value) + { + try + { + return StrictUTF8.GetString(value); + } + catch (DecoderFallbackException ex) + { + var bytesInfo = value.Length <= 32 ? $"Bytes: [{string.Join(", ", value.ToArray().Select(b => $"0x{b:X2}"))}]" : $"Length: {value.Length} bytes"; + throw new DecoderFallbackException($"Failed to decode byte span to UTF-8 string (strict mode): The input contains invalid UTF-8 byte sequences. {bytesInfo}. Ensure all bytes form valid UTF-8 character sequences.", ex); + } + catch (ArgumentException ex) + { + throw new ArgumentException("Invalid byte span provided for UTF-8 decoding. The span may be corrupted or contain invalid data.", nameof(value), ex); + } + catch (Exception ex) + { + throw new InvalidOperationException("An unexpected error occurred while decoding byte span to UTF-8 string in strict mode. This may indicate a system-level encoding issue.", ex); + } + } + + /// + /// Converts a byte array to a strict UTF8 string. + /// + /// The byte array to convert. + /// The converted string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToStrictUtf8String(this byte[] value) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Cannot decode null byte array to UTF-8 string."); + + try + { + return StrictUTF8.GetString(value); + } + catch (DecoderFallbackException ex) + { + var bytesInfo = value.Length <= 32 ? $"Bytes: {BitConverter.ToString(value)}" : $"Length: {value.Length} bytes, First 16: {BitConverter.ToString(value, 0, Math.Min(16, value.Length))}..."; + throw new DecoderFallbackException($"Failed to decode byte array to UTF-8 string (strict mode): The input contains invalid UTF-8 byte sequences. {bytesInfo}. Ensure all bytes form valid UTF-8 character sequences.", ex); + } + catch (ArgumentException ex) + { + throw new ArgumentException("Invalid byte array provided for UTF-8 decoding. The array may be corrupted or contain invalid data.", nameof(value), ex); + } + catch (Exception ex) + { + throw new InvalidOperationException("An unexpected error occurred while decoding byte array to UTF-8 string in strict mode. This may indicate a system-level encoding issue.", ex); + } + } + + /// + /// Converts a byte array to a strict UTF8 string. + /// + /// The byte array to convert. + /// The start index of the byte array. + /// The count of the byte array. + /// The converted string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToStrictUtf8String(this byte[] value, int start, int count) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Cannot decode null byte array to UTF-8 string."); + if (start < 0) + throw new ArgumentOutOfRangeException(nameof(start), start, "Start index cannot be negative."); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), count, "Count cannot be negative."); + if (start + count > value.Length) + throw new ArgumentOutOfRangeException(nameof(count), $"The specified range [{start}, {start + count}) exceeds the array bounds (length: {value.Length}). Ensure start + count <= array.Length."); + + try + { + return StrictUTF8.GetString(value, start, count); + } + catch (DecoderFallbackException ex) + { + var rangeBytes = new byte[count]; + Array.Copy(value, start, rangeBytes, 0, count); + var bytesInfo = count <= 32 ? $"Bytes: {BitConverter.ToString(rangeBytes)}" : $"Length: {count} bytes, First 16: {BitConverter.ToString(rangeBytes, 0, Math.Min(16, count))}..."; + throw new DecoderFallbackException($"Failed to decode byte array range [{start}, {start + count}) to UTF-8 string (strict mode): The input contains invalid UTF-8 byte sequences. {bytesInfo}. Ensure all bytes form valid UTF-8 character sequences.", ex); + } + catch (ArgumentException ex) + { + throw new ArgumentException($"Invalid parameters provided for UTF-8 decoding. Array length: {value.Length}, Start: {start}, Count: {count}.", ex); + } + catch (Exception ex) + { + throw new InvalidOperationException($"An unexpected error occurred while decoding byte array range [{start}, {start + count}) to UTF-8 string in strict mode. This may indicate a system-level encoding issue.", ex); + } + } + + /// + /// Converts a string to a strict UTF8 byte array. + /// + /// The string to convert. + /// The converted byte array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] ToStrictUtf8Bytes(this string value) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Cannot encode null string to UTF-8 bytes."); + + try + { + return StrictUTF8.GetBytes(value); + } + catch (EncoderFallbackException ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters, First 50: '{value[..50]}...'"; + throw new EncoderFallbackException($"Failed to encode string to UTF-8 bytes (strict mode): The input contains characters that cannot be encoded in UTF-8. {valueInfo}. Ensure the string contains only valid Unicode characters.", ex); + } + catch (ArgumentException ex) + { + throw new ArgumentException("Invalid string provided for UTF-8 encoding. The string may contain unsupported characters.", nameof(value), ex); + } + catch (Exception ex) + { + throw new InvalidOperationException("An unexpected error occurred while encoding string to UTF-8 bytes in strict mode. This may indicate a system-level encoding issue.", ex); + } + } + + /// + /// Gets the size of the specified encoded in strict UTF8. + /// + /// The specified . + /// The size of the . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetStrictUtf8ByteCount(this string value) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Cannot get UTF-8 byte count for null string."); + + try + { + return StrictUTF8.GetByteCount(value); + } + catch (EncoderFallbackException ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters, First 50: '{value[..50]}...'"; + throw new EncoderFallbackException($"Failed to get UTF-8 byte count for string (strict mode): The input contains characters that cannot be encoded in UTF-8. {valueInfo}. Ensure the string contains only valid Unicode characters.", ex); + } + catch (ArgumentException ex) + { + throw new ArgumentException("Invalid string provided for UTF-8 byte count calculation. The string may contain unsupported characters.", nameof(value), ex); + } + catch (Exception ex) + { + throw new InvalidOperationException("An unexpected error occurred while calculating UTF-8 byte count for string in strict mode. This may indicate a system-level encoding issue.", ex); + } + } + + /// + /// Determines if the specified is a valid hex string. + /// + /// The specified . + /// + /// True if the is a valid hex string(or empty); + /// otherwise false(not valid hex string or null). + /// + public static bool IsHex(this string value) + { + if (value is null || value.Length % 2 == 1) + return false; + foreach (var c in value) + { + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) + return false; + } + return true; + } + + /// + /// Converts a hex to byte array. + /// + /// The hex to convert. + /// The converted byte array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] HexToBytes(this string? value) + { + if (value == null) + return []; + + try + { + return HexToBytes(value.AsSpan()); + } + catch (ArgumentException ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new ArgumentException($"Failed to convert hex string to bytes: The input has an invalid length (must be even) or contains non-hexadecimal characters. {valueInfo}. Valid hex characters are 0-9, A-F, and a-f.", nameof(value), ex); + } + catch (FormatException ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new FormatException($"Failed to convert hex string to bytes: The input contains invalid hexadecimal characters. {valueInfo}. Valid hex characters are 0-9, A-F, and a-f.", ex); + } + catch (Exception ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new InvalidOperationException($"An unexpected error occurred while converting hex string to bytes. {valueInfo}. This may indicate a system-level parsing issue.", ex); + } + } + + /// + /// Converts a hex to byte array then reverses the order of the bytes. + /// + /// The hex to convert. + /// The converted reversed byte array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] HexToBytesReversed(this ReadOnlySpan value) + { + try + { + var data = HexToBytes(value); + Array.Reverse(data); + return data; + } + catch (ArgumentException ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new ArgumentException($"Failed to convert hex span to reversed bytes: The input has an invalid length (must be even) or contains non-hexadecimal characters. {valueInfo}. Valid hex characters are 0-9, A-F, and a-f.", ex); + } + catch (FormatException ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new FormatException($"Failed to convert hex span to reversed bytes: The input contains invalid hexadecimal characters. {valueInfo}. Valid hex characters are 0-9, A-F, and a-f.", ex); + } + catch (Exception ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new InvalidOperationException($"An unexpected error occurred while converting hex span to reversed bytes. {valueInfo}. This may indicate a system-level parsing or array manipulation issue.", ex); + } + } + + /// + /// Converts a hex to byte array. + /// + /// The hex to convert. + /// The converted byte array. + public static byte[] HexToBytes(this ReadOnlySpan value) + { + try + { + return Convert.FromHexString(value); + } + catch (ArgumentException ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new ArgumentException($"Failed to convert hex span to bytes: The input has an invalid length (must be even) or contains non-hexadecimal characters. {valueInfo}. Valid hex characters are 0-9, A-F, and a-f.", ex); + } + catch (FormatException ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new FormatException($"Failed to convert hex span to bytes: The input contains invalid hexadecimal characters. {valueInfo}. Valid hex characters are 0-9, A-F, and a-f.", ex); + } + catch (Exception ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new InvalidOperationException($"An unexpected error occurred while converting hex span to bytes. {valueInfo}. This may indicate a system-level parsing issue.", ex); + } + } + + /// + /// Gets the size of the specified encoded in variable-length encoding. + /// + /// The specified . + /// The size of the . + public static int GetVarSize(this string value) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Cannot calculate variable size for null string."); + + try + { + var size = value.GetStrictUtf8ByteCount(); + return size.GetVarSize() + size; + } + catch (EncoderFallbackException ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters, First 50: '{value[..50]}...'"; + throw new EncoderFallbackException($"Failed to calculate variable size: The string contains characters that cannot be encoded in UTF-8 (strict mode). {valueInfo}. Ensure the string contains only valid Unicode characters.", ex); + } + catch (Exception ex) + { + var valueInfo = value.Length <= 100 ? $"Input: '{value}'" : $"Input length: {value.Length} characters"; + throw new InvalidOperationException($"An unexpected error occurred while calculating variable size for string. {valueInfo}. This may indicate an issue with the string encoding or variable size calculation.", ex); + } + } + + /// + /// Trims the specified prefix from the start of the , ignoring case. + /// + /// The to trim. + /// The prefix to trim. + /// + /// The trimmed ReadOnlySpan without prefix. If no prefix is found, the input is returned unmodified. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan TrimStartIgnoreCase(this ReadOnlySpan value, ReadOnlySpan prefix) + { + if (value.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) + return value[prefix.Length..]; + return value; + } +} diff --git a/src/Neo.Extensions/Utility.cs b/src/Neo.Extensions/Utility.cs new file mode 100644 index 0000000000..7211f22f34 --- /dev/null +++ b/src/Neo.Extensions/Utility.cs @@ -0,0 +1,55 @@ +// Copyright (C) 2015-2025 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 Akka.Actor; +using Akka.Event; +using System.Text; + +namespace Neo; + +public delegate void LogEventHandler(string source, LogLevel level, object message); + +/// +/// A utility class that provides common functions. +/// +public static class Utility +{ + internal class Logger : ReceiveActor + { + public Logger() + { + Receive(_ => Sender.Tell(new LoggerInitialized())); + Receive(e => Log("Akka", (LogLevel)e.LogLevel(), $"[{e.LogSource}] {e.Message}{Environment.NewLine}{e.Cause?.StackTrace ?? ""}")); + } + } + + public static LogLevel LogLevel { get; set; } = LogLevel.Info; + + public static event LogEventHandler? Logging; + + /// + /// A strict UTF8 encoding used in NEO system. + /// + public static Encoding StrictUTF8 => StringExtensions.StrictUTF8; + + /// + /// Writes a log. + /// + /// The source of the log. Used to identify the producer of the log. + /// The level of the log. + /// The message of the log. + public static void Log(string source, LogLevel level, object message) + { + if ((int)level < (int)LogLevel) return; + + Logging?.Invoke(source, level, message); + } +} diff --git a/src/Neo.IO/Actors/Idle.cs b/src/Neo.IO/Actors/Idle.cs new file mode 100644 index 0000000000..d1fabd61d6 --- /dev/null +++ b/src/Neo.IO/Actors/Idle.cs @@ -0,0 +1,17 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Idle.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.IO.Actors; + +internal sealed class Idle +{ + public static Idle Instance { get; } = new(); +} diff --git a/src/Neo.IO/Caching/Cache.cs b/src/Neo.IO/Caching/Cache.cs new file mode 100644 index 0000000000..a1699f24bd --- /dev/null +++ b/src/Neo.IO/Caching/Cache.cs @@ -0,0 +1,273 @@ +// Copyright (C) 2015-2025 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 System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Neo.IO.Caching; + +public abstract class Cache(int maxCapacity, IEqualityComparer? comparer = null) + : ICollection, IDisposable where TKey : notnull where TValue : notnull +{ + protected class CacheItem + { + public readonly TKey Key; + public readonly TValue Value; + + private CacheItem _prev, _next; + + public CacheItem(TKey key, TValue value) + { + Key = key; + Value = value; + _prev = this; + _next = this; + } + + public bool IsEmpty => ReferenceEquals(_prev, this); + + /// + /// Adds an item after the current item. + /// + /// The item to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(CacheItem another) => another.Link(this, _next); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Link(CacheItem prev, CacheItem next) + { + _prev = prev; + _next = next; + prev._next = this; + next._prev = this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Unlink() + { + _prev._next = _next; + _next._prev = _prev; + _prev = this; + _next = this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CacheItem? RemovePrevious() + { + if (IsEmpty) return null; + var prev = _prev; + prev.Unlink(); + return prev; + } + } + + protected CacheItem Head { get; } = new(default!, default!); + private readonly Lock _lock = new(); + + private readonly Dictionary _innerDictionary = new(comparer); + + public TValue this[TKey key] + { + get + { + lock (_lock) + { + if (!_innerDictionary.TryGetValue(key, out var item)) + throw new KeyNotFoundException(); + OnAccess(item); + return item.Value; + } + } + } + + public int Count + { + get + { + lock (_lock) + { + return _innerDictionary.Count; + } + } + } + + public bool IsReadOnly => false; + + public bool IsDisposable { get; } = typeof(IDisposable).IsAssignableFrom(typeof(TValue)); + + public void Add(TValue item) + { + var key = GetKeyForItem(item); + lock (_lock) + { + AddInternal(key, item); + } + } + + private void AddInternal(TKey key, TValue item) + { + if (_innerDictionary.TryGetValue(key, out var cached)) + { + OnAccess(cached); + } + else + { + if (_innerDictionary.Count >= maxCapacity) + { + var prev = Head.RemovePrevious(); + if (prev is not null) RemoveInternal(prev.Key); + } + + var added = new CacheItem(key, item); + _innerDictionary.Add(key, added); + Head.Add(added); + } + } + + public void AddRange(IEnumerable items) + { + lock (_lock) + { + foreach (var item in items) + { + var key = GetKeyForItem(item); + AddInternal(key, item); + } + } + } + + public void Clear() + { + CacheItem[]? items = null; + + lock (_lock) + { + if (IsDisposable) + { + items = [.. _innerDictionary.Values]; + } + _innerDictionary.Clear(); + Head.Unlink(); + } + + if (items != null) + { + foreach (var item in items) + { + if (item.Value is IDisposable disposable) disposable.Dispose(); + } + } + } + + public bool Contains(TKey key) + { + lock (_lock) + { + if (!_innerDictionary.TryGetValue(key, out var cached)) return false; + OnAccess(cached); + return true; + } + } + + public bool Contains(TValue item) + { + return Contains(GetKeyForItem(item)); + } + + public void CopyTo(TValue[] array, int startIndex) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(startIndex); + + lock (_lock) + { + var count = _innerDictionary.Count; + if (startIndex + count > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(startIndex), + $"startIndex({startIndex}) + Count({count}) > Length({array.Length})"); + } + + foreach (var item in _innerDictionary.Values) + { + array[startIndex++] = item.Value; + } + } + } + + public void Dispose() + { + Clear(); + GC.SuppressFinalize(this); + } + + public IEnumerator GetEnumerator() + { + TValue[] values; + lock (_lock) + { + var index = 0; + values = new TValue[_innerDictionary.Count]; + foreach (var item in _innerDictionary.Values) + { + values[index++] = item.Value; + } + } + return values.AsEnumerable().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + protected abstract TKey GetKeyForItem(TValue item); + + public bool Remove(TKey key) + { + lock (_lock) + { + return RemoveInternal(key); + } + } + + protected abstract void OnAccess(CacheItem item); + + public bool Remove(TValue item) + { + return Remove(GetKeyForItem(item)); + } + + private bool RemoveInternal(TKey key) + { + if (!_innerDictionary.Remove(key, out var item)) return false; + + item.Unlink(); + if (IsDisposable && item.Value is IDisposable disposable) + { + disposable.Dispose(); + } + return true; + } + + public bool TryGet(TKey key, [NotNullWhen(true)] out TValue? item) + { + lock (_lock) + { + if (_innerDictionary.TryGetValue(key, out var cached)) + { + OnAccess(cached); + item = cached.Value; + return true; + } + } + item = default; + return false; + } +} diff --git a/src/Neo.IO/Caching/FIFOCache.cs b/src/Neo.IO/Caching/FIFOCache.cs new file mode 100644 index 0000000000..4f27123fb1 --- /dev/null +++ b/src/Neo.IO/Caching/FIFOCache.cs @@ -0,0 +1,18 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FIFOCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.IO.Caching; + +public abstract class FIFOCache(int maxCapacity, IEqualityComparer? comparer = null) + : Cache(maxCapacity, comparer) where TKey : notnull where TValue : notnull +{ + protected override void OnAccess(CacheItem item) { } +} diff --git a/src/Neo.IO/Caching/HashSetCache.cs b/src/Neo.IO/Caching/HashSetCache.cs new file mode 100644 index 0000000000..f45917f78c --- /dev/null +++ b/src/Neo.IO/Caching/HashSetCache.cs @@ -0,0 +1,135 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// HashSetCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#nullable enable + +using System.Collections; +using System.Runtime.CompilerServices; + +namespace Neo.IO.Caching; + +/// +/// A cache that uses a hash set to store items. +/// +/// The type of the items in the cache. +internal class HashSetCache : ICollection where T : IEquatable +{ + private class Items(int initialCapacity) : KeyedCollectionSlim(initialCapacity) + { + protected sealed override T GetKeyForItem(T item) => item; + } + + private readonly int _capacity; + private readonly Items _items; + + /// + /// Gets the number of items in the cache. + /// + public int Count => _items.Count; + + public bool IsReadOnly => false; + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of items in the cache. + /// is less than 0. + public HashSetCache(int capacity) + { + if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity), $"{capacity} less than 0."); + + _capacity = capacity; + // Avoid allocating a large memory at initialization + _items = new(Math.Min(capacity, 4096)); + } + + /// + /// Adds an item to the cache. + /// + /// The item to add. + /// + /// if the item was added; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryAdd(T item) + { + if (!_items.TryAdd(item)) return false; + if (_items.Count > _capacity) _items.RemoveFirst(); + return true; + } + + /// + /// Determines whether the cache contains an item. + /// + /// The item to locate in the cache. + /// + /// if the item is found in the cache; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(T item) => _items.Contains(item); + + /// + /// Removes all items from the cache. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() => _items.Clear(); + + /// + /// Removes an item from the cache. + /// + /// The items to remove. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ExceptWith(IEnumerable items) + { + foreach (var item in items) _items.Remove(item); + } + + /// + /// Returns an enumerator that iterates through the cache. + /// + /// An enumerator that can be used to iterate through the cache. + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through the cache. + /// + /// An enumerator that can be used to iterate through the cache. + IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); + + public void Add(T item) + { + _ = TryAdd(item); + } + + public bool Remove(T item) + { + return _items.Remove(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + + if (arrayIndex < 0 || arrayIndex > array.Length) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + + if (array.Length - arrayIndex < Count) + throw new ArgumentException("The number of elements in the source ICollection is greater than the available space from arrayIndex to the end of the destination array."); + + var i = arrayIndex; + foreach (var item in this) + { + array[i++] = item; + } + } +} + +#nullable disable diff --git a/src/Neo.IO/Caching/IndexedQueue.cs b/src/Neo.IO/Caching/IndexedQueue.cs new file mode 100644 index 0000000000..f6358b7e7a --- /dev/null +++ b/src/Neo.IO/Caching/IndexedQueue.cs @@ -0,0 +1,248 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IndexedQueue.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#nullable enable + +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.IO.Caching; + +/// +/// Represents a queue with indexed access to the items +/// +/// The type of items in the queue +class IndexedQueue : IReadOnlyCollection +{ + private const int DefaultCapacity = 16; + private const int GrowthFactor = 2; + private const float TrimThreshold = 0.9f; + + private T[] _array; + private int _head; + private int _count; + + /// + /// Indicates the count of items in the queue + /// + public int Count => _count; + + /// + /// Creates a queue with the default capacity + /// + public IndexedQueue() : this(DefaultCapacity) + { + } + + /// + /// Creates a queue with the specified capacity + /// + /// The initial capacity of the queue + public IndexedQueue(int capacity) + { + if (capacity <= 0) + throw new ArgumentOutOfRangeException(nameof(capacity), "The capacity must be greater than zero."); + _array = new T[capacity]; + _head = 0; + _count = 0; + } + + /// + /// Creates a queue filled with the specified items + /// + /// The collection of items to fill the queue with + public IndexedQueue(IEnumerable collection) + { + _array = collection.ToArray(); + _head = 0; + _count = _array.Length; + } + + /// + /// Gets the value at the index + /// + /// The index + /// The value at the specified index + public ref T this[int index] + { + get + { + if (index < 0 || index >= _count) + throw new IndexOutOfRangeException(); + return ref _array[(index + _head) % _array.Length]; + } + } + + /// + /// Inserts an item at the rear of the queue + /// + /// The item to insert + public void Enqueue(T item) + { + if (_array.Length == _count) + { + var newSize = _array.Length * GrowthFactor; + if (_head == 0) + { + Array.Resize(ref _array, newSize); + } + else + { + var buffer = new T[newSize]; + Array.Copy(_array, _head, buffer, 0, _array.Length - _head); + Array.Copy(_array, 0, buffer, _array.Length - _head, _head); + _array = buffer; + _head = 0; + } + } + _array[(_head + _count) % _array.Length] = item; + ++_count; + } + + /// + /// Provides access to the item at the front of the queue without dequeuing it + /// + /// The front most item + public T Peek() + { + if (_count == 0) + throw new InvalidOperationException("The queue is empty."); + return _array[_head]; + } + + /// + /// Attempts to return an item from the front of the queue without removing it + /// + /// The item + /// True if the queue returned an item or false if the queue is empty + public bool TryPeek([NotNullWhen(true)] out T? item) + { + if (_count == 0) + { + item = default!; + return false; + } + else + { + item = _array[_head]!; + return true; + } + } + + /// + /// Removes an item from the front of the queue, returning it + /// + /// The item that was removed + public T Dequeue() + { + if (_count == 0) + throw new InvalidOperationException("The queue is empty"); + var result = _array[_head]; + _array[_head] = default!; + ++_head; + _head %= _array.Length; + --_count; + return result; + } + + /// + /// Attempts to return an item from the front of the queue, removing it + /// + /// The item + /// True if the queue returned an item or false if the queue is empty + public bool TryDequeue([NotNullWhen(true)] out T? item) + { + if (_count == 0) + { + item = default!; + return false; + } + else + { + item = _array[_head]!; + _array[_head] = default!; + ++_head; + _head %= _array.Length; + --_count; + return true; + } + } + + /// + /// Clears the items from the queue + /// + public void Clear() + { + Array.Clear(_array, _head, _count); + _head = 0; + _count = 0; + } + + /// + /// Trims the extra array space that isn't being used. + /// + public void TrimExcess() + { + if (_count == 0) + { + _array = new T[DefaultCapacity]; + } + else if (_array.Length * TrimThreshold >= _count) + { + var arr = new T[_count]; + CopyTo(arr, 0); + _array = arr; + _head = 0; + } + } + + /// + /// Copy the queue's items to a destination array + /// + /// The destination array + /// The index in the destination to start copying at + public void CopyTo(T[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + if (arrayIndex < 0 || arrayIndex + _count > array.Length) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + if (_head + _count <= _array.Length) + { + Array.Copy(_array, _head, array, arrayIndex, _count); + } + else + { + Array.Copy(_array, _head, array, arrayIndex, _array.Length - _head); + Array.Copy(_array, 0, array, arrayIndex + _array.Length - _head, _count + _head - _array.Length); + } + } + + /// + /// Returns an array of the items in the queue + /// + /// An array containing the queue's items + public T[] ToArray() + { + var result = new T[_count]; + CopyTo(result, 0); + return result; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < _count; i++) + yield return _array[(_head + i) % _array.Length]; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +#nullable disable diff --git a/src/Neo.IO/Caching/KeyedCollectionSlim.cs b/src/Neo.IO/Caching/KeyedCollectionSlim.cs new file mode 100644 index 0000000000..05aa379ae5 --- /dev/null +++ b/src/Neo.IO/Caching/KeyedCollectionSlim.cs @@ -0,0 +1,141 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// KeyedCollectionSlim.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; +using System.Runtime.CompilerServices; + +namespace Neo.IO.Caching; + +/// +/// A slimmed down version of KeyedCollection; +/// +/// The type of the keys. +/// The type of the items. +internal abstract class KeyedCollectionSlim : IReadOnlyCollection where TKey : notnull +{ + private LinkedList _items = new(); + + private readonly Dictionary> _dict; + + /// + /// Gets the number of items in the collection. + /// + public int Count => _dict.Count; + + /// + /// Gets the first item in the collection, or the default value if the collection is empty. + /// + public TItem? FirstOrDefault => _items.First is not null ? _items.First.Value : default; + + /// + /// Initializes a new instance of the class. + /// + /// The initial capacity of the collection. + /// is less than 0. + public KeyedCollectionSlim(int initialcapacity = 0) + { + if (initialcapacity < 0) + throw new ArgumentOutOfRangeException(nameof(initialcapacity), $"{initialcapacity} less than 0."); + _dict = new(initialcapacity); + } + + /// + /// Gets the key for an item. + /// + /// The item to get the key for. + /// The key for the item. + protected abstract TKey GetKeyForItem(TItem item); + + /// + /// Adds an item to the collection. + /// + /// The item to add. + /// if the item was added; otherwise, . + public bool TryAdd(TItem item) + { + var key = GetKeyForItem(item); + var node = _items.AddLast(item); + if (!_dict.TryAdd(key, node)) + { + _items.RemoveLast(); + return false; + } + return true; + } + + /// + /// Determines whether the collection contains an item with the specified key. + /// + /// The key to locate in the collection. + /// + /// if the collection contains an item with the key; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(TKey key) => _dict.ContainsKey(key); + + /// + /// Removes an item from the collection. + /// + /// The key of the item to remove. + /// + /// if the item was removed successfully; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Remove(TKey key) + { + if (_dict.Remove(key, out var node)) + { + _items.Remove(node); + return true; + } + return false; + } + + /// + /// Removes the first item from the collection. + /// + /// + /// if the item was removed successfully; otherwise, . + /// + public bool RemoveFirst() + { + var first = _items.First; + if (first is null) return false; + + var key = GetKeyForItem(first.Value); + _dict.Remove(key); + _items.RemoveFirst(); + return true; + } + + /// + /// Removes all items from the collection. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + // reset _items, not _items.Clear(), because LinkedList.Clear() is O(n). + _items = new(); + _dict.Clear(); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/Neo.IO/Caching/LRUCache.cs b/src/Neo.IO/Caching/LRUCache.cs new file mode 100644 index 0000000000..05c6c4ac02 --- /dev/null +++ b/src/Neo.IO/Caching/LRUCache.cs @@ -0,0 +1,22 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// LRUCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.IO.Caching; + +public abstract class LRUCache(int maxCapacity, IEqualityComparer? comparer = null) + : Cache(maxCapacity, comparer) where TKey : notnull where TValue : notnull +{ + protected override void OnAccess(CacheItem item) + { + item.Unlink(); + Head.Add(item); + } +} diff --git a/src/Neo.IO/Caching/ReflectionCacheAttribute.cs b/src/Neo.IO/Caching/ReflectionCacheAttribute.cs new file mode 100644 index 0000000000..3a411d4d21 --- /dev/null +++ b/src/Neo.IO/Caching/ReflectionCacheAttribute.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ReflectionCacheAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.IO.Caching; + +/// +/// Constructor +/// +/// Type +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] +internal class ReflectionCacheAttribute + (Type type) : Attribute +{ + /// + /// Type + /// + public Type Type { get; } = type; +} diff --git a/src/Neo.IO/ISerializable.cs b/src/Neo.IO/ISerializable.cs new file mode 100644 index 0000000000..bfe0d62457 --- /dev/null +++ b/src/Neo.IO/ISerializable.cs @@ -0,0 +1,35 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ISerializable.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.IO; + +/// +/// Represents NEO objects that can be serialized. +/// +public interface ISerializable +{ + /// + /// The size of the object in bytes after serialization. + /// + int Size { get; } + + /// + /// Serializes the object using the specified . + /// + /// The for writing data. + void Serialize(BinaryWriter writer); + + /// + /// Deserializes the object using the specified . + /// + /// The for reading data. + void Deserialize(ref MemoryReader reader); +} diff --git a/src/Neo.IO/ISerializableSpan.cs b/src/Neo.IO/ISerializableSpan.cs new file mode 100644 index 0000000000..e44beb6364 --- /dev/null +++ b/src/Neo.IO/ISerializableSpan.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ISerializableSpan.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.IO; + +/// +/// Represents NEO objects that can be serialized. +/// +public interface ISerializableSpan +{ + /// + /// The size of the object in bytes after serialization. + /// + int Size { get; } + + /// + /// Gets a ReadOnlySpan that represents the current value + /// Requires keeping the data returned by GetSpan consistent with the data generated by . + /// + ReadOnlySpan GetSpan(); + + /// + /// Serializes the object using the specified Span. + /// + /// The Span for writing data. + void Serialize(Span destination) + { + var buffer = GetSpan(); + buffer.CopyTo(destination); + } +} diff --git a/src/Neo.IO/MemoryReader.cs b/src/Neo.IO/MemoryReader.cs new file mode 100644 index 0000000000..f27e9f7116 --- /dev/null +++ b/src/Neo.IO/MemoryReader.cs @@ -0,0 +1,242 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MemoryReader.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace Neo.IO; + +public ref struct MemoryReader +{ + private readonly ReadOnlyMemory _memory; + private readonly ReadOnlySpan _span; + private int _pos = 0; + + public readonly int Position => _pos; + + public MemoryReader(ReadOnlyMemory memory) + { + _memory = memory; + _span = memory.Span; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly void EnsurePosition(int move) + { + if (_pos + move > _span.Length) + throw new FormatException($"Position {_pos} + Wanted {move} is exceeded boundary({_span.Length})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly byte Peek() + { + EnsurePosition(1); + return _span[_pos]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ReadBoolean() + { + var value = ReadByte(); + return value switch + { + 0 => false, + 1 => true, + _ => throw new FormatException($"Invalid boolean value: {value}") + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public sbyte ReadSByte() + { + EnsurePosition(1); + var b = _span[_pos++]; + return unchecked((sbyte)b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() + { + EnsurePosition(1); + return _span[_pos++]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short ReadInt16() + { + EnsurePosition(sizeof(short)); + var result = BinaryPrimitives.ReadInt16LittleEndian(_span[_pos..]); + _pos += sizeof(short); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short ReadInt16BigEndian() + { + EnsurePosition(sizeof(short)); + var result = BinaryPrimitives.ReadInt16BigEndian(_span[_pos..]); + _pos += sizeof(short); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort ReadUInt16() + { + EnsurePosition(sizeof(ushort)); + var result = BinaryPrimitives.ReadUInt16LittleEndian(_span[_pos..]); + _pos += sizeof(ushort); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort ReadUInt16BigEndian() + { + EnsurePosition(sizeof(ushort)); + var result = BinaryPrimitives.ReadUInt16BigEndian(_span[_pos..]); + _pos += sizeof(ushort); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadInt32() + { + EnsurePosition(sizeof(int)); + var result = BinaryPrimitives.ReadInt32LittleEndian(_span[_pos..]); + _pos += sizeof(int); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadInt32BigEndian() + { + EnsurePosition(sizeof(int)); + var result = BinaryPrimitives.ReadInt32BigEndian(_span[_pos..]); + _pos += sizeof(int); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ReadUInt32() + { + EnsurePosition(sizeof(uint)); + var result = BinaryPrimitives.ReadUInt32LittleEndian(_span[_pos..]); + _pos += sizeof(uint); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ReadUInt32BigEndian() + { + EnsurePosition(sizeof(uint)); + var result = BinaryPrimitives.ReadUInt32BigEndian(_span[_pos..]); + _pos += sizeof(uint); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long ReadInt64() + { + EnsurePosition(sizeof(long)); + var result = BinaryPrimitives.ReadInt64LittleEndian(_span[_pos..]); + _pos += sizeof(long); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long ReadInt64BigEndian() + { + EnsurePosition(sizeof(long)); + var result = BinaryPrimitives.ReadInt64BigEndian(_span[_pos..]); + _pos += sizeof(long); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReadUInt64() + { + EnsurePosition(sizeof(ulong)); + var result = BinaryPrimitives.ReadUInt64LittleEndian(_span[_pos..]); + _pos += sizeof(ulong); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReadUInt64BigEndian() + { + EnsurePosition(sizeof(ulong)); + var result = BinaryPrimitives.ReadUInt64BigEndian(_span[_pos..]); + _pos += sizeof(ulong); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReadVarInt(ulong max = ulong.MaxValue) + { + var b = ReadByte(); + var value = b switch + { + 0xfd => ReadUInt16(), + 0xfe => ReadUInt32(), + 0xff => ReadUInt64(), + _ => b + }; + if (value > max) throw new FormatException($"VarInt value is greater than max: {value}/{max}"); + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadFixedString(int length) + { + EnsurePosition(length); + var end = _pos + length; + var i = _pos; + while (i < end && _span[i] != 0) i++; + var data = _span[_pos..i]; + for (; i < end; i++) + { + if (_span[i] != 0) + throw new FormatException($"The padding is not 0 at fixed string offset {i}"); + } + _pos = end; + return data.ToStrictUtf8String(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadVarString(int max = 0x1000000) + { + var length = (int)ReadVarInt((ulong)max); + EnsurePosition(length); + var data = _span.Slice(_pos, length); + _pos += length; + return data.ToStrictUtf8String(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyMemory ReadMemory(int count) + { + EnsurePosition(count); + var result = _memory.Slice(_pos, count); + _pos += count; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyMemory ReadVarMemory(int max = 0x1000000) => + ReadMemory((int)ReadVarInt((ulong)max)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyMemory ReadToEnd() + { + var result = _memory[_pos..]; + _pos = _memory.Length; + return result; + } +} diff --git a/src/Neo.IO/Neo.IO.csproj b/src/Neo.IO/Neo.IO.csproj new file mode 100644 index 0000000000..65ac309d54 --- /dev/null +++ b/src/Neo.IO/Neo.IO.csproj @@ -0,0 +1,19 @@ + + + + NEO;Blockchain;IO + + + + + + + + + + + + + + + diff --git a/src/Neo.Json/JArray.cs b/src/Neo.Json/JArray.cs new file mode 100644 index 0000000000..1e0e277b4d --- /dev/null +++ b/src/Neo.Json/JArray.cs @@ -0,0 +1,142 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JArray.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; +using System.Text.Json; + +namespace Neo.Json; + +/// +/// Represents a JSON array. +/// +public class JArray : JContainer, IList +{ + private readonly List _items = []; + + /// + /// Initializes a new instance of the class. + /// + /// The initial items in the array. + public JArray(params JToken?[] items) : this((IEnumerable)items) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial items in the array. + public JArray(IEnumerable items) + { + _items.AddRange(items); + } + + public override JToken? this[int index] + { + get + { + return _items[index]; + } + set + { + _items[index] = value; + } + } + + public override IReadOnlyList Children => _items; + + public bool IsReadOnly + { + get + { + return false; + } + } + + public void Add(JToken? item) + { + _items.Add(item); + } + + public override string AsString() + { + return ToString(); + } + + public override void Clear() + { + _items.Clear(); + } + + public bool Contains(JToken? item) + { + return _items.Contains(item); + } + + public IEnumerator GetEnumerator() + { + return _items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public int IndexOf(JToken? item) + { + return _items.IndexOf(item); + } + + public void Insert(int index, JToken? item) + { + _items.Insert(index, item); + } + + public bool Remove(JToken? item) + { + return _items.Remove(item); + } + + public void RemoveAt(int index) + { + _items.RemoveAt(index); + } + + internal override void Write(Utf8JsonWriter writer) + { + writer.WriteStartArray(); + foreach (var item in _items) + { + if (item is null) + writer.WriteNullValue(); + else + item.Write(writer); + } + writer.WriteEndArray(); + } + + public override JToken Clone() + { + var cloned = new JArray(); + + foreach (var item in _items) + { + cloned.Add(item?.Clone()); + } + + return cloned; + } + + public static implicit operator JArray(JToken?[] value) + { + return [.. value]; + } +} diff --git a/src/Neo.Json/JBoolean.cs b/src/Neo.Json/JBoolean.cs new file mode 100644 index 0000000000..9508f88d77 --- /dev/null +++ b/src/Neo.Json/JBoolean.cs @@ -0,0 +1,103 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JBoolean.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Text.Json; + +namespace Neo.Json; + +/// +/// Represents a JSON boolean value. +/// +public class JBoolean : JToken +{ + /// + /// Gets the value of the JSON token. + /// + public bool Value { get; } + + /// + /// Initializes a new instance of the class with the specified value. + /// + /// The value of the JSON token. + public JBoolean(bool value = false) + { + Value = value; + } + + public override bool AsBoolean() + { + return Value; + } + + /// + /// Converts the current JSON token to a floating point number. + /// + /// The number 1 if value is ; otherwise, 0. + public override double AsNumber() + { + return Value ? 1 : 0; + } + + public override string AsString() + { + return Value.ToString().ToLowerInvariant(); + } + + public override bool GetBoolean() => Value; + + public override string ToString() + { + return AsString(); + } + + internal override void Write(Utf8JsonWriter writer) + { + writer.WriteBooleanValue(Value); + } + + public override JToken Clone() + { + return this; + } + + public static implicit operator JBoolean(bool value) + { + return new JBoolean(value); + } + + public static bool operator ==(JBoolean left, JBoolean right) + { + return left.Value.Equals(right.Value); + } + + public static bool operator !=(JBoolean left, JBoolean right) + { + return !left.Value.Equals(right.Value); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + if (obj is JBoolean other) + { + return Value.Equals(other.Value); + } + return false; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } +} diff --git a/src/Neo.Json/JContainer.cs b/src/Neo.Json/JContainer.cs new file mode 100644 index 0000000000..52885b3dc2 --- /dev/null +++ b/src/Neo.Json/JContainer.cs @@ -0,0 +1,29 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JContainer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Json; + +public abstract class JContainer : JToken +{ + public override JToken? this[int index] => Children[index]; + + public abstract IReadOnlyList Children { get; } + + public int Count => Children.Count; + + public abstract void Clear(); + + public void CopyTo(JToken?[] array, int arrayIndex) + { + for (var i = 0; i < Count && i + arrayIndex < array.Length; i++) + array[i + arrayIndex] = Children[i]; + } +} diff --git a/src/Neo.Json/JNumber.cs b/src/Neo.Json/JNumber.cs new file mode 100644 index 0000000000..511b355332 --- /dev/null +++ b/src/Neo.Json/JNumber.cs @@ -0,0 +1,167 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JNumber.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Globalization; +using System.Text.Json; + +namespace Neo.Json; + +/// +/// Represents a JSON number. +/// +public class JNumber : JToken +{ + /// + /// Represents the largest safe integer in JSON. + /// + public static readonly long MAX_SAFE_INTEGER = (long)Math.Pow(2, 53) - 1; + + /// + /// Represents the smallest safe integer in JSON. + /// + public static readonly long MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER; + + /// + /// Gets the value of the JSON token. + /// + public double Value { get; } + + /// + /// Initializes a new instance of the class with the specified value. + /// + /// The value of the JSON token. + public JNumber(double value = 0) + { + if (!double.IsFinite(value)) throw new FormatException($"value is not finite: {value}"); + Value = value; + } + + /// + /// Converts the current JSON token to a boolean value. + /// + /// if value is not zero; otherwise, . + public override bool AsBoolean() + { + return Value != 0; + } + + public override double AsNumber() + { + return Value; + } + + public override string AsString() + { + return Value.ToString(CultureInfo.InvariantCulture); + } + + public override double GetNumber() => Value; + + public override string ToString() + { + return AsString(); + } + + public override T AsEnum(T defaultValue = default, bool ignoreCase = false) + { + var enumType = typeof(T); + object value; + try + { + value = Convert.ChangeType(Value, enumType.GetEnumUnderlyingType()); + } + catch (OverflowException) + { + return defaultValue; + } + var result = Enum.ToObject(enumType, value); + return Enum.IsDefined(enumType, result) ? (T)result : defaultValue; + } + + public override T GetEnum(bool ignoreCase = false) + { + var enumType = typeof(T); + object value; + try + { + value = Convert.ChangeType(Value, enumType.GetEnumUnderlyingType()); + } + catch (OverflowException) + { + throw new InvalidCastException($"The value is out of range for the enum {enumType.FullName}"); + } + + var result = Enum.ToObject(enumType, value); + if (!Enum.IsDefined(enumType, result)) + throw new InvalidCastException($"The value is not defined in the enum {enumType.FullName}"); + return (T)result; + } + + internal override void Write(Utf8JsonWriter writer) + { + writer.WriteNumberValue(Value); + } + + public override JToken Clone() + { + return this; + } + + public static implicit operator JNumber(double value) + { + return new JNumber(value); + } + + public static implicit operator JNumber(long value) + { + return new JNumber(value); + } + + public static bool operator ==(JNumber left, JNumber? right) + { + if (right is null) return false; + return ReferenceEquals(left, right) || left.Value.Equals(right.Value); + } + + public static bool operator !=(JNumber left, JNumber right) + { + return !(left == right); + } + + public override bool Equals(object? obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + + var other = obj switch + { + JNumber jNumber => jNumber, + uint u => new JNumber(u), + int i => new JNumber(i), + ulong ul => new JNumber(ul), + long l => new JNumber(l), + byte b => new JNumber(b), + sbyte sb => new JNumber(sb), + short s => new JNumber(s), + ushort us => new JNumber(us), + decimal d => new JNumber((double)d), + float f => new JNumber(f), + double d => new JNumber(d), + _ => throw new ArgumentOutOfRangeException(nameof(obj), obj, null) + }; + return other == this; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } +} diff --git a/src/Neo.Json/JObject.cs b/src/Neo.Json/JObject.cs new file mode 100644 index 0000000000..edc01e6396 --- /dev/null +++ b/src/Neo.Json/JObject.cs @@ -0,0 +1,107 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JObject.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Text.Json; + +namespace Neo.Json; + +/// +/// Represents a JSON object. +/// +public class JObject : JContainer +{ + private readonly OrderedDictionary _properties = []; + + /// + /// Gets or sets the properties of the JSON object. + /// + public IDictionary Properties => _properties; + + /// + /// Gets or sets the properties of the JSON object. + /// + /// The name of the property to get or set. + /// The property with the specified name. + public override JToken? this[string name] + { + get + { + if (Properties.TryGetValue(name, out var value)) + return value; + return null; + } + set + { + Properties[name] = value; + } + } + + public override IReadOnlyList Children => _properties.Values; + + /// + /// Constructor + /// + public JObject() { } + + /// + /// Constructor + /// + /// Properties + public JObject(IDictionary properties) + { + foreach (var (key, value) in properties) + { + Properties[key] = value; + } + } + + /// + /// Determines whether the JSON object contains a property with the specified name. + /// + /// The property name to locate in the JSON object. + /// if the JSON object contains a property with the name; otherwise, . + public bool ContainsProperty(string key) + { + return Properties.ContainsKey(key); + } + + public override void Clear() => _properties.Clear(); + + internal override void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + foreach (var (key, value) in Properties) + { + writer.WritePropertyName(key); + if (value is null) + writer.WriteNullValue(); + else + value.Write(writer); + } + writer.WriteEndObject(); + } + + /// + /// Creates a copy of the current JSON object. + /// + /// A copy of the current JSON object. + public override JToken Clone() + { + var cloned = new JObject(); + + foreach (var (key, value) in Properties) + { + cloned[key] = value != null ? value.Clone() : Null; + } + + return cloned; + } +} diff --git a/src/Neo.Json/JPathToken.cs b/src/Neo.Json/JPathToken.cs new file mode 100644 index 0000000000..3606ee19df --- /dev/null +++ b/src/Neo.Json/JPathToken.cs @@ -0,0 +1,340 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JPathToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Json; + +sealed class JPathToken +{ + public JPathTokenType Type { get; private set; } + public string? Content { get; private set; } + + public static IEnumerable Parse(string expr) + { + for (var i = 0; i < expr.Length; i++) + { + JPathToken token = new(); + switch (expr[i]) + { + case '$': + token.Type = JPathTokenType.Root; + break; + case '.': + token.Type = JPathTokenType.Dot; + break; + case '[': + token.Type = JPathTokenType.LeftBracket; + break; + case ']': + token.Type = JPathTokenType.RightBracket; + break; + case '*': + token.Type = JPathTokenType.Asterisk; + break; + case ',': + token.Type = JPathTokenType.Comma; + break; + case ':': + token.Type = JPathTokenType.Colon; + break; + case '\'': + token.Type = JPathTokenType.String; + token.Content = ParseString(expr, i); + i += token.Content.Length - 1; + break; + case '_': + case >= 'a' and <= 'z': + case >= 'A' and <= 'Z': + token.Type = JPathTokenType.Identifier; + token.Content = ParseIdentifier(expr, i); + i += token.Content.Length - 1; + break; + case '-': + case >= '0' and <= '9': + token.Type = JPathTokenType.Number; + token.Content = ParseNumber(expr, i); + i += token.Content.Length - 1; + break; + default: + throw new FormatException($"Invalid character '{expr[i]}' at position {i}"); + } + yield return token; + } + } + + private static string ParseString(string expr, int start) + { + var end = start + 1; + while (end < expr.Length) + { + var c = expr[end]; + end++; + if (c == '\'') return expr[start..end]; + } + throw new FormatException("Unterminated string"); + } + + public static string ParseIdentifier(string expr, int start) + { + var end = start + 1; + while (end < expr.Length) + { + var c = expr[end]; + if (c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9') + end++; + else + break; + } + return expr[start..end]; + } + + private static string ParseNumber(string expr, int start) + { + var end = start + 1; + while (end < expr.Length) + { + var c = expr[end]; + if (c >= '0' && c <= '9') + end++; + else + break; + } + return expr[start..end]; + } + + private static JPathToken DequeueToken(Queue tokens) + { + if (!tokens.TryDequeue(out var token)) + throw new FormatException("Unexpected end of expression"); + return token; + } + + public static void ProcessJsonPath(ref JToken?[] objects, Queue tokens) + { + var maxDepth = 6; + var maxObjects = 1024; + while (tokens.Count > 0) + { + var token = DequeueToken(tokens); + switch (token.Type) + { + case JPathTokenType.Dot: + ProcessDot(ref objects, ref maxDepth, maxObjects, tokens); + break; + case JPathTokenType.LeftBracket: + ProcessBracket(ref objects, ref maxDepth, maxObjects, tokens); + break; + default: + throw new FormatException($"Unexpected token {token.Type}"); + } + } + } + + private static void ProcessDot(ref JToken?[] objects, ref int maxDepth, int maxObjects, Queue tokens) + { + var token = DequeueToken(tokens); + switch (token.Type) + { + case JPathTokenType.Asterisk: + Descent(ref objects, ref maxDepth, maxObjects); + break; + case JPathTokenType.Dot: + ProcessRecursiveDescent(ref objects, ref maxDepth, maxObjects, tokens); + break; + case JPathTokenType.Identifier: + Descent(ref objects, ref maxDepth, maxObjects, token.Content!); + break; + default: + throw new FormatException($"Unexpected token {token.Type}"); + } + } + + private static void ProcessBracket(ref JToken?[] objects, ref int maxDepth, int maxObjects, Queue tokens) + { + var token = DequeueToken(tokens); + switch (token.Type) + { + case JPathTokenType.Asterisk: + var rightBracket = DequeueToken(tokens); + if (rightBracket.Type != JPathTokenType.RightBracket) + throw new FormatException($"Unexpected token {rightBracket.Type}"); + Descent(ref objects, ref maxDepth, maxObjects); + break; + case JPathTokenType.Colon: + ProcessSlice(ref objects, ref maxDepth, maxObjects, tokens, 0); + break; + case JPathTokenType.Number: + var next = DequeueToken(tokens); + switch (next.Type) + { + case JPathTokenType.Colon: + ProcessSlice(ref objects, ref maxDepth, maxObjects, tokens, int.Parse(token.Content!)); + break; + case JPathTokenType.Comma: + ProcessUnion(ref objects, ref maxDepth, maxObjects, tokens, token); + break; + case JPathTokenType.RightBracket: + Descent(ref objects, ref maxDepth, maxObjects, int.Parse(token.Content!)); + break; + default: + throw new FormatException($"Unexpected token {next.Type}"); + } + break; + case JPathTokenType.String: + next = DequeueToken(tokens); + switch (next.Type) + { + case JPathTokenType.Comma: + ProcessUnion(ref objects, ref maxDepth, maxObjects, tokens, token); + break; + case JPathTokenType.RightBracket: + Descent(ref objects, ref maxDepth, maxObjects, JToken.Parse($"\"{token.Content!.Trim('\'')}\"")!.GetString()); + break; + default: + throw new FormatException($"Unexpected token {next.Type}"); + } + break; + default: + throw new FormatException($"Unexpected token {token.Type}"); + } + } + + private static void ProcessRecursiveDescent(ref JToken?[] objects, ref int maxDepth, int maxObjects, Queue tokens) + { + var results = new List(); + var token = DequeueToken(tokens); + if (token.Type != JPathTokenType.Identifier) + throw new FormatException($"Unexpected token {token.Type}"); + + while (objects.Length > 0) + { + results.AddRange(objects.OfType().SelectMany(p => p.Properties).Where(p => p.Key == token.Content).Select(p => p.Value)); + Descent(ref objects, ref maxDepth, maxObjects); + if (results.Count > maxObjects) throw new InvalidOperationException(nameof(maxObjects)); + } + objects = [.. results]; + } + + private static void ProcessSlice(ref JToken?[] objects, ref int maxDepth, int maxObjects, Queue tokens, int start) + { + var token = DequeueToken(tokens); + switch (token.Type) + { + case JPathTokenType.Number: + var next = DequeueToken(tokens); + if (next.Type != JPathTokenType.RightBracket) + throw new FormatException($"Unexpected token {next.Type}"); + DescentRange(ref objects, ref maxDepth, maxObjects, start, int.Parse(token.Content!)); + break; + case JPathTokenType.RightBracket: + DescentRange(ref objects, ref maxDepth, maxObjects, start, 0); + break; + default: + throw new FormatException($"Unexpected token {token.Type}"); + } + } + + private static void ProcessUnion(ref JToken?[] objects, ref int maxDepth, int maxObjects, Queue tokens, JPathToken first) + { + var items = new List([first]); + while (true) + { + var token = DequeueToken(tokens); + if (token.Type != first.Type) + throw new FormatException($"Unexpected token {token.Type} != {first.Type}"); + items.Add(token); + token = DequeueToken(tokens); + if (token.Type == JPathTokenType.RightBracket) + break; + if (token.Type != JPathTokenType.Comma) + throw new FormatException($"Unexpected token {token.Type} != {JPathTokenType.Comma}"); + } + + switch (first.Type) + { + case JPathTokenType.Number: + Descent(ref objects, ref maxDepth, maxObjects, items.Select(p => int.Parse(p.Content!)).ToArray()); + break; + case JPathTokenType.String: + Descent(ref objects, ref maxDepth, maxObjects, items.Select(p => JToken.Parse($"\"{p.Content!.Trim('\'')}\"")!.GetString()).ToArray()); + break; + default: + throw new FormatException($"Unexpected token {first.Type}"); + } + } + + private static void Descent(ref JToken?[] objects, ref int maxDepth, int maxObjects) + { + if (maxDepth <= 0) + throw new InvalidOperationException("Exceeded max depth"); + --maxDepth; + + objects = [.. objects.OfType().SelectMany(p => p.Children)]; + if (objects.Length > maxObjects) + throw new InvalidOperationException(nameof(maxObjects)); + } + + private static void Descent(ref JToken?[] objects, ref int maxDepth, int maxObjects, params string[] names) + { + static IEnumerable GetProperties(JObject obj, string[] names) + { + foreach (var name in names) + if (obj.ContainsProperty(name)) + yield return obj[name]; + } + + if (maxDepth <= 0) + throw new InvalidOperationException("Exceeded max depth"); + --maxDepth; + + objects = [.. objects.OfType().SelectMany(p => GetProperties(p, names))]; + if (objects.Length > maxObjects) + throw new InvalidOperationException(nameof(maxObjects)); + } + + private static void Descent(ref JToken?[] objects, ref int maxDepth, int maxObjects, params int[] indexes) + { + static IEnumerable GetElements(JArray array, int[] indexes) + { + foreach (var index in indexes) + { + var i = index >= 0 ? index : index + array.Count; + if (i >= 0 && i < array.Count) + yield return array[i]; + } + } + + if (maxDepth <= 0) + throw new InvalidOperationException("Exceeded max depth"); + --maxDepth; + + objects = [.. objects.OfType().SelectMany(p => GetElements(p, indexes))]; + if (objects.Length > maxObjects) + throw new InvalidOperationException(nameof(maxObjects)); + } + + private static void DescentRange(ref JToken?[] objects, ref int maxDepth, int maxObjects, int start, int end) + { + if (maxDepth <= 0) + throw new InvalidOperationException("Exceeded max depth"); + --maxDepth; + + objects = [.. objects.OfType().SelectMany(p => + { + var iStart = start >= 0 ? start : start + p.Count; + if (iStart < 0) iStart = 0; + var iEnd = end > 0 ? end : end + p.Count; + var count = iEnd - iStart; + return p.Skip(iStart).Take(count); + })]; + + if (objects.Length > maxObjects) throw new InvalidOperationException(nameof(maxObjects)); + } +} diff --git a/src/Neo.Json/JPathTokenType.cs b/src/Neo.Json/JPathTokenType.cs new file mode 100644 index 0000000000..0f20cb511e --- /dev/null +++ b/src/Neo.Json/JPathTokenType.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JPathTokenType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Json; + +enum JPathTokenType : byte +{ + Root, + Dot, + LeftBracket, + RightBracket, + Asterisk, + Comma, + Colon, + Identifier, + String, + Number +} diff --git a/src/Neo.Json/JString.cs b/src/Neo.Json/JString.cs new file mode 100644 index 0000000000..9b75e1dd4d --- /dev/null +++ b/src/Neo.Json/JString.cs @@ -0,0 +1,130 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JString.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; + +namespace Neo.Json; + +/// +/// Represents a JSON string. +/// +public class JString : JToken +{ + /// + /// Gets the value of the JSON token. + /// + public string Value { get; } + + /// + /// Initializes a new instance of the class with the specified value. + /// + /// The value of the JSON token. + public JString(string value) + { + Value = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Converts the current JSON token to a boolean value. + /// + /// if value is not empty; otherwise, . + public override bool AsBoolean() + { + return !string.IsNullOrEmpty(Value); + } + + public override double AsNumber() + { + if (string.IsNullOrEmpty(Value)) return 0; + return double.TryParse(Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) ? result : double.NaN; + } + + public override string AsString() + { + return Value; + } + + public override string GetString() => Value; + + public override T AsEnum(T defaultValue = default, bool ignoreCase = false) + { + try + { + return Enum.Parse(Value, ignoreCase); + } + catch + { + return defaultValue; + } + } + + public override T GetEnum(bool ignoreCase = false) + { + var result = Enum.Parse(Value, ignoreCase); + if (!Enum.IsDefined(result)) throw new InvalidCastException(); + return result; + } + + internal override void Write(Utf8JsonWriter writer) + { + writer.WriteStringValue(Value); + } + + public override JToken Clone() + { + return this; + } + + public static implicit operator JString(Enum value) + { + return new JString(value.ToString()); + } + + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator JString?(string? value) + { + return value is null ? null : new JString(value); + } + + public static bool operator ==(JString? left, JString? right) + { + if (ReferenceEquals(left, right)) return true; + if (left is null || right is null) return false; + return left.Value.Equals(right.Value); + } + + public static bool operator !=(JString? left, JString? right) + { + return !(left == right); + } + + public override bool Equals(object? obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj is JString other) + { + return this == other; + } + if (obj is string str) + { + return Value == str; + } + return false; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } +} diff --git a/src/Neo.Json/JToken.cs b/src/Neo.Json/JToken.cs new file mode 100644 index 0000000000..37abcfe984 --- /dev/null +++ b/src/Neo.Json/JToken.cs @@ -0,0 +1,312 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using static Neo.Json.Utility; + +namespace Neo.Json; + +/// +/// Represents an abstract JSON token. +/// +public abstract class JToken +{ + /// + /// Represents a token. + /// + public const JToken? Null = null; + + /// + /// Gets or sets the child token at the specified index. + /// + /// The zero-based index of the child token to get or set. + /// The child token at the specified index. + public virtual JToken? this[int index] + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + /// Gets or sets the properties of the JSON object. + /// + /// The key of the property to get or set. + /// The property with the specified name. + public virtual JToken? this[string key] + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + /// Converts the current JSON token to a boolean value. + /// + /// The converted value. + public virtual bool AsBoolean() + { + return true; + } + + /// + /// Converts the current JSON token to an . + /// + /// The type of the . + /// If the current JSON token cannot be converted to type , then the default value is returned. + /// Indicates whether case should be ignored during conversion. + /// The converted value. + public virtual T AsEnum(T defaultValue = default, bool ignoreCase = false) where T : unmanaged, Enum + { + return defaultValue; + } + + /// + /// Converts the current JSON token to a floating point number. + /// + /// The converted value. + public virtual double AsNumber() + { + return double.NaN; + } + + /// + /// Converts the current JSON token to a . + /// + /// The converted value. + public virtual string AsString() + { + return ToString(); + } + + /// + /// Converts the current JSON token to a boolean value. + /// + /// The converted value. + /// The JSON token is not a . + public virtual bool GetBoolean() => throw new InvalidCastException(); + + public virtual T GetEnum(bool ignoreCase = false) where T : unmanaged, Enum => throw new InvalidCastException(); + + /// + /// Converts the current JSON token to a 32-bit signed integer. + /// + /// The converted value. + /// The JSON token is not a . + /// The JSON token cannot be converted to an integer. + /// The JSON token cannot be converted to a 32-bit signed integer. + public int GetInt32() + { + var d = GetNumber(); + if (d % 1 != 0) throw new InvalidCastException(); + return checked((int)d); + } + + /// + /// Converts the current JSON token to a floating point number. + /// + /// The converted value. + /// The JSON token is not a . + public virtual double GetNumber() => throw new InvalidCastException(); + + /// + /// Converts the current JSON token to a . + /// + /// The converted value. + /// The JSON token is not a . + public virtual string GetString() => throw new InvalidCastException(); + + /// + /// Parses a JSON token from a byte array. + /// + /// The byte array that contains the JSON token. + /// The maximum nesting depth when parsing the JSON token. + /// The parsed JSON token. + public static JToken? Parse(ReadOnlySpan value, int max_nest = 64) + { + var reader = new Utf8JsonReader(value, new JsonReaderOptions + { + AllowTrailingCommas = false, + CommentHandling = JsonCommentHandling.Skip, + MaxDepth = max_nest + }); + try + { + var json = Read(ref reader); + if (reader.Read()) throw new FormatException("Read json token failed"); + return json; + } + catch (JsonException ex) + { + throw new FormatException(ex.Message, ex); + } + } + + /// + /// Parses a JSON token from a . + /// + /// The that contains the JSON token. + /// The maximum nesting depth when parsing the JSON token. + /// The parsed JSON token. + public static JToken? Parse(string value, int max_nest = 64) + { + return Parse(StrictUTF8.GetBytes(value), max_nest); + } + + private static JToken? Read(ref Utf8JsonReader reader, bool skipReading = false) + { + if (!skipReading && !reader.Read()) throw new FormatException("Read json token failed"); + return reader.TokenType switch + { + JsonTokenType.False => false, + JsonTokenType.Null => Null, + JsonTokenType.Number => reader.GetDouble(), + JsonTokenType.StartArray => ReadArray(ref reader), + JsonTokenType.StartObject => ReadObject(ref reader), + JsonTokenType.String => ReadString(ref reader), + JsonTokenType.True => true, + _ => throw new FormatException($"Unexpected token {reader.TokenType}"), + }; + } + + private static JArray ReadArray(ref Utf8JsonReader reader) + { + var array = new JArray(); + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.EndArray: + return array; + default: + array.Add(Read(ref reader, skipReading: true)); + break; + } + } + throw new FormatException("Unterminated array"); + } + + private static JObject ReadObject(ref Utf8JsonReader reader) + { + JObject obj = new(); + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.EndObject: + return obj; + case JsonTokenType.PropertyName: + var name = ReadString(ref reader); + if (obj.Properties.ContainsKey(name)) + throw new FormatException($"Duplicate property name: {name}"); + + var value = Read(ref reader); + obj.Properties.Add(name, value); + break; + default: + throw new FormatException($"Unexpected token {reader.TokenType}"); + } + } + throw new FormatException("Unterminated object"); + } + + private static string ReadString(ref Utf8JsonReader reader) + { + try + { + return reader.GetString()!; + } + catch (InvalidOperationException ex) + { + throw new FormatException(ex.Message, ex); + } + } + + /// + /// Encode the current JSON token into a byte array. + /// + /// Indicates whether indentation is required. + /// The encoded JSON token. + public byte[] ToByteArray(bool indented) + { + using MemoryStream ms = new(); + using Utf8JsonWriter writer = new(ms, new JsonWriterOptions + { + Indented = indented, + SkipValidation = true + }); + Write(writer); + writer.Flush(); + return ms.ToArray(); + } + + /// + /// Encode the current JSON token into a . + /// + /// The encoded JSON token. + public override string ToString() + { + return ToString(false); + } + + /// + /// Encode the current JSON token into a . + /// + /// Indicates whether indentation is required. + /// The encoded JSON token. + public string ToString(bool indented) + { + return StrictUTF8.GetString(ToByteArray(indented)); + } + + internal abstract void Write(Utf8JsonWriter writer); + + public abstract JToken Clone(); + + public JArray JsonPath(string expr) + { + JToken?[] objects = [this]; + if (expr.Length == 0) return objects; + + Queue tokens = new(JPathToken.Parse(expr)); + var first = tokens.Dequeue(); + if (first.Type != JPathTokenType.Root) + throw new FormatException($"Unexpected token {first.Type}"); + + JPathToken.ProcessJsonPath(ref objects, tokens); + return objects; + } + + public static implicit operator JToken(Enum value) + { + return (JString)value; + } + + public static implicit operator JToken(JToken?[] value) + { + return (JArray)value; + } + + public static implicit operator JToken(bool value) + { + return (JBoolean)value; + } + + public static implicit operator JToken(double value) + { + return (JNumber)value; + } + + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator JToken?(string? value) + { + return (JString?)value; + } +} diff --git a/src/Neo.Json/Neo.Json.csproj b/src/Neo.Json/Neo.Json.csproj new file mode 100644 index 0000000000..fa4ebbd909 --- /dev/null +++ b/src/Neo.Json/Neo.Json.csproj @@ -0,0 +1,11 @@ + + + + NEO;JSON + + + + + + + diff --git a/src/Neo.Json/Utility.cs b/src/Neo.Json/Utility.cs new file mode 100644 index 0000000000..9b123157bc --- /dev/null +++ b/src/Neo.Json/Utility.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015-2025 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 System.Text; + +namespace Neo.Json; + +static class Utility +{ + public static Encoding StrictUTF8 { get; } + + static Utility() + { + StrictUTF8 = (Encoding)Encoding.UTF8.Clone(); + StrictUTF8.DecoderFallback = DecoderFallback.ExceptionFallback; + StrictUTF8.EncoderFallback = EncoderFallback.ExceptionFallback; + } +} diff --git a/src/Neo/BigDecimal.cs b/src/Neo/BigDecimal.cs new file mode 100644 index 0000000000..bd778b3a90 --- /dev/null +++ b/src/Neo/BigDecimal.cs @@ -0,0 +1,239 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BigDecimal.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; +using System.Runtime.InteropServices; + +namespace Neo; + +/// +/// Represents a fixed-point number of arbitrary precision. +/// +public readonly struct BigDecimal : IComparable, IEquatable +{ + private readonly BigInteger _value; + private readonly byte _decimals; + + /// + /// The value of the number. + /// + public readonly BigInteger Value => _value; + + /// + /// The number of decimal places for this number. + /// + public readonly byte Decimals => _decimals; + + /// + /// The sign of the number. + /// + public readonly int Sign => _value.Sign; + + /// + /// Initializes a new instance of the struct. + /// + /// The value of the number. + /// The number of decimal places for this number. + public BigDecimal(BigInteger value, byte decimals) + { + _value = value; + _decimals = decimals; + } + + /// + /// Initializes a new instance of the struct with the value of . + /// + /// The value of the number. + public BigDecimal(decimal value) + { + Span span = stackalloc int[4]; + decimal.GetBits(value, span); + var buffer = MemoryMarshal.AsBytes((ReadOnlySpan)span); + _value = new BigInteger(buffer[..12], isUnsigned: true); + + if (buffer[15] != 0) _value = -_value; + _decimals = buffer[14]; + } + + /// + /// Initializes a new instance of the struct with the value of . + /// + /// The value of the number. + /// The number of decimal places for this number. + public BigDecimal(decimal value, byte decimals) + { + Span span = stackalloc int[4]; + decimal.GetBits(value, span); + var buffer = MemoryMarshal.AsBytes((ReadOnlySpan)span); + _value = new BigInteger(buffer[..12], isUnsigned: true); + if (buffer[14] > decimals) + throw new ArgumentException($"Decimal value has {buffer[14]} decimal places, which exceeds the maximum allowed precision of {decimals}.", nameof(value)); + else if (buffer[14] < decimals) + _value *= BigInteger.Pow(10, decimals - buffer[14]); + + if (buffer[15] != 0) _value = -_value; + _decimals = decimals; + } + + /// + /// Changes the decimals of the . + /// + /// The new decimals field. + /// The that has the new number of decimal places. + public readonly BigDecimal ChangeDecimals(byte decimals) + { + if (_decimals == decimals) return this; + BigInteger value; + if (_decimals < decimals) + { + value = _value * BigInteger.Pow(10, decimals - _decimals); + } + else + { + var divisor = BigInteger.Pow(10, _decimals - decimals); + value = BigInteger.DivRem(_value, divisor, out var remainder); + if (remainder > BigInteger.Zero) + throw new ArgumentOutOfRangeException(nameof(decimals)); + } + return new BigDecimal(value, decimals); + } + + /// + /// Parses a from the specified . + /// + /// A number represented by a . + /// The number of decimal places for this number. + /// The parsed . + /// is not in the correct format. + public static BigDecimal Parse(string s, byte decimals) + { + if (!TryParse(s, decimals, out var result)) + throw new FormatException($"Failed to parse BigDecimal from string '{s}' with {decimals} decimal places. Please ensure the string represents a valid number in the correct format."); + return result; + } + + /// + /// Gets a representing the number. + /// + /// The representing the number. + public override readonly string ToString() + { + var divisor = BigInteger.Pow(10, _decimals); + var result = BigInteger.DivRem(_value, divisor, out var remainder); + if (remainder == 0) return result.ToString(); + return $"{result}.{remainder.ToString("d" + _decimals)}".TrimEnd('0'); + } + + /// + /// Parses a from the specified . + /// + /// A number represented by a . + /// The number of decimal places for this number. + /// The parsed . + /// if a number is successfully parsed; otherwise, . + public static bool TryParse(string s, byte decimals, out BigDecimal result) + { + var e = 0; + var index = s.IndexOfAny(['e', 'E']); + if (index >= 0) + { + if (!sbyte.TryParse(s[(index + 1)..], out var eTemp)) + { + result = default; + return false; + } + e = eTemp; + s = s[..index]; + } + index = s.IndexOf('.'); + if (index >= 0) + { + s = s.TrimEnd('0'); + e -= s.Length - index - 1; + s = s.Remove(index, 1); + } + var ds = e + decimals; + if (ds < 0) + { + result = default; + return false; + } + if (ds > 0) + s += new string('0', ds); + if (!BigInteger.TryParse(s, out var value)) + { + result = default; + return false; + } + result = new BigDecimal(value, decimals); + return true; + } + + public readonly int CompareTo(BigDecimal other) + { + BigInteger left = _value, right = other._value; + if (_decimals < other._decimals) + left *= BigInteger.Pow(10, other._decimals - _decimals); + else if (_decimals > other._decimals) + right *= BigInteger.Pow(10, _decimals - other._decimals); + return left.CompareTo(right); + } + + public override readonly bool Equals(object? obj) + { + if (obj is not BigDecimal @decimal) return false; + return Equals(@decimal); + } + + public readonly bool Equals(BigDecimal other) + { + return CompareTo(other) == 0; + } + + /// + /// Get the hash code of the decimal value. Semantic equivalence is not guaranteed. + /// + /// hash code + public override readonly int GetHashCode() + { + return HashCode.Combine(_decimals, _value.GetHashCode()); + } + + public static bool operator ==(BigDecimal left, BigDecimal right) + { + return left.CompareTo(right) == 0; + } + + public static bool operator !=(BigDecimal left, BigDecimal right) + { + return left.CompareTo(right) != 0; + } + + public static bool operator <(BigDecimal left, BigDecimal right) + { + return left.CompareTo(right) < 0; + } + + public static bool operator <=(BigDecimal left, BigDecimal right) + { + return left.CompareTo(right) <= 0; + } + + public static bool operator >(BigDecimal left, BigDecimal right) + { + return left.CompareTo(right) > 0; + } + + public static bool operator >=(BigDecimal left, BigDecimal right) + { + return left.CompareTo(right) >= 0; + } +} diff --git a/src/Neo/Builders/AndConditionBuilder.cs b/src/Neo/Builders/AndConditionBuilder.cs new file mode 100644 index 0000000000..9bc5b36a19 --- /dev/null +++ b/src/Neo/Builders/AndConditionBuilder.cs @@ -0,0 +1,88 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// AndConditionBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Conditions; + +namespace Neo.Builders; + +public sealed class AndConditionBuilder +{ + private readonly AndCondition _condition = new() { Expressions = [] }; + + private AndConditionBuilder() { } + + public static AndConditionBuilder CreateEmpty() + { + return new AndConditionBuilder(); + } + + public AndConditionBuilder And(Action config) + { + var acb = new AndConditionBuilder(); + config(acb); + + _condition.Expressions = [.. _condition.Expressions, acb.Build()]; + + return this; + } + + public AndConditionBuilder Or(Action config) + { + var ocb = OrConditionBuilder.CreateEmpty(); + config(ocb); + + _condition.Expressions = [.. _condition.Expressions, ocb.Build()]; + + return this; + } + + public AndConditionBuilder Boolean(bool expression) + { + _condition.Expressions = [.. _condition.Expressions, new BooleanCondition { Expression = expression }]; + return this; + } + + public AndConditionBuilder CalledByContract(UInt160 hash) + { + _condition.Expressions = [.. _condition.Expressions, new CalledByContractCondition { Hash = hash }]; + return this; + } + + public AndConditionBuilder CalledByEntry() + { + _condition.Expressions = [.. _condition.Expressions, new CalledByEntryCondition()]; + return this; + } + + public AndConditionBuilder CalledByGroup(ECPoint publicKey) + { + _condition.Expressions = [.. _condition.Expressions, new CalledByGroupCondition { Group = publicKey }]; + return this; + } + + public AndConditionBuilder Group(ECPoint publicKey) + { + _condition.Expressions = [.. _condition.Expressions, new GroupCondition() { Group = publicKey }]; + return this; + } + + public AndConditionBuilder ScriptHash(UInt160 scriptHash) + { + _condition.Expressions = [.. _condition.Expressions, new ScriptHashCondition() { Hash = scriptHash }]; + return this; + } + + public AndCondition Build() + { + return _condition; + } +} diff --git a/src/Neo/Builders/OrConditionBuilder.cs b/src/Neo/Builders/OrConditionBuilder.cs new file mode 100644 index 0000000000..7b59ba28af --- /dev/null +++ b/src/Neo/Builders/OrConditionBuilder.cs @@ -0,0 +1,88 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// OrConditionBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Conditions; + +namespace Neo.Builders; + +public sealed class OrConditionBuilder +{ + private readonly OrCondition _condition = new() { Expressions = [] }; + + private OrConditionBuilder() { } + + public static OrConditionBuilder CreateEmpty() + { + return new OrConditionBuilder(); + } + + public OrConditionBuilder And(Action config) + { + var acb = AndConditionBuilder.CreateEmpty(); + config(acb); + + _condition.Expressions = [.. _condition.Expressions, acb.Build()]; + + return this; + } + + public OrConditionBuilder Or(Action config) + { + var acb = new OrConditionBuilder(); + config(acb); + + _condition.Expressions = [.. _condition.Expressions, acb.Build()]; + + return this; + } + + public OrConditionBuilder Boolean(bool expression) + { + _condition.Expressions = [.. _condition.Expressions, new BooleanCondition { Expression = expression }]; + return this; + } + + public OrConditionBuilder CalledByContract(UInt160 hash) + { + _condition.Expressions = [.. _condition.Expressions, new CalledByContractCondition { Hash = hash }]; + return this; + } + + public OrConditionBuilder CalledByEntry() + { + _condition.Expressions = [.. _condition.Expressions, new CalledByEntryCondition()]; + return this; + } + + public OrConditionBuilder CalledByGroup(ECPoint publicKey) + { + _condition.Expressions = [.. _condition.Expressions, new CalledByGroupCondition { Group = publicKey }]; + return this; + } + + public OrConditionBuilder Group(ECPoint publicKey) + { + _condition.Expressions = [.. _condition.Expressions, new GroupCondition() { Group = publicKey }]; + return this; + } + + public OrConditionBuilder ScriptHash(UInt160 scriptHash) + { + _condition.Expressions = [.. _condition.Expressions, new ScriptHashCondition() { Hash = scriptHash }]; + return this; + } + + public OrCondition Build() + { + return _condition; + } +} diff --git a/src/Neo/Builders/SignerBuilder.cs b/src/Neo/Builders/SignerBuilder.cs new file mode 100644 index 0000000000..d16cc712e7 --- /dev/null +++ b/src/Neo/Builders/SignerBuilder.cs @@ -0,0 +1,72 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// SignerBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.Builders; + +public sealed class SignerBuilder +{ + private readonly UInt160 _account; + private WitnessScope _scopes = WitnessScope.None; + private readonly List _allowedContracts = []; + private readonly List _allowedGroups = []; + private readonly List _rules = []; + + private SignerBuilder(UInt160 account) + { + _account = account; + } + + public static SignerBuilder Create(UInt160 account) + { + return new SignerBuilder(account); + } + + public SignerBuilder AllowContract(UInt160 contractHash) + { + _allowedContracts.Add(contractHash); + return AddWitnessScope(WitnessScope.CustomContracts); + } + + public SignerBuilder AllowGroup(ECPoint publicKey) + { + _allowedGroups.Add(publicKey); + return AddWitnessScope(WitnessScope.CustomGroups); + } + + public SignerBuilder AddWitnessScope(WitnessScope scope) + { + _scopes |= scope; + return this; + } + + public SignerBuilder AddWitnessRule(WitnessRuleAction action, Action config) + { + var rb = WitnessRuleBuilder.Create(action); + config(rb); + _rules.Add(rb.Build()); + return AddWitnessScope(WitnessScope.WitnessRules); + } + + public Signer Build() + { + return new() + { + Account = _account, + Scopes = _scopes, + AllowedContracts = _scopes.HasFlag(WitnessScope.CustomContracts) ? _allowedContracts.ToArray() : null, + AllowedGroups = _scopes.HasFlag(WitnessScope.CustomGroups) ? _allowedGroups.ToArray() : null, + Rules = _scopes.HasFlag(WitnessScope.WitnessRules) ? _rules.ToArray() : null + }; + } +} diff --git a/src/Neo/Builders/TransactionAttributesBuilder.cs b/src/Neo/Builders/TransactionAttributesBuilder.cs new file mode 100644 index 0000000000..a7418d8827 --- /dev/null +++ b/src/Neo/Builders/TransactionAttributesBuilder.cs @@ -0,0 +1,70 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionAttributesBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P.Payloads; + +namespace Neo.Builders; + +public sealed class TransactionAttributesBuilder +{ + private TransactionAttribute[] _attributes = []; + + private TransactionAttributesBuilder() { } + + public static TransactionAttributesBuilder CreateEmpty() + { + return new TransactionAttributesBuilder(); + } + + public TransactionAttributesBuilder AddConflict(UInt256 hash) + { + var conflicts = new Conflicts { Hash = hash }; + _attributes = [.. _attributes, conflicts]; + return this; + } + + public TransactionAttributesBuilder AddOracleResponse(Action config) + { + var oracleResponse = new OracleResponse(); + config(oracleResponse); + _attributes = [.. _attributes, oracleResponse]; + return this; + } + + public TransactionAttributesBuilder AddHighPriority() + { + if (_attributes.Any(a => a is HighPriorityAttribute)) + throw new InvalidOperationException("HighPriority attribute already exists in the transaction attributes. Only one HighPriority attribute is allowed per transaction."); + + var highPriority = new HighPriorityAttribute(); + _attributes = [.. _attributes, highPriority]; + return this; + } + + public TransactionAttributesBuilder AddNotValidBefore(uint block) + { + if (_attributes.Any(a => a is NotValidBefore b && b.Height == block)) + throw new InvalidOperationException($"NotValidBefore attribute for block {block} already exists in the transaction attributes. Each block height can only be specified once."); + + var validUntilBlock = new NotValidBefore() + { + Height = block + }; + + _attributes = [.. _attributes, validUntilBlock]; + return this; + } + + public TransactionAttribute[] Build() + { + return _attributes; + } +} diff --git a/src/Neo/Builders/TransactionBuilder.cs b/src/Neo/Builders/TransactionBuilder.cs new file mode 100644 index 0000000000..2c7f8cb99d --- /dev/null +++ b/src/Neo/Builders/TransactionBuilder.cs @@ -0,0 +1,114 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.VM; + +namespace Neo.Builders; + +public sealed class TransactionBuilder +{ + private readonly Transaction _tx = new() + { + Script = new[] { (byte)OpCode.RET }, + Attributes = [], + Signers = [], + Witnesses = [], + }; + + private TransactionBuilder() { } + + public static TransactionBuilder CreateEmpty() + { + return new TransactionBuilder(); + } + + public TransactionBuilder Version(byte version) + { + _tx.Version = version; + return this; + } + + public TransactionBuilder Nonce(uint nonce) + { + _tx.Nonce = nonce; + return this; + } + + public TransactionBuilder SystemFee(uint systemFee) + { + _tx.SystemFee = systemFee; + return this; + } + + public TransactionBuilder NetworkFee(uint networkFee) + { + _tx.NetworkFee = networkFee; + return this; + } + + public TransactionBuilder ValidUntil(uint blockIndex) + { + _tx.ValidUntilBlock = blockIndex; + return this; + } + + public TransactionBuilder AttachSystem(Action config) + { + using var sb = new ScriptBuilder(); + config(sb); + _tx.Script = sb.ToArray(); + return this; + } + + public TransactionBuilder AttachSystem(byte[] script) + { + _tx.Script = script; + return this; + } + + public TransactionBuilder AddAttributes(Action config) + { + var ab = TransactionAttributesBuilder.CreateEmpty(); + config(ab); + _tx.Attributes = ab.Build(); + return this; + } + + public TransactionBuilder AddWitness(Action config) + { + var wb = WitnessBuilder.CreateEmpty(); + config(wb); + _tx.Witnesses = [.. _tx.Witnesses, wb.Build()]; + return this; + } + + public TransactionBuilder AddWitness(Action config) + { + var wb = WitnessBuilder.CreateEmpty(); + config(wb, _tx); + _tx.Witnesses = [.. _tx.Witnesses, wb.Build()]; + return this; + } + + public TransactionBuilder AddSigner(UInt160 account, Action config) + { + var wb = SignerBuilder.Create(account); + config(wb, _tx); + _tx.Signers = [.. _tx.Signers, wb.Build()]; + return this; + } + + public Transaction Build() + { + return _tx; + } +} diff --git a/src/Neo/Builders/WitnessBuilder.cs b/src/Neo/Builders/WitnessBuilder.cs new file mode 100644 index 0000000000..12fa1c55ac --- /dev/null +++ b/src/Neo/Builders/WitnessBuilder.cs @@ -0,0 +1,77 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WitnessBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.VM; + +namespace Neo.Builders; + +public sealed class WitnessBuilder +{ + private byte[] _invocationScript = []; + private byte[] _verificationScript = []; + + private WitnessBuilder() { } + + public static WitnessBuilder CreateEmpty() + { + return new WitnessBuilder(); + } + + public WitnessBuilder AddInvocation(Action config) + { + if (_invocationScript.Length > 0) + throw new InvalidOperationException("Invocation script already exists in the witness builder. Only one invocation script can be added per witness."); + + using var sb = new ScriptBuilder(); + config(sb); + _invocationScript = sb.ToArray(); + return this; + } + + public WitnessBuilder AddInvocation(byte[] bytes) + { + if (_invocationScript.Length > 0) + throw new InvalidOperationException("Invocation script already exists in the witness builder. Only one invocation script can be added per witness."); + + _invocationScript = bytes; + return this; + } + + public WitnessBuilder AddVerification(Action config) + { + if (_verificationScript.Length > 0) + throw new InvalidOperationException("Verification script already exists in the witness builder. Only one verification script can be added per witness."); + + using var sb = new ScriptBuilder(); + config(sb); + _verificationScript = sb.ToArray(); + return this; + } + + public WitnessBuilder AddVerification(byte[] bytes) + { + if (_verificationScript.Length > 0) + throw new InvalidOperationException("Verification script already exists in the witness builder. Only one verification script can be added per witness."); + + _verificationScript = bytes; + return this; + } + + public Witness Build() + { + return new Witness() + { + InvocationScript = _invocationScript, + VerificationScript = _verificationScript, + }; + } +} diff --git a/src/Neo/Builders/WitnessConditionBuilder.cs b/src/Neo/Builders/WitnessConditionBuilder.cs new file mode 100644 index 0000000000..7ae44872e0 --- /dev/null +++ b/src/Neo/Builders/WitnessConditionBuilder.cs @@ -0,0 +1,124 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WitnessConditionBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Conditions; + +namespace Neo.Builders; + +public sealed class WitnessConditionBuilder +{ + WitnessCondition? _condition; + + private WitnessConditionBuilder() { } + + public static WitnessConditionBuilder Create() + { + return new WitnessConditionBuilder(); + } + + public WitnessConditionBuilder And(Action config) + { + var acb = AndConditionBuilder.CreateEmpty(); + config(acb); + + _condition = acb.Build(); + + return this; + } + + public WitnessConditionBuilder Boolean(bool expression) + { + var condition = new BooleanCondition() { Expression = expression }; + + _condition = condition; + + return this; + } + + public WitnessConditionBuilder CalledByContract(UInt160 hash) + { + var condition = new CalledByContractCondition() { Hash = hash }; + + _condition = condition; + + return this; + } + + public WitnessConditionBuilder CalledByEntry() + { + var condition = new CalledByEntryCondition(); + + _condition = condition; + + return this; + } + + public WitnessConditionBuilder CalledByGroup(ECPoint publicKey) + { + var condition = new CalledByGroupCondition() { Group = publicKey }; + + _condition = condition; + + return this; + } + + public WitnessConditionBuilder Group(ECPoint publicKey) + { + var condition = new GroupCondition() { Group = publicKey }; + + _condition = condition; + + return this; + } + + public WitnessConditionBuilder Not(Action config) + { + var wcb = new WitnessConditionBuilder(); + config(wcb); + + var condition = new NotCondition() + { + Expression = wcb.Build() + }; + + _condition = condition; + + return this; + } + + public WitnessConditionBuilder Or(Action config) + { + var ocb = OrConditionBuilder.CreateEmpty(); + config(ocb); + + _condition = ocb.Build(); + + return this; + } + + public WitnessConditionBuilder ScriptHash(UInt160 scriptHash) + { + var condition = new ScriptHashCondition() { Hash = scriptHash }; + + _condition = condition; + + return this; + } + + public WitnessCondition Build() + { + if (_condition is null) + return new BooleanCondition() { Expression = true }; + + return _condition; + } +} diff --git a/src/Neo/Builders/WitnessRuleBuilder.cs b/src/Neo/Builders/WitnessRuleBuilder.cs new file mode 100644 index 0000000000..417005ba54 --- /dev/null +++ b/src/Neo/Builders/WitnessRuleBuilder.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WitnessRuleBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.P2P.Payloads.Conditions; + +namespace Neo.Builders; + +public sealed class WitnessRuleBuilder +{ + private readonly WitnessRuleAction _action; + private WitnessCondition? _condition; + + private WitnessRuleBuilder(WitnessRuleAction action) + { + _action = action; + } + + public static WitnessRuleBuilder Create(WitnessRuleAction action) + { + return new WitnessRuleBuilder(action); + } + + public WitnessRuleBuilder AddCondition(Action config) + { + var cb = WitnessConditionBuilder.Create(); + config(cb); + _condition = cb.Build(); + return this; + } + + public WitnessRule Build() + { + return new() + { + Action = _action, + Condition = _condition ?? throw new InvalidOperationException("Condition is not set."), + }; + } +} diff --git a/src/Neo/ContainsTransactionType.cs b/src/Neo/ContainsTransactionType.cs new file mode 100644 index 0000000000..2c413077bc --- /dev/null +++ b/src/Neo/ContainsTransactionType.cs @@ -0,0 +1,19 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContainsTransactionType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +public enum ContainsTransactionType +{ + NotExist, + ExistsInPool, + ExistsInLedger +} diff --git a/src/Neo/Cryptography/Base58.cs b/src/Neo/Cryptography/Base58.cs new file mode 100644 index 0000000000..036649669e --- /dev/null +++ b/src/Neo/Cryptography/Base58.cs @@ -0,0 +1,156 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Base58.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Neo.Cryptography; + +/// +/// A helper class for base-58 encoder. +/// +public static class Base58 +{ + /// + /// Represents the alphabet of the base-58 encoder. + /// + public const string Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + private const char ZeroChar = '1'; + private static readonly BigInteger s_alphabetLength = Alphabet.Length; + + #pragma warning disable format + private static readonly sbyte[] s_decodeMap = + [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, + 7, 8, -1, -1, -1, -1, -1, -1, -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, + 18, 19, 20, 21, -1, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, + -1, -1, -1, -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, + 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57 + ]; + #pragma warning restore format + + /// + /// Converts the specified , which encodes binary data as base-58 digits, to an equivalent byte array. + /// The encoded contains the checksum of the binary data. + /// + /// The to convert. + /// A byte array that is equivalent to . + public static byte[] Base58CheckDecode(this string input) + { + ArgumentNullException.ThrowIfNull(input); + byte[] buffer = Decode(input); + if (buffer.Length < 4) throw new FormatException($"Invalid Base58Check format: decoded data length ({buffer.Length} bytes) is too short. Base58Check requires at least 4 bytes for the checksum."); + byte[] checksum = buffer.Sha256(0, buffer.Length - 4).Sha256(); + if (!buffer.AsSpan(^4).SequenceEqual(checksum.AsSpan(..4))) + throw new FormatException($"Invalid Base58Check checksum: the provided checksum does not match the calculated checksum. The data may be corrupted or the Base58Check string is invalid."); + var ret = buffer[..^4]; + Array.Clear(buffer, 0, buffer.Length); + return ret; + } + + /// + /// Converts a byte array to its equivalent + /// representation that is encoded with base-58 digits. + /// The encoded contains the checksum of the binary data. + /// + /// The byte array to convert. + /// The representation, in base-58, of the contents of . + public static string Base58CheckEncode(this ReadOnlySpan data) + { + byte[] checksum = data.Sha256().Sha256(); + Span buffer = stackalloc byte[data.Length + 4]; + data.CopyTo(buffer); + checksum.AsSpan(..4).CopyTo(buffer[data.Length..]); + var ret = Encode(buffer); + buffer.Clear(); + return ret; + } + + /// + /// Converts the specified , which encodes binary data as base-58 digits, to an equivalent byte array. + /// + /// The to convert. + /// A byte array that is equivalent to . + public static byte[] Decode(string input) + { + // Decode Base58 string to BigInteger + var bi = BigInteger.Zero; + sbyte digit; + for (var i = 0; i < input.Length; i++) + { + if (input[i] >= 123) + throw new FormatException($"Invalid Base58 character '{input[i]}' at position {i}"); + digit = s_decodeMap[input[i]]; + if (digit == -1) + throw new FormatException($"Invalid Base58 character '{input[i]}' at position {i}"); + bi = bi * s_alphabetLength + digit; + } + + // Encode BigInteger to byte[] + // Leading zero bytes get encoded as leading `1` characters + + var leadingZeroCount = LeadingBase58Zeros(input); + if (bi.IsZero) + { + return new byte[leadingZeroCount]; + } + + var result = new byte[leadingZeroCount + bi.GetByteCount(true)]; + + _ = bi.TryWriteBytes(result.AsSpan(leadingZeroCount), out _, true, true); + return result; + } + + /// + /// Converts a byte array to its equivalent representation that is encoded with base-58 digits. + /// + /// The byte array to convert. + /// The representation, in base-58, of the contents of . + public static string Encode(ReadOnlySpan input) + { + // Decode byte[] to BigInteger + BigInteger value = new(input, isUnsigned: true, isBigEndian: true); + + // Encode BigInteger to Base58 string + var sb = new StringBuilder(input.Length * 138 / 100 + 5); + + while (value > 0) + { + value = BigInteger.DivRem(value, s_alphabetLength, out var remainder); + sb.Append(Alphabet[(int)remainder]); + } + + // Append `1` for each leading 0 byte + for (int i = 0; i < input.Length && input[i] == 0; i++) + { + sb.Append(ZeroChar); + } + + Span copy = stackalloc char[sb.Length]; + sb.CopyTo(0, copy, sb.Length); + copy.Reverse(); + + return copy.ToString(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int LeadingBase58Zeros(string collection) + { + var i = 0; + var len = collection.Length; + for (; i < len && collection[i] == ZeroChar; i++) { } + + return i; + } +} diff --git a/src/Neo/Cryptography/BloomFilter.cs b/src/Neo/Cryptography/BloomFilter.cs new file mode 100644 index 0000000000..e778704a23 --- /dev/null +++ b/src/Neo/Cryptography/BloomFilter.cs @@ -0,0 +1,111 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BloomFilter.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Collections; + +namespace Neo.Cryptography; + +/// +/// Represents a bloom filter. +/// +public class BloomFilter +{ + private readonly uint[] _seeds; + private readonly BitArray _bits; + + /// + /// The number of hash functions used by the bloom filter. + /// + public int K => _seeds.Length; + + /// + /// The size of the bit array used by the bloom filter. + /// + public int M => _bits.Length; + + /// + /// Used to generate the seeds of the murmur hash functions. + /// + public uint Tweak { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The size of the bit array used by the bloom filter, and must be greater than 0. + /// The number of hash functions used by the bloom filter, and must be greater than 0. + /// Used to generate the seeds of the murmur hash functions. + /// Thrown when or is less than or equal to 0. + public BloomFilter(int m, int k, uint nTweak) + { + if (k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "The number of hash functions (k) must be greater than 0."); + if (m <= 0) throw new ArgumentOutOfRangeException(nameof(m), "The size of the bit array (m) must be greater than 0."); + + _seeds = Enumerable.Range(0, k).Select(p => (uint)p * 0xFBA4C795 + nTweak).ToArray(); + _bits = new BitArray(m) + { + Length = m + }; + Tweak = nTweak; + } + + /// + /// Initializes a new instance of the class. + /// + /// The size of the bit array used by the bloom filter, and must be greater than 0. + /// The number of hash functions used by the bloom filter, and must be greater than 0. + /// Used to generate the seeds of the murmur hash functions. + /// The initial elements contained in this object. + /// Thrown when or is less than or equal to 0. + public BloomFilter(int m, int k, uint nTweak, ReadOnlyMemory elements) + { + if (k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "The number of hash functions (k) must be greater than 0."); + if (m <= 0) throw new ArgumentOutOfRangeException(nameof(m), "The size of the bit array (m) must be greater than 0."); + + _seeds = Enumerable.Range(0, k).Select(p => (uint)p * 0xFBA4C795 + nTweak).ToArray(); + _bits = new BitArray(elements.ToArray()) + { + Length = m + }; + Tweak = nTweak; + } + + /// + /// Adds an element to the . + /// + /// The object to add to the . + public void Add(ReadOnlyMemory element) + { + foreach (var i in _seeds.AsParallel().Select(s => element.Span.Murmur32(s))) + _bits.Set((int)(i % (uint)_bits.Length), true); + } + + /// + /// Determines whether the contains a specific element. + /// + /// The object to locate in the . + /// if is found in the ; otherwise, . + public bool Check(byte[] element) + { + foreach (var i in _seeds.AsParallel().Select(element.Murmur32)) + if (!_bits.Get((int)(i % (uint)_bits.Length))) + return false; + return true; + } + + /// + /// Gets the bit array in this . + /// + /// The byte array to store the bits. + public void GetBits(byte[] newBits) + { + _bits.CopyTo(newBits, 0); + } +} diff --git a/src/Neo/Cryptography/Crypto.cs b/src/Neo/Cryptography/Crypto.cs new file mode 100644 index 0000000000..5c104049f9 --- /dev/null +++ b/src/Neo/Cryptography/Crypto.cs @@ -0,0 +1,249 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Crypto.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Utilities.Encoders; +using ECPoint = Neo.Cryptography.ECC.ECPoint; + +namespace Neo.Cryptography; + +/// +/// A cryptographic helper class. +/// +public static class Crypto +{ + private static readonly BigInteger s_prime = new(1, + Hex.Decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F")); + + /// + /// Calculates the 160-bit hash value of the specified message. + /// + /// The message to be hashed. + /// 160-bit hash value. + public static byte[] Hash160(ReadOnlySpan message) + { + return message.Sha256().RIPEMD160(); + } + + /// + /// Calculates the 256-bit hash value of the specified message. + /// + /// The message to be hashed. + /// 256-bit hash value. + public static byte[] Hash256(ReadOnlySpan message) + { + return message.Sha256().Sha256(); + } + + /// + /// Signs the specified message using the ECDSA algorithm and specified hash algorithm. + /// + /// The message to be signed. + /// The private key to be used. + /// The curve of the signature, default is . + /// The hash algorithm to hash the message, default is SHA256. + /// The ECDSA signature for the specified message. + public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve? ecCurve = null, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256) + { + ecCurve ??= ECC.ECCurve.Secp256r1; + var signer = new ECDsaSigner(); + var privateKey = new BigInteger(1, priKey); + var priKeyParameters = new ECPrivateKeyParameters(privateKey, ecCurve.BouncyCastleDomainParams); + signer.Init(true, priKeyParameters); + var messageHash = GetMessageHash(message, hashAlgorithm); + var signature = signer.GenerateSignature(messageHash); + var signatureBytes = new byte[64]; + var rBytes = signature[0].ToByteArrayUnsigned(); + var sBytes = signature[1].ToByteArrayUnsigned(); + // Copy r and s into their respective parts of the signatureBytes array, aligning them to the right. + Buffer.BlockCopy(rBytes, 0, signatureBytes, 32 - rBytes.Length, rBytes.Length); + Buffer.BlockCopy(sBytes, 0, signatureBytes, 64 - sBytes.Length, sBytes.Length); + return signatureBytes; + } + + /// + /// Verifies that a digital signature is appropriate for the provided key, message and hash algorithm. + /// + /// The signed message. + /// The signature to be verified. + /// The public key to be used. + /// The hash algorithm to be used to hash the message, the default is SHA256. + /// if the signature is valid; otherwise, . + public static bool VerifySignature(ReadOnlySpan message, ReadOnlySpan signature, ECPoint pubkey, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256) + { + if (signature.Length != 64) + throw new FormatException("Signature size should be 64 bytes."); + var point = pubkey.Curve.BouncyCastleCurve.Curve.CreatePoint( + new BigInteger(pubkey.X!.Value.ToString()), + new BigInteger(pubkey.Y!.Value.ToString())); + var pubKey = new ECPublicKeyParameters("ECDSA", point, pubkey.Curve.BouncyCastleDomainParams); + var signer = new ECDsaSigner(); + signer.Init(false, pubKey); + var r = new BigInteger(1, signature[..32]); + var s = new BigInteger(1, signature[32..]); + var messageHash = GetMessageHash(message, hashAlgorithm); + return signer.VerifySignature(messageHash, r, s); + } + + /// + /// Verifies that a digital signature is appropriate for the provided key, curve, message and hasher. + /// + /// The signed message. + /// The signature to be verified. + /// The public key to be used. + /// The curve to be used by the ECDSA algorithm. + /// The hash algorithm to be used hash the message, the default is SHA256. + /// if the signature is valid; otherwise, . + public static bool VerifySignature(ReadOnlySpan message, ReadOnlySpan signature, ReadOnlySpan pubkey, ECC.ECCurve curve, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256) + { + return VerifySignature(message, signature, ECPoint.DecodePoint(pubkey, curve), hashAlgorithm); + } + + /// + /// Get hash from message. + /// + /// Original message + /// The hash algorithm to be used hash the message, the default is SHA256. + /// Hashed message + public static byte[] GetMessageHash(byte[] message, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256) + { + return hashAlgorithm switch + { + HashAlgorithm.SHA256 => message.Sha256(), + HashAlgorithm.SHA512 => message.Sha512(), + HashAlgorithm.Keccak256 => message.Keccak256(), + _ => throw new NotSupportedException(nameof(hashAlgorithm)) + }; + } + + /// + /// Get hash from message. + /// + /// Original message + /// The hash algorithm to be used hash the message, the default is SHA256. + /// Hashed message + public static byte[] GetMessageHash(ReadOnlySpan message, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256) + { + return hashAlgorithm switch + { + HashAlgorithm.SHA256 => message.Sha256(), + HashAlgorithm.SHA512 => message.Sha512(), + HashAlgorithm.Keccak256 => message.Keccak256(), + _ => throw new NotSupportedException(nameof(hashAlgorithm)) + }; + } + + /// + /// Recovers the public key from a signature and message hash. + /// + /// Signature, either 65 bytes (r[32] || s[32] || v[1]) or + /// 64 bytes in "compact" form (r[32] || yParityAndS[32]). + /// 32-byte message hash + /// The recovered public key + /// Thrown if signature or hash is invalid + public static ECC.ECPoint ECRecover(byte[] signature, byte[] hash) + { + if (signature.Length != 65 && signature.Length != 64) + throw new ArgumentException("Signature must be 65 or 64 bytes", nameof(signature)); + if (hash.Length != 32) + throw new ArgumentException("Message hash must be 32 bytes", nameof(hash)); + + try + { + // Extract (r, s) and compute integer recId + BigInteger r, s; + int recId; + + if (signature.Length == 65) + { + // Format: r[32] || s[32] || v[1] + r = new BigInteger(1, [.. signature.Take(32)]); + s = new BigInteger(1, [.. signature.Skip(32).Take(32)]); + + // v could be 0..3 or 27..30 (Ethereum style). + var v = signature[64]; + recId = v >= 27 ? v - 27 : v; // normalize + if (recId < 0 || recId > 3) + throw new ArgumentException("Recovery value must be in range [0..3] after normalization", nameof(signature)); + } + else + { + // 64 bytes "compact" format: r[32] || yParityAndS[32] + // yParity is fused into the top bit of s. + + r = new BigInteger(1, [.. signature.Take(32)]); + var yParityAndS = new BigInteger(1, signature.Skip(32).ToArray()); + + // Mask out top bit to get s + var mask = BigInteger.One.ShiftLeft(255).Subtract(BigInteger.One); + s = yParityAndS.And(mask); + + // Extract yParity (0 or 1) + var yParity = yParityAndS.TestBit(255); + + // For "compact," map parity to recId in [0..1]. + // For typical usage, recId in {0,1} is enough: + recId = yParity ? 1 : 0; + } + + // Decompose recId into i = recId >> 1 and yBit = recId & 1 + var iPart = recId >> 1; // usually 0..1 + var yBit = (recId & 1) == 1; + + // BouncyCastle curve constants + var n = ECC.ECCurve.Secp256k1.BouncyCastleCurve.N; + var e = new BigInteger(1, hash); + + // eInv = -e mod n + var eInv = BigInteger.Zero.Subtract(e).Mod(n); + // rInv = (r^-1) mod n + var rInv = r.ModInverse(n); + // srInv = (s * r^-1) mod n + var srInv = rInv.Multiply(s).Mod(n); + // eInvrInv = (eInv * r^-1) mod n + var eInvrInv = rInv.Multiply(eInv).Mod(n); + + // x = r + iPart * n + var x = r.Add(BigInteger.ValueOf(iPart).Multiply(n)); + // Verify x is within the curve prime + if (x.CompareTo(s_prime) >= 0) + throw new ArgumentException("X coordinate is out of range for secp256k1 curve", nameof(signature)); + + // Decompress to get R + var decompressedRKey = DecompressKey(ECC.ECCurve.Secp256k1.BouncyCastleCurve.Curve, x, yBit); + // Check that R is on curve + if (!decompressedRKey.Multiply(n).IsInfinity) + throw new ArgumentException("R point is not valid on this curve", nameof(signature)); + + // Q = (eInv * G) + (srInv * R) + var q = Org.BouncyCastle.Math.EC.ECAlgorithms.SumOfTwoMultiplies( + ECC.ECCurve.Secp256k1.BouncyCastleCurve.G, eInvrInv, + decompressedRKey, srInv); + + return ECPoint.FromBytes(q.Normalize().GetEncoded(false), ECC.ECCurve.Secp256k1); + } + catch (Exception ex) + { + throw new ArgumentException("Invalid signature parameters", nameof(signature), ex); + } + } + + private static Org.BouncyCastle.Math.EC.ECPoint DecompressKey( + Org.BouncyCastle.Math.EC.ECCurve curve, BigInteger xBN, bool yBit) + { + var compEnc = X9IntegerConverter.IntegerToBytes(xBN, 1 + X9IntegerConverter.GetByteLength(curve)); + compEnc[0] = (byte)(yBit ? 0x03 : 0x02); + return curve.DecodePoint(compEnc); + } +} diff --git a/src/Neo/Cryptography/ECC/ECCurve.cs b/src/Neo/Cryptography/ECC/ECCurve.cs new file mode 100644 index 0000000000..1fbbc321e2 --- /dev/null +++ b/src/Neo/Cryptography/ECC/ECCurve.cs @@ -0,0 +1,88 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ECCurve.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Org.BouncyCastle.Crypto.Parameters; +using System.Globalization; +using System.Numerics; + +namespace Neo.Cryptography.ECC; + +/// +/// Represents an elliptic curve. +/// +public class ECCurve +{ + internal readonly BigInteger Q; + internal readonly ECFieldElement A; + internal readonly ECFieldElement B; + public readonly BigInteger N; + /// + /// The point at infinity. + /// + public readonly ECPoint Infinity; + /// + /// The generator, or base point, for operations on the curve. + /// + public readonly ECPoint G; + + public readonly Org.BouncyCastle.Asn1.X9.X9ECParameters BouncyCastleCurve; + /// + /// Holds domain parameters for Secp256r1 elliptic curve. + /// + public readonly ECDomainParameters BouncyCastleDomainParams; + internal readonly int ExpectedECPointLength; + private readonly int _hashCode; + + private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G, string curveName) + { + this.Q = Q; + ExpectedECPointLength = ((int)Q.GetBitLength() + 7) / 8; + this.A = new ECFieldElement(A, this); + this.B = new ECFieldElement(B, this); + this.N = N; + Infinity = new ECPoint(null, null, this); + this.G = ECPoint.DecodePoint(G, this); + BouncyCastleCurve = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName(curveName); + BouncyCastleDomainParams = new ECDomainParameters(BouncyCastleCurve.Curve, BouncyCastleCurve.G, BouncyCastleCurve.N, BouncyCastleCurve.H); + _hashCode = HashCode.Combine(Q.GetHashCode(), A.GetHashCode(), B.GetHashCode(), N.GetHashCode(), G.Murmur32((uint)G.Length), curveName); + } + + /// + /// Represents a secp256k1 named curve. + /// + public static readonly ECCurve Secp256k1 = new + ( + BigInteger.Parse("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", NumberStyles.AllowHexSpecifier), + BigInteger.Zero, + 7, + BigInteger.Parse("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", NumberStyles.AllowHexSpecifier), + ("04" + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8").HexToBytes(), + "secp256k1" + ); + + /// + /// Represents a secp256r1 named curve. + /// + public static readonly ECCurve Secp256r1 = new + ( + BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", NumberStyles.AllowHexSpecifier), + BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", NumberStyles.AllowHexSpecifier), + BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier), + BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier), + ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes(), + "secp256r1" + ); + + public override int GetHashCode() + { + return _hashCode; + } +} diff --git a/src/Neo/Cryptography/ECC/ECFieldElement.cs b/src/Neo/Cryptography/ECC/ECFieldElement.cs new file mode 100644 index 0000000000..949ace735f --- /dev/null +++ b/src/Neo/Cryptography/ECC/ECFieldElement.cs @@ -0,0 +1,183 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ECFieldElement.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using System.Numerics; + +namespace Neo.Cryptography.ECC; + +internal class ECFieldElement : IComparable, IEquatable +{ + internal readonly BigInteger Value; + private readonly ECCurve _curve; + + public ECFieldElement(BigInteger value, ECCurve curve) + { + if (value >= curve.Q) + throw new ArgumentException($"Invalid field element value: {value}. The value must be less than the curve's prime field size {curve.Q}."); + Value = value; + _curve = curve; + } + + public int CompareTo(ECFieldElement? other) + { + if (ReferenceEquals(this, other)) return 0; + ArgumentNullException.ThrowIfNull(other); + if (!_curve.Equals(other._curve)) throw new InvalidOperationException("Cannot compare ECFieldElements from different curves. Both elements must belong to the same elliptic curve."); + return Value.CompareTo(other.Value); + } + + public override bool Equals(object? obj) + { + return Equals(obj as ECFieldElement); + } + + public bool Equals(ECFieldElement? other) + { + if (ReferenceEquals(this, other)) return true; + if (other == null) return false; + + return Value.Equals(other.Value) && _curve.Equals(other._curve); + } + + private static BigInteger[] FastLucasSequence(BigInteger p, BigInteger P, BigInteger Q, BigInteger k) + { + var n = (int)k.GetBitLength(); + var s = k.GetLowestSetBit(); + + BigInteger Uh = 1; + BigInteger Vl = 2; + BigInteger Vh = P; + BigInteger Ql = 1; + BigInteger Qh = 1; + + for (int j = n - 1; j >= s + 1; --j) + { + Ql = (Ql * Qh).Mod(p); + + if (k.TestBit(j)) + { + Qh = (Ql * Q).Mod(p); + Uh = (Uh * Vh).Mod(p); + Vl = (Vh * Vl - P * Ql).Mod(p); + Vh = ((Vh * Vh) - (Qh << 1)).Mod(p); + } + else + { + Qh = Ql; + Uh = (Uh * Vl - Ql).Mod(p); + Vh = (Vh * Vl - P * Ql).Mod(p); + Vl = ((Vl * Vl) - (Ql << 1)).Mod(p); + } + } + + Ql = (Ql * Qh).Mod(p); + Qh = (Ql * Q).Mod(p); + Uh = (Uh * Vl - Ql).Mod(p); + Vl = (Vh * Vl - P * Ql).Mod(p); + Ql = (Ql * Qh).Mod(p); + + for (var j = 1; j <= s; ++j) + { + Uh = Uh * Vl * p; + Vl = ((Vl * Vl) - (Ql << 1)).Mod(p); + Ql = (Ql * Ql).Mod(p); + } + + return [Uh, Vl]; + } + + public override int GetHashCode() + { + return HashCode.Combine(_curve.GetHashCode(), Value.GetHashCode()); + } + + public ECFieldElement? Sqrt() + { + if (_curve.Q.TestBit(1)) + { + var z = new ECFieldElement(BigInteger.ModPow(Value, (_curve.Q >> 2) + 1, _curve.Q), _curve); + return z.Square().Equals(this) ? z : null; + } + var qMinusOne = _curve.Q - 1; + var legendreExponent = qMinusOne >> 1; + if (BigInteger.ModPow(Value, legendreExponent, _curve.Q) != 1) + return null; + var u = qMinusOne >> 2; + var k = (u << 1) + 1; + var Q = Value; + var fourQ = (Q << 2).Mod(_curve.Q); + BigInteger U, V; + do + { + BigInteger P; + do + { + P = RandomNumberFactory.NextBigInteger((int)_curve.Q.GetBitLength()); + } + while (P >= _curve.Q || BigInteger.ModPow(P * P - fourQ, legendreExponent, _curve.Q) != qMinusOne); + var result = FastLucasSequence(_curve.Q, P, Q, k); + U = result[0]; + V = result[1]; + if ((V * V).Mod(_curve.Q) == fourQ) + { + if (V.TestBit(0)) + { + V += _curve.Q; + } + V >>= 1; + return new ECFieldElement(V, _curve); + } + } + while (U.Equals(BigInteger.One) || U.Equals(qMinusOne)); + return null; + } + + public ECFieldElement Square() + { + return new ECFieldElement((Value * Value).Mod(_curve.Q), _curve); + } + + public byte[] ToByteArray() + { + var data = Value.ToByteArray(isUnsigned: true, isBigEndian: true); + if (data.Length == 32) + return data; + var buffer = new byte[32]; + Buffer.BlockCopy(data, 0, buffer, buffer.Length - data.Length, data.Length); + return buffer; + } + + public static ECFieldElement operator -(ECFieldElement x) + { + return new ECFieldElement((-x.Value).Mod(x._curve.Q), x._curve); + } + + public static ECFieldElement operator *(ECFieldElement x, ECFieldElement y) + { + return new ECFieldElement((x.Value * y.Value).Mod(x._curve.Q), x._curve); + } + + public static ECFieldElement operator /(ECFieldElement x, ECFieldElement y) + { + return new ECFieldElement((x.Value * y.Value.ModInverse(x._curve.Q)).Mod(x._curve.Q), x._curve); + } + + public static ECFieldElement operator +(ECFieldElement x, ECFieldElement y) + { + return new ECFieldElement((x.Value + y.Value).Mod(x._curve.Q), x._curve); + } + + public static ECFieldElement operator -(ECFieldElement x, ECFieldElement y) + { + return new ECFieldElement((x.Value - y.Value).Mod(x._curve.Q), x._curve); + } +} diff --git a/src/Neo/Cryptography/ECC/ECPoint.cs b/src/Neo/Cryptography/ECC/ECPoint.cs new file mode 100644 index 0000000000..ac46c6315e --- /dev/null +++ b/src/Neo/Cryptography/ECC/ECPoint.cs @@ -0,0 +1,469 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ECPoint.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO.Caching; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Neo.Cryptography.ECC; + +/// +/// Represents a (X,Y) coordinate pair for elliptic curve cryptography (ECC) structures. +/// +public class ECPoint : IComparable, IEquatable, ISerializable, ISerializableSpan +{ + internal ECFieldElement? X, Y; + internal ECCurve Curve { get => field ?? ECCurve.Secp256r1; private init; } + private byte[]? _compressedPoint, _uncompressedPoint; + + /// + /// Indicates whether it is a point at infinity. + /// + public bool IsInfinity + { + get { return X == null && Y == null; } + } + + public int Size => IsInfinity ? 1 : 33; + + private static ECPointCache PointCacheK1 { get; } = new(1000); + private static ECPointCache PointCacheR1 { get; } = new(1000); + + /// + /// Initializes a new instance of the class with the secp256r1 curve. + /// + public ECPoint() : this(null, null, ECCurve.Secp256r1) { } + + internal ECPoint(ECFieldElement? x, ECFieldElement? y, ECCurve curve) + { + if (x is null ^ y is null) + throw new ArgumentException("Invalid ECPoint construction: exactly one of the field elements (X or Y) is null. Both X and Y must be either null (for infinity point) or non-null (for valid point)."); + X = x; + Y = y; + Curve = curve; + } + + public int CompareTo(ECPoint? other) + { + ArgumentNullException.ThrowIfNull(other); + if (!Curve.Equals(other.Curve)) throw new InvalidOperationException("Cannot compare ECPoints with different curves. Both points must use the same elliptic curve for comparison."); + if (ReferenceEquals(this, other)) return 0; + if (IsInfinity) return other.IsInfinity ? 0 : -1; + if (other.IsInfinity) return IsInfinity ? 0 : 1; + + var result = X!.CompareTo(other.X!); + if (result != 0) return result; + return Y!.CompareTo(other.Y!); + } + + /// + /// Decode an object from a sequence of byte. + /// + /// The sequence of byte to be decoded. + /// The object used to construct the . + /// The decoded point. + public static ECPoint DecodePoint(ReadOnlySpan encoded, ECCurve curve) + { + switch (encoded[0]) + { + case 0x02: // compressed + case 0x03: // compressed + { + if (encoded.Length != (curve.ExpectedECPointLength + 1)) + throw new FormatException($"Invalid compressed ECPoint encoding length: expected {curve.ExpectedECPointLength + 1} bytes, but got {encoded.Length} bytes. Compressed points must be exactly {curve.ExpectedECPointLength + 1} bytes long."); + return DecompressPoint(encoded, curve); + } + case 0x04: // uncompressed + { + if (encoded.Length != (2 * curve.ExpectedECPointLength + 1)) + throw new FormatException($"Invalid uncompressed ECPoint encoding length: expected {2 * curve.ExpectedECPointLength + 1} bytes, but got {encoded.Length} bytes. Uncompressed points must be exactly {2 * curve.ExpectedECPointLength + 1} bytes long."); + var x1 = new BigInteger(encoded[1..(1 + curve.ExpectedECPointLength)], isUnsigned: true, isBigEndian: true); + var y1 = new BigInteger(encoded[(1 + curve.ExpectedECPointLength)..], isUnsigned: true, isBigEndian: true); + return new ECPoint(new ECFieldElement(x1, curve), new ECFieldElement(y1, curve), curve) + { + _uncompressedPoint = encoded.ToArray() + }; + } + default: + throw new FormatException($"Invalid ECPoint encoding format: unknown prefix byte 0x{encoded[0]:X2}. Expected 0x02, 0x03 (compressed), or 0x04 (uncompressed)."); + } + } + + private static ECPoint DecompressPoint(ReadOnlySpan encoded, ECCurve curve) + { + ECPointCache pointCache; + if (curve == ECCurve.Secp256k1) pointCache = PointCacheK1; + else if (curve == ECCurve.Secp256r1) pointCache = PointCacheR1; + else throw new FormatException($"Unsupported elliptic curve: {curve}. Only Secp256k1 and Secp256r1 curves are supported for point decompression."); + + var compressedPoint = encoded.ToArray(); + if (!pointCache.TryGet(compressedPoint, out var p)) + { + var yTilde = encoded[0] & 1; + var x1 = new BigInteger(encoded[1..], isUnsigned: true, isBigEndian: true); + p = DecompressPoint(yTilde, x1, curve); + p._compressedPoint = compressedPoint; + pointCache.Add(p); + } + return p; + } + + private static ECPoint DecompressPoint(int yTilde, BigInteger X1, ECCurve curve) + { + var x = new ECFieldElement(X1, curve); + var alpha = x * (x.Square() + curve.A) + curve.B; + var beta = alpha.Sqrt() ?? throw new ArithmeticException("Failed to decompress ECPoint: the provided X coordinate does not correspond to a valid point on the curve. The point compression is invalid."); + var betaValue = beta.Value; + var bit0 = betaValue.IsEven ? 0 : 1; + + if (bit0 != yTilde) + { + // Use the other root + beta = new ECFieldElement(curve.Q - betaValue, curve); + } + + return new ECPoint(x, beta, curve); + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + var p = DeserializeFrom(ref reader, Curve); + X = p.X; + Y = p.Y; + } + + /// + /// Deserializes an object from a . + /// + /// The for reading data. + /// The object used to construct the . + /// The deserialized point. + public static ECPoint DeserializeFrom(ref MemoryReader reader, ECCurve curve) + { + var size = reader.Peek() switch + { + 0x02 or 0x03 => 1 + curve.ExpectedECPointLength, + 0x04 => 1 + curve.ExpectedECPointLength * 2, + _ => throw new FormatException($"Invalid ECPoint encoding format in serialized data: unknown prefix byte 0x{reader.Peek():X2}. Expected 0x02, 0x03 (compressed), or 0x04 (uncompressed).") + }; + return DecodePoint(reader.ReadMemory(size).Span, curve); + } + + /// + /// Encodes an object to a byte array. + /// + /// Indicates whether to encode it in a compressed format. + /// The encoded point. + /// Note: The return should't be modified because it could be cached. + public byte[] EncodePoint(bool commpressed) + { + if (IsInfinity) return new byte[1]; + byte[] data; + if (commpressed) + { + if (_compressedPoint != null) return _compressedPoint; + data = new byte[33]; + } + else + { + if (_uncompressedPoint != null) return _uncompressedPoint; + data = new byte[65]; + var yBytes = Y!.Value.ToByteArray(isUnsigned: true, isBigEndian: true); + Buffer.BlockCopy(yBytes, 0, data, 65 - yBytes.Length, yBytes.Length); + } + var xBytes = X!.Value.ToByteArray(isUnsigned: true, isBigEndian: true); + Buffer.BlockCopy(xBytes, 0, data, 33 - xBytes.Length, xBytes.Length); + data[0] = commpressed ? Y!.Value.IsEven ? (byte)0x02 : (byte)0x03 : (byte)0x04; + if (commpressed) _compressedPoint = data; + else _uncompressedPoint = data; + return data; + } + + public bool Equals(ECPoint? other) + { + if (ReferenceEquals(this, other)) return true; + if (other is null) return false; + if (!Curve.Equals(other.Curve)) return false; + if (IsInfinity && other.IsInfinity) return true; + if (IsInfinity || other.IsInfinity) return false; + return X!.Equals(other.X) && Y!.Equals(other.Y); + } + + public override bool Equals(object? obj) + { + return Equals(obj as ECPoint); + } + + /// + /// Constructs an object from a byte array. + /// + /// The byte array to be used to construct the object. + /// The object used to construct the . + /// The decoded point. + public static ECPoint FromBytes(byte[] bytes, ECCurve curve) + { + return bytes.Length switch + { + 33 or 65 => DecodePoint(bytes, curve), + 64 or 72 => DecodePoint([.. new byte[] { 0x04 }, .. bytes[^64..]], curve), + 96 or 104 => DecodePoint([.. new byte[] { 0x04 }, .. bytes[^96..^32]], curve), + _ => throw new FormatException($"Invalid ECPoint byte array length: {bytes.Length} bytes. Expected 33, 65 (with prefix), 64, 72 (raw coordinates), 96, or 104 bytes."), + }; + } + + public override int GetHashCode() + { + return HashCode.Combine(Curve.GetHashCode(), X?.GetHashCode() ?? 0, Y?.GetHashCode() ?? 0); + } + + internal static ECPoint Multiply(ECPoint p, BigInteger k) + { + // floor(log2(k)) + var m = (int)k.GetBitLength(); + + // width of the Window NAF + sbyte width; + + // Required length of precomputing array + int reqPreCompLen; + + // Determine optimal width and corresponding length of precomputing array + // array based on literature values + if (m < 13) + { + width = 2; + reqPreCompLen = 1; + } + else if (m < 41) + { + width = 3; + reqPreCompLen = 2; + } + else if (m < 121) + { + width = 4; + reqPreCompLen = 4; + } + else if (m < 337) + { + width = 5; + reqPreCompLen = 8; + } + else if (m < 897) + { + width = 6; + reqPreCompLen = 16; + } + else if (m < 2305) + { + width = 7; + reqPreCompLen = 32; + } + else + { + width = 8; + reqPreCompLen = 127; + } + + // The length of the precomputing array + var preCompLen = 1; + var preComp = new ECPoint[] { p }; + var twiceP = p.Twice(); + + if (preCompLen < reqPreCompLen) + { + // Precomputing array must be made bigger, copy existing preComp + // array into the larger new preComp array + var oldPreComp = preComp; + preComp = new ECPoint[reqPreCompLen]; + Array.Copy(oldPreComp, 0, preComp, 0, preCompLen); + + for (var i = preCompLen; i < reqPreCompLen; i++) + { + // Compute the new ECPoints for the precomputing array. + // The values 1, 3, 5, ..., 2^(width-1)-1 times p are + // computed + preComp[i] = twiceP + preComp[i - 1]; + } + } + + // Compute the Window NAF of the desired width + var wnaf = WindowNaf(width, k); + var l = wnaf.Length; + + // Apply the Window NAF to p using the precomputed ECPoint values. + var q = p.Curve.Infinity; + for (var i = l - 1; i >= 0; i--) + { + q = q.Twice(); + + if (wnaf[i] != 0) + { + if (wnaf[i] > 0) + { + q += preComp[(wnaf[i] - 1) / 2]; + } + else + { + // wnaf[i] < 0 + q -= preComp[(-wnaf[i] - 1) / 2]; + } + } + } + + return q; + } + + /// + /// Parse the object from a . + /// + /// The to be parsed. + /// The object used to construct the . + /// The parsed point. + public static ECPoint Parse(string value, ECCurve curve) + { + return DecodePoint(value.HexToBytes(), curve); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(EncodePoint(true)); + } + + /// + /// Gets a ReadOnlySpan that represents the current value. + /// + /// A ReadOnlySpan that represents the current value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan GetSpan() + { + return EncodePoint(true).AsSpan(); + } + + public override string ToString() + { + return EncodePoint(true).ToHexString(); + } + + /// + /// Try parse the object from a . + /// + /// The to be parsed. + /// The object used to construct the . + /// The parsed point. + /// if was converted successfully; otherwise, . + public static bool TryParse(string value, ECCurve curve, [NotNullWhen(true)] out ECPoint? point) + { + try + { + point = Parse(value, curve); + return true; + } + catch (FormatException) + { + point = null; + return false; + } + } + + internal ECPoint Twice() + { + if (IsInfinity) + return this; + if (Y!.Value.Sign == 0) + return Curve.Infinity; + var two = new ECFieldElement(2, Curve); + var three = new ECFieldElement(3, Curve); + var gamma = (X!.Square() * three + Curve.A) / (Y * two); + var x3 = gamma.Square() - X! * two; + var y3 = gamma * (X! - x3) - Y; + return new ECPoint(x3, y3, Curve); + } + + private static sbyte[] WindowNaf(sbyte width, BigInteger k) + { + var wnaf = new sbyte[k.GetBitLength() + 1]; + var pow2wB = (short)(1 << width); + var i = 0; + var length = 0; + while (k.Sign > 0) + { + if (!k.IsEven) + { + var remainder = k % pow2wB; + if (remainder.TestBit(width - 1)) + { + wnaf[i] = (sbyte)(remainder - pow2wB); + } + else + { + wnaf[i] = (sbyte)remainder; + } + k -= wnaf[i]; + length = i; + } + else + { + wnaf[i] = 0; + } + k >>= 1; + i++; + } + length++; + var wnafShort = new sbyte[length]; + Array.Copy(wnaf, 0, wnafShort, 0, length); + return wnafShort; + } + + public static ECPoint operator -(ECPoint x) + { + return new ECPoint(x.X, -x.Y!, x.Curve); + } + + public static ECPoint operator *(ECPoint p, byte[] n) + { + if (n.Length != 32) + throw new ArgumentException($"Invalid byte array length for ECPoint multiplication: {n.Length} bytes. The scalar must be exactly 32 bytes.", nameof(n)); + if (p.IsInfinity) + return p; + var k = new BigInteger(n, isUnsigned: true, isBigEndian: true); + if (k.Sign == 0) + return p.Curve.Infinity; + return Multiply(p, k); + } + + public static ECPoint operator +(ECPoint x, ECPoint y) + { + if (x.IsInfinity) + return y; + if (y.IsInfinity) + return x; + if (x.X!.Equals(y.X)) + { + if (x.Y!.Equals(y.Y)) + return x.Twice(); + return x.Curve.Infinity; + } + var gamma = (y.Y! - x.Y!) / (y.X! - x.X!); + var x3 = gamma.Square() - x.X! - y.X!; + var y3 = gamma * (x.X! - x3) - x.Y!; + return new ECPoint(x3, y3, x.Curve); + } + + public static ECPoint operator -(ECPoint x, ECPoint y) + { + if (y.IsInfinity) + return x; + return x + (-y); + } +} diff --git a/src/Neo/Cryptography/Ed25519.cs b/src/Neo/Cryptography/Ed25519.cs new file mode 100644 index 0000000000..9157e7b146 --- /dev/null +++ b/src/Neo/Cryptography/Ed25519.cs @@ -0,0 +1,97 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Ed25519.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Security; + +namespace Neo.Cryptography; + +public class Ed25519 +{ + internal const int PublicKeySize = 32; + private const int PrivateKeySize = 32; + internal const int SignatureSize = 64; + + /// + /// Generates a new Ed25519 key pair. + /// + /// A byte array containing the private key. + public static byte[] GenerateKeyPair() + { + var keyPairGenerator = new Ed25519KeyPairGenerator(); + keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom())); + var keyPair = keyPairGenerator.GenerateKeyPair(); + return ((Ed25519PrivateKeyParameters)keyPair.Private).GetEncoded(); + } + + /// + /// Derives the public key from a given private key. + /// + /// The private key as a byte array. + /// The corresponding public key as a byte array. + /// Thrown when the private key size is invalid. + public static byte[] GetPublicKey(byte[] privateKey) + { + if (privateKey.Length != PrivateKeySize) + throw new ArgumentException($"Invalid Ed25519 private key size: expected {PrivateKeySize} bytes, but got {privateKey.Length} bytes.", nameof(privateKey)); + + var privateKeyParams = new Ed25519PrivateKeyParameters(privateKey, 0); + return privateKeyParams.GeneratePublicKey().GetEncoded(); + } + + /// + /// Signs a message using the provided private key. + /// Parameters are in the same order as the sample in the Ed25519 specification + /// Ed25519.sign(privkey, pubkey, msg) with pubkey omitted + /// ref. https://datatracker.ietf.org/doc/html/rfc8032. + /// + /// The private key used for signing. + /// The message to be signed. + /// The signature as a byte array. + /// Thrown when the private key size is invalid. + public static byte[] Sign(byte[] privateKey, byte[] message) + { + if (privateKey.Length != PrivateKeySize) + throw new ArgumentException($"Invalid Ed25519 private key size: expected {PrivateKeySize} bytes, but got {privateKey.Length} bytes.", nameof(privateKey)); + + var signer = new Ed25519Signer(); + signer.Init(true, new Ed25519PrivateKeyParameters(privateKey, 0)); + signer.BlockUpdate(message, 0, message.Length); + return signer.GenerateSignature(); + } + + /// + /// Verifies an Ed25519 signature for a given message using the provided public key. + /// Parameters are in the same order as the sample in the Ed25519 specification + /// Ed25519.verify(public, msg, signature) + /// ref. https://datatracker.ietf.org/doc/html/rfc8032. + /// + /// The 32-byte public key used for verification. + /// The message that was signed. + /// The 64-byte signature to verify. + /// True if the signature is valid for the given message and public key; otherwise, false. + /// Thrown when the signature or public key size is invalid. + public static bool Verify(byte[] publicKey, byte[] message, byte[] signature) + { + if (signature.Length != SignatureSize) + throw new ArgumentException($"Invalid Ed25519 signature size: expected {SignatureSize} bytes, but got {signature.Length} bytes.", nameof(signature)); + + if (publicKey.Length != PublicKeySize) + throw new ArgumentException($"Invalid Ed25519 public key size: expected {PublicKeySize} bytes, but got {publicKey.Length} bytes.", nameof(publicKey)); + + var verifier = new Ed25519Signer(); + verifier.Init(false, new Ed25519PublicKeyParameters(publicKey, 0)); + verifier.BlockUpdate(message, 0, message.Length); + return verifier.VerifySignature(signature); + } +} diff --git a/src/Neo/Cryptography/HashAlgorithm.cs b/src/Neo/Cryptography/HashAlgorithm.cs new file mode 100644 index 0000000000..8614001e5c --- /dev/null +++ b/src/Neo/Cryptography/HashAlgorithm.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// HashAlgorithm.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +public enum HashAlgorithm : byte +{ + /// + /// The SHA256 hash algorithm. + /// + SHA256 = 0x00, + + /// + /// The Keccak256 hash algorithm. + /// + Keccak256 = 0x01, + + /// + /// The SHA512 hash algorithm. + /// + SHA512 = 0x02 +} diff --git a/src/Neo/Cryptography/Helper.cs b/src/Neo/Cryptography/Helper.cs new file mode 100644 index 0000000000..43dab69a7f --- /dev/null +++ b/src/Neo/Cryptography/Helper.cs @@ -0,0 +1,459 @@ +// Copyright (C) 2015-2025 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.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.Wallets; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using ECPoint = Neo.Cryptography.ECC.ECPoint; + +namespace Neo.Cryptography; + +/// +/// A helper class for cryptography +/// +public static class Helper +{ + private static readonly bool s_isOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + + private const int AesNonceSizeBytes = 12; + private const int AesTagSizeBytes = 16; + + /// + /// Computes the hash value for the specified byte array using the ripemd160 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] RIPEMD160(this byte[] value) + { + var digest = new RipeMD160Digest(); + var buffer = new byte[digest.GetDigestSize()]; + digest.BlockUpdate(value, 0, value.Length); + digest.DoFinal(buffer, 0); + return buffer; + } + + /// + /// Computes the hash value for the specified byte array using the ripemd160 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] RIPEMD160(this ReadOnlySpan value) + { + var digest = new RipeMD160Digest(); + var buffer = new byte[digest.GetDigestSize()]; + digest.BlockUpdate(value); + digest.DoFinal(buffer, 0); + return buffer; + } + + /// + /// Computes the hash value for the specified byte array using the murmur algorithm. + /// + /// The input to compute the hash code for. + /// The seed used by the murmur algorithm. + /// The computed hash code. + public static uint Murmur32(this byte[] value, uint seed) + { + return Cryptography.Murmur32.HashToUInt32(value, seed); + } + + /// + /// Computes the hash value for the specified byte array using the murmur algorithm. + /// + /// The input to compute the hash code for. + /// The seed used by the murmur algorithm. + /// The computed hash code. + public static uint Murmur32(this ReadOnlySpan value, uint seed) + { + return Cryptography.Murmur32.HashToUInt32(value, seed); + } + + /// + /// Computes the 128-bit hash value for the specified byte array using the murmur algorithm. + /// + /// The input to compute the hash code for. + /// The seed used by the murmur algorithm. + /// The computed hash code. + public static byte[] Murmur128(this byte[] value, uint seed) => value.AsReadOnlySpan().Murmur128(seed); + + /// + /// Computes the 128-bit hash value for the specified byte array using the murmur algorithm. + /// + /// The input to compute the hash code for. + /// The seed used by the murmur algorithm. + /// The computed hash code. + public static byte[] Murmur128(this ReadOnlySpan value, uint seed) + { + return new Murmur128(seed).ComputeHash(value); + } + + /// + /// Computes the hash value for the specified byte array using the sha256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha256(this byte[] value) + { + return SHA256.HashData(value); + } + + /// + /// Computes the hash value for the specified byte array using the sha512 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha512(this byte[] value) + { + return SHA512.HashData(value); + } + + /// + /// Computes the hash value for the specified byte array using the sha3-512 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha3_512(this byte[] source) => Sha3_512(source.AsSpan()); + + /// + /// Computes the hash value for the specified byte array using the sha3-256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha3_256(this byte[] source) => Sha3_256(source.AsSpan()); + + /// + /// Computes the hash value for the specified byte array using the blake2b-512 algorithm. + /// + /// The input to compute the hash code for. + /// The salt to use for the hash, and must be 16 bytes. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Blake2b_512(this byte[] source, byte[]? salt = null) => Blake2b_512(source.AsSpan(), salt); + + /// + /// Computes the hash value for the specified byte array using the blake2b-512 algorithm. + /// + /// The input to compute the hash code for. + /// The salt to use for the hash, and must be 16 bytes. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Blake2b_256(this byte[] source, byte[]? salt = null) => Blake2b_256(source.AsSpan(), salt); + + /// + /// Computes the hash value for the specified region of the specified byte array using the sha256 algorithm. + /// + /// The input to compute the hash code for. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the array to use as data. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha256(this byte[] value, int offset, int count) + { + return SHA256.HashData(value.AsSpan(offset, count)); + } + + /// + /// Computes the hash value for the specified region of the specified byte array using the sha512 algorithm. + /// + /// The input to compute the hash code for. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the array to use as data. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha512(this byte[] value, int offset, int count) + { + return SHA512.HashData(value.AsSpan(offset, count)); + } + + /// + /// Computes the hash value for the specified byte array using the sha256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha256(this ReadOnlySpan value) + { + var buffer = new byte[32]; + SHA256.HashData(value, buffer); + return buffer; + } + + /// + /// Computes the hash value for the specified byte array using the sha512 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha512(this ReadOnlySpan value) + { + var buffer = new byte[64]; + SHA512.HashData(value, buffer); + return buffer; + } + + /// + /// Computes the hash value for the specified byte array using the sha3-512 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Sha3_512(this ReadOnlySpan source) + { + var sha3 = new Sha3Digest(512); // not all platforms support SHA3-512 for C# standard library. + sha3.BlockUpdate(source); + + var result = new byte[sha3.GetDigestSize()]; + sha3.DoFinal(result, 0); + return result; + } + + /// + /// Computes the hash value for the specified byte array using the sha3-256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Sha3_256(this ReadOnlySpan source) + { + var sha3 = new Sha3Digest(256); + sha3.BlockUpdate(source); + + var result = new byte[sha3.GetDigestSize()]; + sha3.DoFinal(result, 0); + return result; + } + + /// + /// Computes the hash value for the specified byte array using the blake2b-512 algorithm. + /// + /// The input to compute the hash code for. + /// The salt to use for the hash and must be null or 16 bytes. + /// The computed hash code. + /// Thrown when the salt is not null or 16 bytes. + public static byte[] Blake2b_512(this ReadOnlySpan source, byte[]? salt = null) + { + if (salt is not null && salt.Length != 16) + throw new ArgumentException("The salt must be null or 16 bytes.", nameof(salt)); + + var blake2b = new Blake2bDigest(null, 64, salt, null); + blake2b.BlockUpdate(source); + + var result = new byte[blake2b.GetDigestSize()]; + blake2b.DoFinal(result, 0); + return result; + } + + /// + /// Computes the hash value for the specified byte array using the blake2b-256 algorithm. + /// + /// The input to compute the hash code for. + /// The salt to use for the hash and must be null or 16 bytes. + /// The computed hash code. + /// Thrown when the salt is not null or 16 bytes. + public static byte[] Blake2b_256(this ReadOnlySpan source, byte[]? salt = null) + { + if (salt is not null && salt.Length != 16) + throw new ArgumentException("The salt must be null or 16 bytes.", nameof(salt)); + + var blake2b = new Blake2bDigest(null, 32, salt, null); + blake2b.BlockUpdate(source); + + var result = new byte[blake2b.GetDigestSize()]; + blake2b.DoFinal(result, 0); + return result; + } + + /// + /// Computes the hash value for the specified byte array using the sha256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Sha256(this Span value) + { + return Sha256((ReadOnlySpan)value); + } + + /// + /// Computes the hash value for the specified byte array using the sha512 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Sha512(this Span value) + { + return Sha512((ReadOnlySpan)value); + } + + /// + /// Computes the hash value for the specified byte array using the sha3-512 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha3_512(this Span source) => Sha3_512((ReadOnlySpan)source); + + /// + /// Computes the hash value for the specified byte array using the sha3-256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Sha3_256(this Span source) => Sha3_256((ReadOnlySpan)source); + + /// + /// Computes the hash value for the specified byte array using the blake2b-512 algorithm. + /// + /// The input to compute the hash code for. + /// The salt to use for the hash, and must be 16 bytes. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Blake2b_512(this Span source, byte[]? salt = null) + => Blake2b_512((ReadOnlySpan)source, salt); + + /// + /// Computes the hash value for the specified byte array using the blake2b-256 algorithm. + /// + /// The input to compute the hash code for. + /// The salt to use for the hash, and must be 16 bytes. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Blake2b_256(this Span source, byte[]? salt = null) + => Blake2b_256((ReadOnlySpan)source, salt); + + /// + /// Computes the hash value for the specified byte array using the keccak256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Keccak256(this byte[] value) => value.AsSpan().Keccak256(); + + /// + /// Computes the hash value for the specified byte array using the keccak256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Keccak256(this ReadOnlySpan value) + { + var keccak = new KeccakDigest(256); + keccak.BlockUpdate(value); + var result = new byte[keccak.GetDigestSize()]; + keccak.DoFinal(result, 0); + return result; + } + + /// + /// Computes the hash value for the specified byte array using the keccak256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + public static byte[] Keccak256(this Span value) => ((ReadOnlySpan)value).Keccak256(); + + public static byte[] AES256Encrypt(this byte[] plainData, byte[] key, byte[] nonce, byte[]? associatedData = null) + { + if (nonce.Length != AesNonceSizeBytes) + throw new ArgumentOutOfRangeException(nameof(nonce), $"`nonce` must be {AesNonceSizeBytes} bytes"); + + var tag = new byte[AesTagSizeBytes]; + var cipherBytes = new byte[plainData.Length]; + if (!s_isOSX) + { + using var cipher = new AesGcm(key, AesTagSizeBytes); + cipher.Encrypt(nonce, plainData, cipherBytes, tag, associatedData); + } + else + { + var cipher = new GcmBlockCipher(new AesEngine()); + var parameters = new AeadParameters( + new KeyParameter(key), + AesTagSizeBytes * 8, // 128 = 16 * 8 => (tag size * 8) + nonce, + associatedData); + cipher.Init(true, parameters); + cipherBytes = new byte[cipher.GetOutputSize(plainData.Length)]; + var length = cipher.ProcessBytes(plainData, 0, plainData.Length, cipherBytes, 0); + cipher.DoFinal(cipherBytes, length); + } + return [.. nonce, .. cipherBytes, .. tag]; + } + + public static byte[] AES256Decrypt(this byte[] encryptedData, byte[] key, byte[]? associatedData = null) + { + if (encryptedData.Length < AesNonceSizeBytes + AesTagSizeBytes) + throw new ArgumentException($"The encryptedData.Length must be greater than {AesNonceSizeBytes} + {AesTagSizeBytes}"); + + ReadOnlySpan encrypted = encryptedData; + var nonce = encrypted[..AesNonceSizeBytes]; + var cipherBytes = encrypted[AesNonceSizeBytes..^AesTagSizeBytes]; + var tag = encrypted[^AesTagSizeBytes..]; + var decryptedData = new byte[cipherBytes.Length]; + if (!s_isOSX) + { + using var cipher = new AesGcm(key, AesTagSizeBytes); + cipher.Decrypt(nonce, cipherBytes, tag, decryptedData, associatedData); + } + else + { + var cipher = new GcmBlockCipher(new AesEngine()); + var parameters = new AeadParameters( + new KeyParameter(key), + AesTagSizeBytes * 8, // 128 = 16 * 8 => (tag size * 8) + nonce.ToArray(), + associatedData); + cipher.Init(false, parameters); + decryptedData = new byte[cipher.GetOutputSize(cipherBytes.Length)]; + var length = cipher.ProcessBytes(cipherBytes.ToArray(), 0, cipherBytes.Length, decryptedData, 0); + cipher.DoFinal(decryptedData, length); + } + return decryptedData; + } + + public static byte[] ECDHDeriveKey(KeyPair local, ECPoint remote) + { + ReadOnlySpan pubkeyLocal = local.PublicKey.EncodePoint(false); + ReadOnlySpan pubkeyRemote = remote.EncodePoint(false); + using var ecdh1 = ECDiffieHellman.Create(new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + D = local.PrivateKey, + Q = new System.Security.Cryptography.ECPoint + { + X = pubkeyLocal[1..][..32].ToArray(), + Y = pubkeyLocal[1..][32..].ToArray() + } + }); + using var ecdh2 = ECDiffieHellman.Create(new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + Q = new System.Security.Cryptography.ECPoint + { + X = pubkeyRemote[1..][..32].ToArray(), + Y = pubkeyRemote[1..][32..].ToArray() + } + }); + return ecdh1.DeriveKeyMaterial(ecdh2.PublicKey).Sha256();//z = r * P = r* k * G + } + + internal static bool Test(this BloomFilter filter, Transaction tx) + { + if (filter.Check(tx.Hash.ToArray())) return true; + if (tx.Signers.Any(p => filter.Check(p.Account.ToArray()))) + return true; + return false; + } +} diff --git a/src/Neo/Cryptography/MerkleTree.cs b/src/Neo/Cryptography/MerkleTree.cs new file mode 100644 index 0000000000..ce795a24bb --- /dev/null +++ b/src/Neo/Cryptography/MerkleTree.cs @@ -0,0 +1,155 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MerkleTree.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using System.Collections; +using System.Runtime.CompilerServices; + +namespace Neo.Cryptography; + +/// +/// Represents a merkle tree. +/// +public class MerkleTree +{ + private readonly MerkleTreeNode? _root; + + /// + /// The depth of the tree. + /// + public int Depth { get; } + + internal MerkleTree(UInt256[] hashes) + { + _root = Build(hashes.Select(p => new MerkleTreeNode { Hash = p }).ToArray()); + if (_root is null) return; + + var depth = 1; + for (var i = _root; i.LeftChild != null; i = i.LeftChild) + depth++; + Depth = depth; + } + + private static MerkleTreeNode? Build(MerkleTreeNode[] leaves) + { + if (leaves.Length == 0) return null; + if (leaves.Length == 1) return leaves[0]; + + Span buffer = stackalloc byte[64]; + var parents = new MerkleTreeNode[(leaves.Length + 1) / 2]; + for (var i = 0; i < parents.Length; i++) + { + parents[i] = new MerkleTreeNode + { + LeftChild = leaves[i * 2] + }; + leaves[i * 2].Parent = parents[i]; + if (i * 2 + 1 == leaves.Length) + { + parents[i].RightChild = parents[i].LeftChild; + } + else + { + parents[i].RightChild = leaves[i * 2 + 1]; + leaves[i * 2 + 1].Parent = parents[i]; + } + parents[i].Hash = Concat(buffer, parents[i].LeftChild!.Hash!, parents[i].RightChild!.Hash!); + } + return Build(parents); //TailCall + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static UInt256 Concat(Span buffer, UInt256 hash1, UInt256 hash2) + { + hash1.Serialize(buffer); + hash2.Serialize(buffer[32..]); + + return new UInt256(Crypto.Hash256(buffer)); + } + + /// + /// Computes the root of the hash tree. + /// + /// The leaves of the hash tree. + /// The root of the hash tree. + public static UInt256 ComputeRoot(UInt256[] hashes) + { + if (hashes.Length == 0) return UInt256.Zero; + if (hashes.Length == 1) return hashes[0]; + + var tree = new MerkleTree(hashes); + return tree._root!.Hash!; + } + + private static void DepthFirstSearch(MerkleTreeNode node, List hashes) + { + if (node.LeftChild == null) + { + // if left is null, then right must be null + hashes.Add(node.Hash!); + } + else + { + DepthFirstSearch(node.LeftChild, hashes); + DepthFirstSearch(node.RightChild!, hashes); + } + } + + /// + /// Gets all nodes of the hash tree in depth-first order. + /// + /// All nodes of the hash tree. + public UInt256[] ToHashArray() + { + if (_root is null) return []; + var hashes = new List(); + DepthFirstSearch(_root, hashes); + return [.. hashes]; + } + + /// + /// Trims the hash tree using the specified bit array. + /// + /// The bit array to be used. + public void Trim(BitArray flags) + { + if (_root is null) return; + flags = new BitArray(flags) + { + Length = 1 << (Depth - 1) + }; + Trim(_root, 0, Depth, flags); + } + + private static void Trim(MerkleTreeNode node, int index, int depth, BitArray flags) + { + if (depth == 1) return; + if (node.LeftChild == null) return; // if left is null, then right must be null + if (depth == 2) + { + if (!flags.Get(index * 2) && !flags.Get(index * 2 + 1)) + { + node.LeftChild = null; + node.RightChild = null; + } + } + else + { + Trim(node.LeftChild, index * 2, depth - 1, flags); + Trim(node.RightChild!, index * 2 + 1, depth - 1, flags); + if (node.LeftChild.LeftChild == null && node.RightChild!.RightChild == null) + { + node.LeftChild = null; + node.RightChild = null; + } + } + } +} diff --git a/src/Neo/Cryptography/MerkleTreeNode.cs b/src/Neo/Cryptography/MerkleTreeNode.cs new file mode 100644 index 0000000000..f525c5ee1f --- /dev/null +++ b/src/Neo/Cryptography/MerkleTreeNode.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MerkleTreeNode.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +class MerkleTreeNode +{ + public UInt256? Hash { get; set; } + public MerkleTreeNode? Parent { get; set; } + public MerkleTreeNode? LeftChild { get; set; } + public MerkleTreeNode? RightChild { get; set; } + + public bool IsLeaf => LeftChild == null && RightChild == null; + public bool IsRoot => Parent == null; +} diff --git a/src/Neo/Cryptography/Murmur128.cs b/src/Neo/Cryptography/Murmur128.cs new file mode 100644 index 0000000000..ce2c1de4cc --- /dev/null +++ b/src/Neo/Cryptography/Murmur128.cs @@ -0,0 +1,168 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Murmur128.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Buffers.Binary; +using System.IO.Hashing; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Neo.Cryptography; + +/// +/// Computes the 128 bits murmur hash for the input data. +/// +public sealed class Murmur128 : NonCryptographicHashAlgorithm +{ + private const ulong c1 = 0x87c37b91114253d5; + private const ulong c2 = 0x4cf5ad432745937f; + private const int r1 = 31; + private const int r2 = 33; + private const uint m = 5; + private const uint n1 = 0x52dce729; + private const uint n2 = 0x38495ab5; + + private readonly uint _seed; + private int _length; + + public const int HashSizeInBits = 128; + + // The Tail struct is used to store up to 16 bytes of unprocessed data + // when computing the hash. It leverages the InlineArray attribute for + // efficient memory usage in .NET 8.0 or greater, avoiding heap allocations + // and improving performance for small data sizes. + [InlineArray(16)] + private struct Tail + { + private byte v0; + public Span AsSpan(int start = 0) => MemoryMarshal.CreateSpan(ref v0, 16)[start..]; + } + + private Tail _tail = new(); // cannot be readonly here + + private int _tailLength; + + private ulong H1 { get; set; } + private ulong H2 { get; set; } + + /// + /// Initializes a new instance of the class with the specified seed. + /// + /// The seed to be used. + public Murmur128(uint seed) : base(HashSizeInBits / 8) + { + _seed = seed; + Reset(); + } + + public override void Append(ReadOnlySpan source) + { + _length += source.Length; + if (_tailLength > 0) + { + int copyLength = Math.Min(source.Length, HashSizeInBits / 8 - _tailLength); + source[..copyLength].CopyTo(_tail.AsSpan(_tailLength)); + + _tailLength += copyLength; + if (_tailLength == HashSizeInBits / 8) + { + Mix(_tail.AsSpan()); + _tailLength = 0; + _tail.AsSpan().Clear(); + } + source = source[copyLength..]; + } + + for (; source.Length >= 16; source = source[16..]) + { + Mix(source); + } + + if (source.Length > 0) + { + source.CopyTo(_tail.AsSpan()); + _tailLength = source.Length; + } + } + + protected override void GetCurrentHashCore(Span destination) + { + if (_tailLength > 0) + { + var tail = _tail.AsSpan(); + ulong k1 = BinaryPrimitives.ReadUInt64LittleEndian(tail); + ulong k2 = BinaryPrimitives.ReadUInt64LittleEndian(tail[8..]); + H2 ^= BitOperations.RotateLeft(k2 * c2, r2) * c1; + H1 ^= BitOperations.RotateLeft(k1 * c1, r1) * c2; + } + + H1 ^= (ulong)_length; + H2 ^= (ulong)_length; + + H1 += H2; + H2 += H1; + + H1 = FMix(H1); + H2 = FMix(H2); + + H1 += H2; + H2 += H1; + + // NOTE: in some implementations, H1, H2 are output in big-endian, and little-endian is used here. + if (BinaryPrimitives.TryWriteUInt64LittleEndian(destination, H1)) + BinaryPrimitives.TryWriteUInt64LittleEndian(destination[sizeof(ulong)..], H2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Reset() + { + H1 = H2 = _seed; + _length = 0; + _tailLength = 0; + _tail.AsSpan().Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Mix(ReadOnlySpan source) + { + ulong k1 = BinaryPrimitives.ReadUInt64LittleEndian(source); + ulong k2 = BinaryPrimitives.ReadUInt64LittleEndian(source[8..]); + + H1 ^= BitOperations.RotateLeft(k1 * c1, r1) * c2; + H1 = BitOperations.RotateLeft(H1, 27) + H2; + H1 = H1 * m + n1; + + H2 ^= BitOperations.RotateLeft(k2 * c2, r2) * c1; + H2 = BitOperations.RotateLeft(H2, 31) + H1; + H2 = H2 * m + n2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong FMix(ulong h) + { + h = (h ^ (h >> 33)) * 0xff51afd7ed558ccd; + h = (h ^ (h >> 33)) * 0xc4ceb9fe1a85ec53; + return h ^ (h >> 33); + } + + /// + /// Resets the state and computes the 128 bits murmur hash for the input data. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte[] ComputeHash(ReadOnlySpan source) + { + Reset(); + Append(source); + return GetCurrentHash(); + } +} diff --git a/src/Neo/Cryptography/Murmur32.cs b/src/Neo/Cryptography/Murmur32.cs new file mode 100644 index 0000000000..c1aef17fd0 --- /dev/null +++ b/src/Neo/Cryptography/Murmur32.cs @@ -0,0 +1,178 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Murmur32.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Buffers.Binary; +using System.IO.Hashing; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Neo.Cryptography; + +/// +/// Computes the murmur hash for the input data. +/// Murmur32 is a non-cryptographic hash function. +/// +public sealed class Murmur32 : NonCryptographicHashAlgorithm +{ + private const uint c1 = 0xcc9e2d51; + private const uint c2 = 0x1b873593; + private const int r1 = 15; + private const int r2 = 13; + private const uint m = 5; + private const uint n = 0xe6546b64; + + private readonly uint _seed; + private uint _hash; + private int _length; + + private uint _tail; + private int _tailLength; + + public const int HashSizeInBits = 32; + + /// + /// Initializes a new instance of the class with the specified seed. + /// + /// The seed to be used. + public Murmur32(uint seed) : base(HashSizeInBits / 8) + { + _seed = seed; + Reset(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Reset() + { + _hash = _seed; + _length = 0; + _tailLength = 0; + _tail = 0; + } + + /// + public override void Append(ReadOnlySpan source) + { + _length += source.Length; + if (_tailLength > 0) + { + var remaining = Math.Min(4 - _tailLength, source.Length); + _tail ^= ReadUInt32(source[..remaining]) << (_tailLength * 8); + _tailLength += remaining; + if (_tailLength == 4) + { + Mix(_tail); + _tailLength = 0; + _tail = 0; + } + source = source[remaining..]; + } + + for (; source.Length >= 16; source = source[16..]) + { + var k = BinaryPrimitives.ReadUInt128LittleEndian(source); + Mix((uint)k); + Mix((uint)(k >> 32)); + Mix((uint)(k >> 64)); + Mix((uint)(k >> 96)); + } + + for (; source.Length >= 4; source = source[4..]) + { + Mix(BinaryPrimitives.ReadUInt32LittleEndian(source)); + } + + if (source.Length > 0) + { + _tail = ReadUInt32(source); + _tailLength = source.Length; + } + } + + /// + protected override void GetCurrentHashCore(Span destination) + { + BinaryPrimitives.WriteUInt32LittleEndian(destination, GetCurrentHashUInt32()); + } + + internal uint GetCurrentHashUInt32() + { + if (_tailLength > 0) + _hash ^= BitOperations.RotateLeft(_tail * c1, r1) * c2; + + var state = _hash ^ (uint)_length; + state ^= state >> 16; + state *= 0x85ebca6b; + state ^= state >> 13; + state *= 0xc2b2ae35; + state ^= state >> 16; + return state; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Mix(uint k) + { + k *= c1; + k = BitOperations.RotateLeft(k, r1); + k *= c2; + _hash ^= k; + _hash = BitOperations.RotateLeft(_hash, r2); + _hash = _hash * m + n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint ReadUInt32(ReadOnlySpan source) + { + uint value = 0; + switch (source.Length) + { + case 3: value ^= (uint)source[2] << 16; goto case 2; + case 2: value ^= (uint)source[1] << 8; goto case 1; + case 1: value ^= source[0]; break; + } + return value; + } + + /// + /// Computes the murmur hash for the input data and resets the state. + /// + /// The input to compute the hash code for. + /// The computed hash code in byte[4]. + public byte[] ComputeHash(ReadOnlySpan data) + { + var buffer = new byte[HashSizeInBits / 8]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, ComputeHashUInt32(data)); + return buffer; + } + + /// + /// Resets the state and computes the murmur hash for the input data. + /// + /// The input to compute the hash code for. + /// The computed hash code in uint. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ComputeHashUInt32(ReadOnlySpan data) + { + Reset(); + Append(data); + return GetCurrentHashUInt32(); + } + + /// + /// Computes the murmur hash for the input data. + /// + /// The input to compute the hash code for. + /// The seed used by the murmur algorithm. + /// The computed hash code in uint. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint HashToUInt32(ReadOnlySpan data, uint seed) + => new Murmur32(seed).ComputeHashUInt32(data); +} diff --git a/src/Neo/Extensions/ByteExtensions.cs b/src/Neo/Extensions/ByteExtensions.cs new file mode 100644 index 0000000000..b67305be12 --- /dev/null +++ b/src/Neo/Extensions/ByteExtensions.cs @@ -0,0 +1,65 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ByteExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; + +namespace Neo.Extensions; + +public static class ByteExtensions +{ + /// + /// Compresses the specified data using the LZ4 algorithm. + /// + /// The data to be compressed. + /// The compressed data. + public static ReadOnlyMemory CompressLz4(this byte[] data) + { + return data.AsSpan().CompressLz4(); + } + + /// + /// Decompresses the specified data using the LZ4 algorithm. + /// + /// The compressed data. + /// The maximum data size after decompression. + /// The original data. + public static byte[] DecompressLz4(this byte[] data, int maxOutput) + { + return data.AsSpan().DecompressLz4(maxOutput); + } + + /// + /// Converts a byte array to an object. + /// + /// The type to convert to. + /// The byte array to be converted. + /// The offset into the byte array from which to begin using data. + /// The converted object. + public static T AsSerializable(this byte[] value, int start = 0) where T : ISerializable + { + MemoryReader reader = new(value.AsMemory(start)); + return reader.ReadSerializable(); + } + + /// + /// Converts a byte array to an array. + /// + /// The type of the array element. + /// The byte array to be converted. + /// The maximum number of elements contained in the converted array. + /// The converted array. + public static T[] AsSerializableArray(this byte[] value, int max = 0x1000000) where T : ISerializable + { + MemoryReader reader = new(value); + return reader.ReadSerializableArray(max); + } +} diff --git a/src/Neo/Extensions/Collections/ICollectionExtensions.cs b/src/Neo/Extensions/Collections/ICollectionExtensions.cs new file mode 100644 index 0000000000..9b01102b94 --- /dev/null +++ b/src/Neo/Extensions/Collections/ICollectionExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ICollectionExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Neo.Extensions.Collections; + +public static class ICollectionExtensions +{ + /// + /// Gets the size of the specified array encoded in variable-length encoding. + /// + /// The type of the array element. + /// The specified array. + /// The size of the array. + public static int GetVarSize(this IReadOnlyCollection value) + { + int valueSize; + var t = typeof(T); + if (typeof(ISerializable).IsAssignableFrom(t)) + { + valueSize = value.OfType().Sum(p => p.Size); + } + else if (t.GetTypeInfo().IsEnum) + { + int elementSize; + var u = t.GetTypeInfo().GetEnumUnderlyingType(); + if (u == typeof(sbyte) || u == typeof(byte)) + elementSize = 1; + else if (u == typeof(short) || u == typeof(ushort)) + elementSize = 2; + else if (u == typeof(int) || u == typeof(uint)) + elementSize = 4; + else //if (u == typeof(long) || u == typeof(ulong)) + elementSize = 8; + valueSize = value.Count * elementSize; + } + else + { + valueSize = value.Count * Marshal.SizeOf(); + } + return value.Count.GetVarSize() + valueSize; + } + + /// + /// Converts an array to a byte array. + /// + /// The type of the array element. + /// The array to be converted. + /// The converted byte array. + public static byte[] ToByteArray(this IReadOnlyCollection value) + where T : ISerializable + { + using MemoryStream ms = new(); + using BinaryWriter writer = new(ms, Utility.StrictUTF8, true); + writer.Write(value); + writer.Flush(); + return ms.ToArray(); + } +} diff --git a/src/Neo/Extensions/IO/BinaryReaderExtensions.cs b/src/Neo/Extensions/IO/BinaryReaderExtensions.cs new file mode 100644 index 0000000000..ac59e3ef24 --- /dev/null +++ b/src/Neo/Extensions/IO/BinaryReaderExtensions.cs @@ -0,0 +1,74 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BinaryReaderExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Extensions.IO; + +public static class BinaryReaderExtensions +{ + /// + /// Reads a byte array of the specified size from a . + /// + /// The for reading data. + /// The size of the byte array. + /// The byte array read from the . + public static byte[] ReadFixedBytes(this BinaryReader reader, int size) + { + var index = 0; + var data = new byte[size]; + + while (size > 0) + { + var bytesRead = reader.Read(data, index, size); + if (bytesRead <= 0) + { + throw new FormatException($"BinaryReader.Read returned {bytesRead}"); + } + + size -= bytesRead; + index += bytesRead; + } + + return data; + } + + /// + /// Reads a byte array from a . + /// + /// The for reading data. + /// The maximum size of the byte array. + /// The byte array read from the . + public static byte[] ReadVarBytes(this BinaryReader reader, int max = 0x1000000) + { + return reader.ReadFixedBytes((int)reader.ReadVarInt((ulong)max)); + } + + /// + /// Reads an integer from a . + /// + /// The for reading data. + /// The maximum value of the integer. + /// The integer read from the . + public static ulong ReadVarInt(this BinaryReader reader, ulong max = ulong.MaxValue) + { + var fb = reader.ReadByte(); + ulong value; + if (fb == 0xFD) + value = reader.ReadUInt16(); + else if (fb == 0xFE) + value = reader.ReadUInt32(); + else if (fb == 0xFF) + value = reader.ReadUInt64(); + else + value = fb; + if (value > max) throw new FormatException($"`value`({value}) is out of range (max:{max})"); + return value; + } +} diff --git a/src/Neo/Extensions/IO/BinaryWriterExtensions.cs b/src/Neo/Extensions/IO/BinaryWriterExtensions.cs new file mode 100644 index 0000000000..424ff5d51a --- /dev/null +++ b/src/Neo/Extensions/IO/BinaryWriterExtensions.cs @@ -0,0 +1,137 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BinaryWriterExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; + +public static class BinaryWriterExtensions +{ + /// + /// Writes an object into a . + /// + /// The for writing data. + /// The object to be written. + public static void Write(this BinaryWriter writer, ISerializable value) + { + value.Serialize(writer); + } + + /// + /// Writes an array into a . + /// + /// The type of the array element. + /// The for writing data. + /// The array to be written. + public static void Write(this BinaryWriter writer, IReadOnlyCollection value) + where T : ISerializable + { + ArgumentNullException.ThrowIfNull(value); + + writer.WriteVarInt(value.Count); + foreach (T item in value) + { + item.Serialize(writer); + } + } + + /// + /// Writes a into a . + /// + /// The for writing data. + /// The to be written. + /// The fixed size of the . + public static void WriteFixedString(this BinaryWriter writer, string value, int length) + { + ArgumentNullException.ThrowIfNull(value); + if (value.Length > length) + throw new ArgumentException($"The string value length ({value.Length} characters) exceeds the maximum allowed length of {length} characters.", nameof(value)); + + var bytes = value.ToStrictUtf8Bytes(); + if (bytes.Length > length) + throw new ArgumentException($"The UTF-8 encoded string length ({bytes.Length} bytes) exceeds the maximum allowed length of {length} bytes.", nameof(value)); + writer.Write(bytes); + if (bytes.Length < length) + writer.Write(stackalloc byte[length - bytes.Length]); + } + + /// + /// Writes an array into a . + /// + /// The type of the array element. + /// The for writing data. + /// The array to be written. + public static void WriteNullableArray(this BinaryWriter writer, T?[] value) + where T : class, ISerializable + { + ArgumentNullException.ThrowIfNull(value); + + writer.WriteVarInt(value.Length); + foreach (var item in value) + { + var isNull = item is null; + writer.Write(!isNull); + if (isNull) continue; + item!.Serialize(writer); + } + } + + /// + /// Writes a byte array into a . + /// + /// The for writing data. + /// The byte array to be written. + public static void WriteVarBytes(this BinaryWriter writer, ReadOnlySpan value) + { + writer.WriteVarInt(value.Length); + writer.Write(value); + } + + /// + /// Writes an integer into a . + /// + /// The for writing data. + /// The integer to be written. + public static void WriteVarInt(this BinaryWriter writer, long value) + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value), "cannot be negative"); + if (value < 0xFD) + { + writer.Write((byte)value); + } + else if (value <= 0xFFFF) + { + writer.Write((byte)0xFD); + writer.Write((ushort)value); + } + else if (value <= 0xFFFFFFFF) + { + writer.Write((byte)0xFE); + writer.Write((uint)value); + } + else + { + writer.Write((byte)0xFF); + writer.Write(value); + } + } + + /// + /// Writes a into a . + /// + /// The for writing data. + /// The to be written. + public static void WriteVarString(this BinaryWriter writer, string value) + { + writer.WriteVarBytes(value.ToStrictUtf8Bytes()); + } +} diff --git a/src/Neo/Extensions/IO/ISerializableExtensions.cs b/src/Neo/Extensions/IO/ISerializableExtensions.cs new file mode 100644 index 0000000000..f80d7ede87 --- /dev/null +++ b/src/Neo/Extensions/IO/ISerializableExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ISerializableExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; + +public static class ISerializableExtensions +{ + /// + /// Converts an object to a byte array. + /// + /// The object to be converted. + /// The converted byte array. + public static byte[] ToArray(this ISerializable value) + { + using MemoryStream ms = new(); + using BinaryWriter writer = new(ms, Utility.StrictUTF8, true); + value.Serialize(writer); + writer.Flush(); + return ms.ToArray(); + } +} diff --git a/src/Neo/Extensions/IO/MemoryReaderExtensions.cs b/src/Neo/Extensions/IO/MemoryReaderExtensions.cs new file mode 100644 index 0000000000..0029adfbb9 --- /dev/null +++ b/src/Neo/Extensions/IO/MemoryReaderExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MemoryReaderExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Runtime.CompilerServices; + +namespace Neo.Extensions.IO; + +/// +/// A helper class for serialization of NEO objects. +/// +public static class MemoryReaderExtensions +{ + /// + /// Reads an array from a . + /// + /// The type of the array element. + /// The for reading data. + /// The maximum number of elements in the array. + /// The array read from the . + public static T?[] ReadNullableArray(this ref MemoryReader reader, int max = 0x1000000) + where T : class, ISerializable + { + var array = new T?[reader.ReadVarInt((ulong)max)]; + for (var i = 0; i < array.Length; i++) + array[i] = reader.ReadBoolean() ? reader.ReadSerializable() : null; + return array; + } + + /// + /// Reads an object from a . + /// + /// The type of the object. + /// The for reading data. + /// The object read from the . + public static T ReadSerializable(this ref MemoryReader reader) + where T : ISerializable + { + T obj = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); + obj.Deserialize(ref reader); + return obj; + } + + /// + /// Reads an array from a . + /// + /// The type of the array element. + /// The for reading data. + /// The maximum number of elements in the array. + /// The array read from the . + public static T[] ReadSerializableArray(this ref MemoryReader reader, int max = 0x1000000) + where T : ISerializable + { + var array = new T[reader.ReadVarInt((ulong)max)]; + for (var i = 0; i < array.Length; i++) + { + array[i] = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); + array[i].Deserialize(ref reader); + } + return array; + } +} diff --git a/src/Neo/Extensions/MemoryExtensions.cs b/src/Neo/Extensions/MemoryExtensions.cs new file mode 100644 index 0000000000..f65ff616af --- /dev/null +++ b/src/Neo/Extensions/MemoryExtensions.cs @@ -0,0 +1,73 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MemoryExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using System.Reflection; + +namespace Neo.Extensions; + +public static class MemoryExtensions +{ + /// + /// Converts a byte array to an array. + /// + /// The type of the array element. + /// The byte array to be converted. + /// The maximum number of elements contained in the converted array. + /// The converted array. + public static T[] AsSerializableArray(this ReadOnlyMemory value, int max = 0x1000000) where T : ISerializable, new() + { + if (value.IsEmpty) throw new FormatException("`value` is empty"); + MemoryReader reader = new(value); + return reader.ReadSerializableArray(max); + } + + /// + /// Converts a byte array to an object. + /// + /// The type to convert to. + /// The byte array to be converted. + /// The converted object. + public static T AsSerializable(this ReadOnlyMemory value) + where T : ISerializable + { + if (value.IsEmpty) throw new FormatException("`value` is empty"); + MemoryReader reader = new(value); + return reader.ReadSerializable(); + } + + /// + /// Converts a byte array to an object. + /// + /// The byte array to be converted. + /// The type to convert to. + /// The converted object. + public static ISerializable AsSerializable(this ReadOnlyMemory value, Type type) + { + if (!typeof(ISerializable).GetTypeInfo().IsAssignableFrom(type)) + throw new InvalidCastException($"`{type.Name}` is not assignable from `ISerializable`"); + var serializable = (ISerializable)Activator.CreateInstance(type)!; + MemoryReader reader = new(value); + serializable.Deserialize(ref reader); + return serializable; + } + + /// + /// Gets the size of the specified array encoded in variable-length encoding. + /// + /// The specified array. + /// The size of the array. + public static int GetVarSize(this ReadOnlyMemory value) + { + return value.Length.GetVarSize() + value.Length; + } +} diff --git a/src/Neo/Extensions/SmartContract/ContractParameterExtensions.cs b/src/Neo/Extensions/SmartContract/ContractParameterExtensions.cs new file mode 100644 index 0000000000..142e96b843 --- /dev/null +++ b/src/Neo/Extensions/SmartContract/ContractParameterExtensions.cs @@ -0,0 +1,92 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractParameterExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.SmartContract; +using Neo.VM.Types; +using System.Numerics; +using Array = Neo.VM.Types.Array; + +namespace Neo.Extensions.SmartContract; + +public static class ContractParameterExtensions +{ + /// + /// Converts the to a . + /// + /// The to convert. + /// The converted . + public static StackItem ToStackItem(this ContractParameter parameter) + { + return ToStackItem(parameter, null); + } + + private static StackItem ToStackItem(ContractParameter parameter, List<(StackItem, ContractParameter)>? context) + { + ArgumentNullException.ThrowIfNull(parameter); + if (parameter.Value is null) return StackItem.Null; + StackItem? stackItem = null; + switch (parameter.Type) + { + case ContractParameterType.Array: + if (context is null) + context = []; + else + (stackItem, _) = context.FirstOrDefault(p => ReferenceEquals(p.Item2, parameter)); + if (stackItem is null) + { + stackItem = new Array(((IList)parameter.Value).Select(p => ToStackItem(p, context))); + context.Add((stackItem, parameter)); + } + break; + case ContractParameterType.Map: + if (context is null) + context = []; + else + (stackItem, _) = context.FirstOrDefault(p => ReferenceEquals(p.Item2, parameter)); + if (stackItem is null) + { + Map map = new(); + foreach (var pair in (IList>)parameter.Value) + map[(PrimitiveType)ToStackItem(pair.Key, context)] = ToStackItem(pair.Value, context); + stackItem = map; + context.Add((stackItem, parameter)); + } + break; + case ContractParameterType.Boolean: + stackItem = (bool)parameter.Value; + break; + case ContractParameterType.ByteArray: + case ContractParameterType.Signature: + stackItem = (byte[])parameter.Value; + break; + case ContractParameterType.Integer: + stackItem = (BigInteger)parameter.Value; + break; + case ContractParameterType.Hash160: + stackItem = ((UInt160)parameter.Value).ToArray(); + break; + case ContractParameterType.Hash256: + stackItem = ((UInt256)parameter.Value).ToArray(); + break; + case ContractParameterType.PublicKey: + stackItem = ((ECPoint)parameter.Value).EncodePoint(true); + break; + case ContractParameterType.String: + stackItem = (string)parameter.Value; + break; + default: + throw new ArgumentException($"ContractParameterType({parameter.Type}) is not supported for conversion to StackItem. This parameter type cannot be processed by the virtual machine.", nameof(parameter)); + } + return stackItem; + } +} diff --git a/src/Neo/Extensions/SmartContract/ContractStateExtensions.cs b/src/Neo/Extensions/SmartContract/ContractStateExtensions.cs new file mode 100644 index 0000000000..276727eba9 --- /dev/null +++ b/src/Neo/Extensions/SmartContract/ContractStateExtensions.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractStateExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; + +namespace Neo.Extensions.SmartContract; + +public static class ContractStateExtensions +{ + /// + /// Get Storage value by storage map key. + /// + /// + /// Snapshot of the database. + /// Key in the storage map. + /// Storage value of the item. + /// or is null + public static StorageItem? GetStorage(this ContractState contractState, IReadOnlyStore snapshot, byte[] storageKey) + { + ArgumentNullException.ThrowIfNull(contractState); + + ArgumentNullException.ThrowIfNull(snapshot); + + storageKey ??= []; + + if (snapshot.TryGet(StorageKey.CreateSearchPrefix(contractState.Id, storageKey), out var value)) + { + return value; + } + + return null; + } + + /// + /// All storage items stored in the given contract. + /// + /// + /// Snapshot of the database. + /// Prefix of the key. + /// + /// All storage of the given contract. + /// or is null + public static IEnumerable<(StorageKey Key, StorageItem Value)> FindStorage(this ContractState contractState, IReadOnlyStore snapshot, byte[]? prefix = null, SeekDirection seekDirection = SeekDirection.Forward) + { + ArgumentNullException.ThrowIfNull(contractState); + + ArgumentNullException.ThrowIfNull(snapshot); + + prefix ??= []; + + return snapshot.Find(StorageKey.CreateSearchPrefix(contractState.Id, prefix), seekDirection); + } + + /// + /// All storage items stored in the given contract. + /// + /// + /// Snapshot of the database. + /// Prefix of the key. + /// Id of the contract. + /// + /// All storage of the given contract. + /// is null + public static IEnumerable<(StorageKey Key, StorageItem Value)> FindContractStorage(this ContractManagement contractManagement, IReadOnlyStore snapshot, int contractId, byte[]? prefix = null, SeekDirection seekDirection = SeekDirection.Forward) + { + ArgumentNullException.ThrowIfNull(snapshot); + + prefix ??= []; + + return snapshot.Find(StorageKey.CreateSearchPrefix(contractId, prefix), seekDirection); + } +} diff --git a/src/Neo/Extensions/SmartContract/GasTokenExtensions.cs b/src/Neo/Extensions/SmartContract/GasTokenExtensions.cs new file mode 100644 index 0000000000..bb54d74bf6 --- /dev/null +++ b/src/Neo/Extensions/SmartContract/GasTokenExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// GasTokenExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using System.Numerics; + +namespace Neo.Extensions.SmartContract; + +public static class GasTokenExtensions +{ + public static IEnumerable<(UInt160 Address, BigInteger Balance)> GetAccounts(this GasToken gasToken, IReadOnlyStore snapshot) + { + ArgumentNullException.ThrowIfNull(gasToken); + + ArgumentNullException.ThrowIfNull(snapshot); + + StorageKey kb = new KeyBuilder(gasToken.Id, GasToken.Prefix_Account); + var kbLength = kb.Length; + + foreach (var (key, value) in snapshot.Find(kb, SeekDirection.Forward)) + { + var keyBytes = key.ToArray(); + var addressHash = new UInt160(keyBytes.AsSpan(kbLength)); + yield return new(addressHash, value.GetInteroperable().Balance); + } + } +} diff --git a/src/Neo/Extensions/SmartContract/NeoTokenExtensions.cs b/src/Neo/Extensions/SmartContract/NeoTokenExtensions.cs new file mode 100644 index 0000000000..47f3216f7d --- /dev/null +++ b/src/Neo/Extensions/SmartContract/NeoTokenExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NeoTokenExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using System.Numerics; + +namespace Neo.Extensions.SmartContract; + +public static class NeoTokenExtensions +{ + public static IEnumerable<(UInt160 Address, BigInteger Balance)> GetAccounts(this NeoToken neoToken, IReadOnlyStore snapshot) + { + ArgumentNullException.ThrowIfNull(neoToken); + + ArgumentNullException.ThrowIfNull(snapshot); + + StorageKey kb = new KeyBuilder(neoToken.Id, NeoToken.Prefix_Account); + var kbLength = kb.Length; + + foreach (var (key, value) in snapshot.Find(kb, SeekDirection.Forward)) + { + var keyBytes = key.ToArray(); + var addressHash = new UInt160(keyBytes.AsSpan(kbLength)); + yield return new(addressHash, value.GetInteroperable().Balance); + } + } +} diff --git a/src/Neo/Extensions/SpanExtensions.cs b/src/Neo/Extensions/SpanExtensions.cs new file mode 100644 index 0000000000..aec05b9723 --- /dev/null +++ b/src/Neo/Extensions/SpanExtensions.cs @@ -0,0 +1,81 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// SpanExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using K4os.Compression.LZ4; +using System.Buffers.Binary; + +namespace Neo.Extensions; + +public static class SpanExtensions +{ + /// + /// Compresses the specified data using the LZ4 algorithm. + /// + /// The data to be compressed. + /// The compressed data. + public static ReadOnlyMemory CompressLz4(this ReadOnlySpan data) + { + var maxLength = LZ4Codec.MaximumOutputSize(data.Length); + var buffer = new byte[sizeof(uint) + maxLength]; + BinaryPrimitives.WriteInt32LittleEndian(buffer, data.Length); + var length = LZ4Codec.Encode(data, buffer.AsSpan(sizeof(uint))); + return buffer.AsMemory(0, sizeof(uint) + length); + } + + /// + /// Compresses the specified data using the LZ4 algorithm. + /// + /// The data to be compressed. + /// The compressed data. + public static ReadOnlyMemory CompressLz4(this Span data) + { + var maxLength = LZ4Codec.MaximumOutputSize(data.Length); + var buffer = new byte[sizeof(uint) + maxLength]; + BinaryPrimitives.WriteInt32LittleEndian(buffer, data.Length); + var length = LZ4Codec.Encode(data, buffer.AsSpan(sizeof(uint))); + return buffer.AsMemory(0, sizeof(uint) + length); + } + + /// + /// Decompresses the specified data using the LZ4 algorithm. + /// + /// The compressed data. + /// The maximum data size after decompression. + /// The original data. + public static byte[] DecompressLz4(this ReadOnlySpan data, int maxOutput) + { + var length = BinaryPrimitives.ReadInt32LittleEndian(data); + if (length < 0 || length > maxOutput) throw new FormatException($"`length`({length}) is out of range [0, {maxOutput}]"); + var result = new byte[length]; + + var decoded = LZ4Codec.Decode(data[4..], result); + if (decoded != length) + throw new FormatException($"`length`({length}) does not match the decompressed data length({decoded})"); + return result; + } + + /// + /// Decompresses the specified data using the LZ4 algorithm. + /// + /// The compressed data. + /// The maximum data size after decompression. + /// The original data. + public static byte[] DecompressLz4(this Span data, int maxOutput) + { + var length = BinaryPrimitives.ReadInt32LittleEndian(data); + if (length < 0 || length > maxOutput) throw new FormatException($"`length`({length}) is out of range [0, {maxOutput}]"); + var result = new byte[length]; + var decoded = LZ4Codec.Decode(data[4..], result); + if (decoded != length) + throw new FormatException($"`length`({length}) does not match the decompressed data length({decoded})"); + return result; + } +} diff --git a/src/Neo/Extensions/UInt160Extensions.cs b/src/Neo/Extensions/UInt160Extensions.cs new file mode 100644 index 0000000000..edac87fdb2 --- /dev/null +++ b/src/Neo/Extensions/UInt160Extensions.cs @@ -0,0 +1,32 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UInt160Extensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.VM; +using Neo.VM; + +namespace Neo.Extensions; + +public static class UInt160Extensions +{ + /// + /// Generates the script for calling a contract dynamically. + /// + /// The hash of the contract to be called. + /// The method to be called in the contract. + /// The arguments for calling the contract. + /// The generated script. + public static byte[] MakeScript(this UInt160 scriptHash, string method, params object?[] args) + { + using ScriptBuilder sb = new(); + sb.EmitDynamicCall(scriptHash, method, args); + return sb.ToArray(); + } +} diff --git a/src/Neo/Extensions/VM/EvaluationStackExtensions.cs b/src/Neo/Extensions/VM/EvaluationStackExtensions.cs new file mode 100644 index 0000000000..7f812526ea --- /dev/null +++ b/src/Neo/Extensions/VM/EvaluationStackExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// EvaluationStackExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.Extensions.VM; + +public static class EvaluationStackExtensions +{ + /// + /// Converts the to a JSON object. + /// + /// The to convert. + /// The maximum size in bytes of the result. + /// The represented by a JSON object. + public static JArray ToJson(this EvaluationStack stack, int maxSize = int.MaxValue) + { + if (maxSize <= 0) throw new ArgumentOutOfRangeException(nameof(maxSize), "must be positive"); + maxSize -= 2/*[]*/+ Math.Max(0, (stack.Count - 1))/*,*/; + JArray result = []; + foreach (var item in stack) + result.Add(item.ToJson(null, ref maxSize)); + if (maxSize < 0) throw new InvalidOperationException("Max size reached."); + return result; + } +} diff --git a/src/Neo/Extensions/VM/ScriptBuilderExtensions.cs b/src/Neo/Extensions/VM/ScriptBuilderExtensions.cs new file mode 100644 index 0000000000..b1ff351b67 --- /dev/null +++ b/src/Neo/Extensions/VM/ScriptBuilderExtensions.cs @@ -0,0 +1,303 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ScriptBuilderExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.IO; +using Neo.SmartContract; +using Neo.VM; +using System.Numerics; + +namespace Neo.Extensions.VM; + +public static class ScriptBuilderExtensions +{ + /// + /// Emits the opcodes for creating an array. + /// + /// The type of the elements of the array. + /// The to be used. + /// The elements of the array. + /// The same instance as . + public static ScriptBuilder CreateArray(this ScriptBuilder builder, IReadOnlyList? list = null) + { + if (list is null || list.Count == 0) + return builder.Emit(OpCode.NEWARRAY0); + for (var i = list.Count - 1; i >= 0; i--) + builder.EmitPush(list[i]); + builder.EmitPush(list.Count); + return builder.Emit(OpCode.PACK); + } + + /// + /// Emits the opcodes for creating a map. + /// + /// The type of the key of the map. + /// The type of the value of the map. + /// The to be used. + /// The key/value pairs of the map. + /// The same instance as . + public static ScriptBuilder CreateMap(this ScriptBuilder builder, IEnumerable> map) + where TKey : notnull + where TValue : notnull + { + var count = map.Count(); + + if (count == 0) + return builder.Emit(OpCode.NEWMAP); + + foreach (var (key, value) in map.Reverse()) + { + builder.EmitPush(value); + builder.EmitPush(key); + } + builder.EmitPush(count); + return builder.Emit(OpCode.PACKMAP); + } + + /// + /// Emits the opcodes for creating a map. + /// + /// The type of the key of the map. + /// The type of the value of the map. + /// The to be used. + /// The key/value pairs of the map. + /// The same instance as . + public static ScriptBuilder CreateMap(this ScriptBuilder builder, IReadOnlyDictionary map) + where TKey : notnull + where TValue : notnull + { + if (map.Count == 0) + return builder.Emit(OpCode.NEWMAP); + + foreach (var (key, value) in map.Reverse()) + { + builder.EmitPush(value); + builder.EmitPush(key); + } + builder.EmitPush(map.Count); + return builder.Emit(OpCode.PACKMAP); + } + + /// + /// Emits the opcodes for creating a struct. + /// + /// The type of the property. + /// The to be used. + /// The list of properties. + /// The same instance as . + public static ScriptBuilder CreateStruct(this ScriptBuilder builder, IReadOnlyList array) + where T : notnull + { + if (array.Count == 0) + return builder.Emit(OpCode.NEWSTRUCT0); + for (var i = array.Count - 1; i >= 0; i--) + builder.EmitPush(array[i]); + builder.EmitPush(array.Count); + return builder.Emit(OpCode.PACKSTRUCT); + } + + /// + /// Emits the specified opcodes. + /// + /// The to be used. + /// The opcodes to emit. + /// The same instance as . + public static ScriptBuilder Emit(this ScriptBuilder builder, params OpCode[] ops) + { + foreach (var op in ops) + builder.Emit(op); + return builder; + } + + /// + /// Emits the opcodes for calling a contract dynamically. + /// + /// The to be used. + /// The hash of the contract to be called. + /// The method to be called in the contract. + /// The arguments for calling the contract. + /// The same instance as . + public static ScriptBuilder EmitDynamicCall(this ScriptBuilder builder, UInt160 scriptHash, string method, params object?[] args) + { + return EmitDynamicCall(builder, scriptHash, method, CallFlags.All, args); + } + + /// + /// Emits the opcodes for calling a contract dynamically. + /// + /// The to be used. + /// The hash of the contract to be called. + /// The method to be called in the contract. + /// The for calling the contract. + /// The arguments for calling the contract. + /// The same instance as . + public static ScriptBuilder EmitDynamicCall(this ScriptBuilder builder, UInt160 scriptHash, string method, CallFlags flags, params object?[] args) + { + builder.CreateArray(args); + builder.EmitPush(flags); + builder.EmitPush(method); + builder.EmitPush(scriptHash); + builder.EmitSysCall(ApplicationEngine.System_Contract_Call); + return builder; + } + + /// + /// Emits the opcodes for pushing the specified data onto the stack. + /// + /// The to be used. + /// The data to be pushed. + /// The same instance as . + public static ScriptBuilder EmitPush(this ScriptBuilder builder, ISerializable data) + { + return builder.EmitPush(data.ToArray()); + } + + /// + /// Emits the opcodes for pushing the specified data onto the stack. + /// + /// The to be used. + /// The data to be pushed. + /// The same instance as . + public static ScriptBuilder EmitPush(this ScriptBuilder builder, ContractParameter parameter) + { + if (parameter.Value is null) + builder.Emit(OpCode.PUSHNULL); + else + switch (parameter.Type) + { + case ContractParameterType.Signature: + case ContractParameterType.ByteArray: + builder.EmitPush((byte[])parameter.Value); + break; + case ContractParameterType.Boolean: + builder.EmitPush((bool)parameter.Value); + break; + case ContractParameterType.Integer: + if (parameter.Value is BigInteger bi) + builder.EmitPush(bi); + else + builder.EmitPush((BigInteger)typeof(BigInteger).GetConstructor([parameter.Value.GetType()])!.Invoke([parameter.Value])); + break; + case ContractParameterType.Hash160: + builder.EmitPush((UInt160)parameter.Value); + break; + case ContractParameterType.Hash256: + builder.EmitPush((UInt256)parameter.Value); + break; + case ContractParameterType.PublicKey: + builder.EmitPush((ECPoint)parameter.Value); + break; + case ContractParameterType.String: + builder.EmitPush((string)parameter.Value); + break; + case ContractParameterType.Array: + { + var parameters = (IList)parameter.Value; + for (var i = parameters.Count - 1; i >= 0; i--) + builder.EmitPush(parameters[i]); + builder.EmitPush(parameters.Count); + builder.Emit(OpCode.PACK); + } + break; + case ContractParameterType.Map: + { + var pairs = (IList>)parameter.Value; + builder.CreateMap(pairs); + } + break; + default: + throw new ArgumentException($"Unsupported parameter type: {parameter.Type}. This parameter type cannot be converted to a stack item for script execution.", nameof(parameter)); + } + return builder; + } + + /// + /// Emits the opcodes for pushing the specified data onto the stack. + /// + /// The to be used. + /// The data to be pushed. + /// The same instance as . + public static ScriptBuilder EmitPush(this ScriptBuilder builder, object? obj) + { + switch (obj) + { + case bool data: + builder.EmitPush(data); + break; + case byte[] data: + builder.EmitPush(data); + break; + case string data: + builder.EmitPush(data); + break; + case BigInteger data: + builder.EmitPush(data); + break; + case ISerializable data: + builder.EmitPush(data); + break; + case sbyte data: + builder.EmitPush(data); + break; + case byte data: + builder.EmitPush(data); + break; + case short data: + builder.EmitPush(data); + break; + case char data: + builder.EmitPush(data); + break; + case ushort data: + builder.EmitPush(data); + break; + case int data: + builder.EmitPush(data); + break; + case uint data: + builder.EmitPush(data); + break; + case long data: + builder.EmitPush(data); + break; + case ulong data: + builder.EmitPush(data); + break; + case Enum data: + builder.EmitPush(BigInteger.Parse(data.ToString("d"))); + break; + case ContractParameter data: + builder.EmitPush(data); + break; + case null: + builder.Emit(OpCode.PUSHNULL); + break; + default: + throw new ArgumentException($"Unsupported object type: {obj.GetType()}. This object type cannot be converted to a stack item for script execution.", nameof(obj)); + } + return builder; + } + + /// + /// Emits the opcodes for invoking an interoperable service. + /// + /// The to be used. + /// The hash of the interoperable service. + /// The arguments for calling the interoperable service. + /// The same instance as . + public static ScriptBuilder EmitSysCall(this ScriptBuilder builder, uint method, params object[] args) + { + for (var i = args.Length - 1; i >= 0; i--) + EmitPush(builder, args[i]); + return builder.EmitSysCall(method); + } +} diff --git a/src/Neo/Extensions/VM/StackItemExtensions.cs b/src/Neo/Extensions/VM/StackItemExtensions.cs new file mode 100644 index 0000000000..e353911f84 --- /dev/null +++ b/src/Neo/Extensions/VM/StackItemExtensions.cs @@ -0,0 +1,192 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StackItemExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Types; +using Array = Neo.VM.Types.Array; +using Boolean = Neo.VM.Types.Boolean; +using Buffer = Neo.VM.Types.Buffer; + +namespace Neo.Extensions.VM; + +public static class StackItemExtensions +{ + /// + /// Converts the to a JSON object. + /// + /// The to convert. + /// The maximum size in bytes of the result. + /// The represented by a JSON object. + public static JObject ToJson(this StackItem item, int maxSize = int.MaxValue) + { + return ToJson(item, null, ref maxSize); + } + + public static JObject ToJson(this StackItem item, HashSet? context, ref int maxSize) + { + JObject json = new() + { + ["type"] = item.Type + }; + JToken? value = null; + maxSize -= 11/*{"type":""}*/+ item.Type.ToString().Length; + switch (item) + { + case Array array: + { + context ??= new(ReferenceEqualityComparer.Instance); + if (!context.Add(array)) throw new InvalidOperationException("Circular reference."); + maxSize -= 2/*[]*/+ Math.Max(0, (array.Count - 1))/*,*/; + JArray a = []; + foreach (var stackItem in array) + a.Add(ToJson(stackItem, context, ref maxSize)); + value = a; + if (!context.Remove(array)) throw new InvalidOperationException("Circular reference."); + break; + } + case Boolean boolean: + { + var b = boolean.GetBoolean(); + maxSize -= b ? 4/*true*/: 5/*false*/; + value = b; + break; + } + case Buffer _: + case ByteString _: + { + var s = Convert.ToBase64String(item.GetSpan()); + maxSize -= 2/*""*/+ s.Length; + value = s; + break; + } + case Integer integer: + { + var s = integer.GetInteger().ToString(); + maxSize -= 2/*""*/+ s.Length; + value = s; + break; + } + case Map map: + { + context ??= new(ReferenceEqualityComparer.Instance); + if (!context.Add(map)) throw new InvalidOperationException("Circular reference."); + maxSize -= 2/*[]*/+ Math.Max(0, (map.Count - 1))/*,*/; + JArray a = new(); + foreach (var (k, v) in map) + { + maxSize -= 17/*{"key":,"value":}*/; + JObject i = new() + { + ["key"] = ToJson(k, context, ref maxSize), + ["value"] = ToJson(v, context, ref maxSize) + }; + a.Add(i); + } + value = a; + if (!context.Remove(map)) throw new InvalidOperationException("Circular reference."); + break; + } + case Pointer pointer: + { + maxSize -= pointer.Position.ToString().Length; + value = pointer.Position; + break; + } + } + if (value is not null) + { + maxSize -= 9/*,"value":*/; + json["value"] = value; + } + if (maxSize < 0) throw new InvalidOperationException("Max size reached."); + return json; + } + + /// + /// Converts the to a . + /// + /// The to convert. + /// The converted . + public static ContractParameter ToParameter(this StackItem item) + { + return ToParameter(item, null); + } + + public static ContractParameter ToParameter(this StackItem item, List<(StackItem, ContractParameter)>? context) + { + ArgumentNullException.ThrowIfNull(item); + ContractParameter? parameter = null; + switch (item) + { + case Array array: + if (context is null) + context = []; + else + (_, parameter) = context.FirstOrDefault(p => ReferenceEquals(p.Item1, item)); + if (parameter is null) + { + parameter = new ContractParameter { Type = ContractParameterType.Array }; + context.Add((item, parameter)); + parameter.Value = array.Select(p => ToParameter(p, context)).ToList(); + } + break; + case Map map: + if (context is null) + context = []; + else + (_, parameter) = context.FirstOrDefault(p => ReferenceEquals(p.Item1, item)); + if (parameter is null) + { + parameter = new ContractParameter { Type = ContractParameterType.Map }; + context.Add((item, parameter)); + parameter.Value = map.Select(p => new KeyValuePair(ToParameter(p.Key, context), ToParameter(p.Value, context))).ToList(); + } + break; + case Boolean _: + parameter = new ContractParameter + { + Type = ContractParameterType.Boolean, + Value = item.GetBoolean() + }; + break; + case ByteString array: + parameter = new ContractParameter + { + Type = ContractParameterType.ByteArray, + Value = array.GetSpan().ToArray() + }; + break; + case Integer i: + parameter = new ContractParameter + { + Type = ContractParameterType.Integer, + Value = i.GetInteger() + }; + break; + case InteropInterface _: + parameter = new ContractParameter + { + Type = ContractParameterType.InteropInterface + }; + break; + case Null _: + parameter = new ContractParameter + { + Type = ContractParameterType.Any + }; + break; + default: + throw new ArgumentException($"StackItemType({item.Type}) is not supported for conversion to ContractParameter. This stack item type cannot be converted to a contract parameter.", nameof(item)); + } + return parameter; + } +} diff --git a/src/Neo/Hardfork.cs b/src/Neo/Hardfork.cs new file mode 100644 index 0000000000..1283f185f8 --- /dev/null +++ b/src/Neo/Hardfork.cs @@ -0,0 +1,16 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Hardfork.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +public enum Hardfork : byte +{ +} diff --git a/src/Neo/IEventHandlers/ICommittedHandler.cs b/src/Neo/IEventHandlers/ICommittedHandler.cs new file mode 100644 index 0000000000..46fa67c8da --- /dev/null +++ b/src/Neo/IEventHandlers/ICommittedHandler.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ICommittedHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.IEventHandlers; + +public interface ICommittedHandler +{ + /// + /// This is the handler of Commited event from + /// Triggered after a new block is Commited, and state has being updated. + /// + /// The object. + /// The committed . + void Blockchain_Committed_Handler(NeoSystem system, Block block); +} diff --git a/src/Neo/IEventHandlers/ICommittingHandler.cs b/src/Neo/IEventHandlers/ICommittingHandler.cs new file mode 100644 index 0000000000..85684882ac --- /dev/null +++ b/src/Neo/IEventHandlers/ICommittingHandler.cs @@ -0,0 +1,29 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ICommittingHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; + +namespace Neo.IEventHandlers; + +public interface ICommittingHandler +{ + /// + /// This is the handler of Committing event from + /// Triggered when a new block is committing, and the state is still in the cache. + /// + /// The instance associated with the event. + /// The block that is being committed. + /// The current data snapshot. + /// A list of executed applications associated with the block. + void Blockchain_Committing_Handler(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList); +} diff --git a/src/Neo/IEventHandlers/ILogHandler.cs b/src/Neo/IEventHandlers/ILogHandler.cs new file mode 100644 index 0000000000..9b4eb9d7a6 --- /dev/null +++ b/src/Neo/IEventHandlers/ILogHandler.cs @@ -0,0 +1,25 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ILogHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.IEventHandlers; + +public interface ILogHandler +{ + /// + /// The handler of Log event from the . + /// Triggered when a contract calls System.Runtime.Log. + /// + /// The source of the event. + /// The arguments of the log. + void ApplicationEngine_Log_Handler(ApplicationEngine sender, LogEventArgs logEventArgs); +} diff --git a/src/Neo/IEventHandlers/ILoggingHandler.cs b/src/Neo/IEventHandlers/ILoggingHandler.cs new file mode 100644 index 0000000000..b6bd5c896f --- /dev/null +++ b/src/Neo/IEventHandlers/ILoggingHandler.cs @@ -0,0 +1,24 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ILoggingHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.IEventHandlers; + +public interface ILoggingHandler +{ + /// + /// The handler of Logging event from + /// Triggered when a new log is added by calling + /// + /// The source of the log. Used to identify the producer of the log. + /// The level of the log. + /// The message of the log. + void Utility_Logging_Handler(string source, LogLevel level, object message); +} diff --git a/src/Neo/IEventHandlers/IMessageReceivedHandler.cs b/src/Neo/IEventHandlers/IMessageReceivedHandler.cs new file mode 100644 index 0000000000..d0e4d839a2 --- /dev/null +++ b/src/Neo/IEventHandlers/IMessageReceivedHandler.cs @@ -0,0 +1,25 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IMessageReceivedHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.IEventHandlers; + +public interface IMessageReceivedHandler +{ + /// + /// The handler of MessageReceived event from + /// Triggered when a new message is received from a peer + /// + /// The object + /// The current node received from a peer + bool RemoteNode_MessageReceived_Handler(NeoSystem system, Message message); +} diff --git a/src/Neo/IEventHandlers/INotifyHandler.cs b/src/Neo/IEventHandlers/INotifyHandler.cs new file mode 100644 index 0000000000..aeee75d561 --- /dev/null +++ b/src/Neo/IEventHandlers/INotifyHandler.cs @@ -0,0 +1,25 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// INotifyHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.IEventHandlers; + +public interface INotifyHandler +{ + /// + /// The handler of Notify event from + /// Triggered when a contract calls System.Runtime.Notify. + /// + /// The source of the event. + /// The arguments of the notification. + void ApplicationEngine_Notify_Handler(object sender, NotifyEventArgs notifyEventArgs); +} diff --git a/src/Neo/IEventHandlers/IServiceAddedHandler.cs b/src/Neo/IEventHandlers/IServiceAddedHandler.cs new file mode 100644 index 0000000000..31ed88ab31 --- /dev/null +++ b/src/Neo/IEventHandlers/IServiceAddedHandler.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IServiceAddedHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.IEventHandlers; + +public interface IServiceAddedHandler +{ + /// + /// The handler of ServiceAdded event from the . + /// Triggered when a service is added to the . + /// + /// The source of the event. + /// The service added. + void NeoSystem_ServiceAdded_Handler(object sender, object service); +} diff --git a/src/Neo/IEventHandlers/ITransactionAddedHandler.cs b/src/Neo/IEventHandlers/ITransactionAddedHandler.cs new file mode 100644 index 0000000000..85a9d51858 --- /dev/null +++ b/src/Neo/IEventHandlers/ITransactionAddedHandler.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ITransactionAddedHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.IEventHandlers; + +public interface ITransactionAddedHandler +{ + /// + /// The handler of TransactionAdded event from the . + /// Triggered when a transaction is added to the . + /// + /// The source of the event + /// The transaction added to the memory pool . + void MemoryPool_TransactionAdded_Handler(object sender, Transaction tx); +} diff --git a/src/Neo/IEventHandlers/ITransactionRemovedHandler.cs b/src/Neo/IEventHandlers/ITransactionRemovedHandler.cs new file mode 100644 index 0000000000..e912ad7512 --- /dev/null +++ b/src/Neo/IEventHandlers/ITransactionRemovedHandler.cs @@ -0,0 +1,25 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ITransactionRemovedHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.IEventHandlers; + +public interface ITransactionRemovedHandler +{ + /// + /// Handler of TransactionRemoved event from + /// Triggered when a transaction is removed to the . + /// + /// The source of the event + /// The arguments of event that removes a transaction from the + void MemoryPool_TransactionRemoved_Handler(object sender, TransactionRemovedEventArgs tx); +} diff --git a/src/Neo/IEventHandlers/IWalletChangedHandler.cs b/src/Neo/IEventHandlers/IWalletChangedHandler.cs new file mode 100644 index 0000000000..4e7a580941 --- /dev/null +++ b/src/Neo/IEventHandlers/IWalletChangedHandler.cs @@ -0,0 +1,25 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IWalletChangedHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Wallets; + +namespace Neo.IEventHandlers; + +public interface IWalletChangedHandler +{ + /// + /// The handler of WalletChanged event from the . + /// Triggered when a new wallet is assigned to the node. + /// + /// The source of the event + /// The new wallet being assigned to the system. + void IWalletProvider_WalletChanged_Handler(object sender, Wallet wallet); +} diff --git a/src/Neo/IO/Actors/PriorityMailbox.cs b/src/Neo/IO/Actors/PriorityMailbox.cs new file mode 100644 index 0000000000..186f9527f7 --- /dev/null +++ b/src/Neo/IO/Actors/PriorityMailbox.cs @@ -0,0 +1,28 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// PriorityMailbox.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Configuration; +using Akka.Dispatch; +using Akka.Dispatch.MessageQueues; +using System.Collections; + +namespace Neo.IO.Actors; + +internal abstract class PriorityMailbox + (Settings settings, Config config) : MailboxType(settings, config), IProducesMessageQueue +{ + public override IMessageQueue Create(IActorRef owner, ActorSystem system) => + new PriorityMessageQueue(ShallDrop, IsHighPriority); + + internal protected virtual bool IsHighPriority(object message) => false; + internal protected virtual bool ShallDrop(object message, IEnumerable queue) => false; +} diff --git a/src/Neo/IO/Actors/PriorityMessageQueue.cs b/src/Neo/IO/Actors/PriorityMessageQueue.cs new file mode 100644 index 0000000000..6613f717fa --- /dev/null +++ b/src/Neo/IO/Actors/PriorityMessageQueue.cs @@ -0,0 +1,57 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// PriorityMessageQueue.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Dispatch; +using Akka.Dispatch.MessageQueues; +using System.Collections; +using System.Collections.Concurrent; + +namespace Neo.IO.Actors; + +internal class PriorityMessageQueue(Func dropper, Func priorityGenerator) + : IMessageQueue, IUnboundedMessageQueueSemantics +{ + private readonly ConcurrentQueue _high = new(); + private readonly ConcurrentQueue _low = new(); + private readonly Func _dropper = dropper; + private readonly Func _priorityGenerator = priorityGenerator; + private int _idle = 1; + + public bool HasMessages => !_high.IsEmpty || !_low.IsEmpty; + public int Count => _high.Count + _low.Count; + + public void CleanUp(IActorRef owner, IMessageQueue deadletters) + { + } + + public void Enqueue(IActorRef receiver, Envelope envelope) + { + Interlocked.Increment(ref _idle); + if (envelope.Message is Idle) return; + if (_dropper(envelope.Message, _high.Concat(_low).Select(p => p.Message))) + return; + var queue = _priorityGenerator(envelope.Message) ? _high : _low; + queue.Enqueue(envelope); + } + + public bool TryDequeue(out Envelope envelope) + { + if (_high.TryDequeue(out envelope)) return true; + if (_low.TryDequeue(out envelope)) return true; + if (Interlocked.Exchange(ref _idle, 0) > 0) + { + envelope = new Envelope(Idle.Instance, ActorRefs.NoSender); + return true; + } + return false; + } +} diff --git a/src/Neo/IO/Caching/ECPointCache.cs b/src/Neo/IO/Caching/ECPointCache.cs new file mode 100644 index 0000000000..2b1bc92528 --- /dev/null +++ b/src/Neo/IO/Caching/ECPointCache.cs @@ -0,0 +1,25 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ECPointCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO.Caching; + +internal class ECPointCache : FIFOCache +{ + public ECPointCache(int maxCapacity) + : base(maxCapacity, ByteArrayEqualityComparer.Default) { } + + protected override byte[] GetKeyForItem(ECPoint item) + { + return item.EncodePoint(true); + } +} diff --git a/src/Neo/IO/Caching/ReflectionCache.cs b/src/Neo/IO/Caching/ReflectionCache.cs new file mode 100644 index 0000000000..80970f1c2e --- /dev/null +++ b/src/Neo/IO/Caching/ReflectionCache.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ReflectionCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using System.Reflection; + +namespace Neo.IO.Caching; + +internal static class ReflectionCache + where T : Enum +{ + private static readonly Dictionary s_dictionary = []; + + public static int Count => s_dictionary.Count; + + static ReflectionCache() + { + foreach (var field in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + // Get attribute + var attribute = field.GetCustomAttribute(); + if (attribute == null) continue; + + // Append to cache + var key = (T?)field.GetValue(null); + if (key == null) continue; + s_dictionary.Add(key, attribute.Type); + } + } + + public static object? CreateInstance(T key, object? def = null) + { + // Get Type from cache + if (s_dictionary.TryGetValue(key, out var t)) + return Activator.CreateInstance(t); + + // return null + return def; + } + + public static ISerializable? CreateSerializable(T key, ReadOnlyMemory data) + { + if (s_dictionary.TryGetValue(key, out var t)) + return data.AsSerializable(t); + + return null; + } +} diff --git a/src/Neo/IO/Caching/RelayCache.cs b/src/Neo/IO/Caching/RelayCache.cs new file mode 100644 index 0000000000..5ae130662f --- /dev/null +++ b/src/Neo/IO/Caching/RelayCache.cs @@ -0,0 +1,22 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// RelayCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P.Payloads; + +namespace Neo.IO.Caching; + +internal class RelayCache(int maxCapacity) : FIFOCache(maxCapacity) +{ + protected override UInt256 GetKeyForItem(IInventory item) + { + return item.Hash; + } +} diff --git a/src/Neo/Ledger/Blockchain.ApplicationExecuted.cs b/src/Neo/Ledger/Blockchain.ApplicationExecuted.cs new file mode 100644 index 0000000000..46122ed13f --- /dev/null +++ b/src/Neo/Ledger/Blockchain.ApplicationExecuted.cs @@ -0,0 +1,69 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Blockchain.ApplicationExecuted.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.VM; +using Neo.VM.Types; + +namespace Neo.Ledger; + +partial class Blockchain +{ + partial class ApplicationExecuted + { + /// + /// The transaction that contains the executed script. This field could be if the contract is invoked by system. + /// + public Transaction? Transaction { get; } + + /// + /// The trigger of the execution. + /// + public TriggerType Trigger { get; } + + /// + /// The state of the virtual machine after the contract is executed. + /// + public VMState VMState { get; } + + /// + /// The exception that caused the execution to terminate abnormally. This field could be if the execution ends normally. + /// + public Exception? Exception { get; } + + /// + /// GAS spent to execute. + /// + public long GasConsumed { get; } + + /// + /// Items on the stack of the virtual machine after execution. + /// + public StackItem[] Stack { get; } + + /// + /// The notifications sent during the execution. + /// + public NotifyEventArgs[] Notifications { get; } + + internal ApplicationExecuted(ApplicationEngine engine) + { + Transaction = engine.ScriptContainer as Transaction; + Trigger = engine.Trigger; + VMState = engine.State; + GasConsumed = engine.FeeConsumed; + Exception = engine.FaultException; + Stack = [.. engine.ResultStack]; + Notifications = [.. engine.Notifications]; + } + } +} diff --git a/src/Neo/Ledger/Blockchain.cs b/src/Neo/Ledger/Blockchain.cs new file mode 100644 index 0000000000..d961208569 --- /dev/null +++ b/src/Neo/Ledger/Blockchain.cs @@ -0,0 +1,578 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// 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 Akka.Actor; +using Akka.Configuration; +using Akka.IO; +using Neo.IO.Actors; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.Plugins; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Neo.Ledger; + +public delegate void CommittingHandler(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList); +public delegate void CommittedHandler(NeoSystem system, Block block); + +/// +/// Actor used to verify and relay . +/// +public sealed partial class Blockchain : UntypedActor +{ + /// + /// Sent by the when a smart contract is executed. + /// + public partial class ApplicationExecuted { } + + /// + /// Sent by the when a is persisted. + /// + /// The that is persisted. + public record PersistCompleted(Block Block); + + /// + /// Sent to the when importing blocks. + /// + /// The blocks to be imported. + /// Indicates whether the blocks need to be verified when importing. + public record Import(IEnumerable Blocks, bool Verify = true); + + /// + /// Sent by the when the import is complete. + /// + public record ImportCompleted; + + /// + /// Sent to the when the consensus is filling the memory pool. + /// + /// The transactions to be sent. + public record FillMemoryPool(IEnumerable Transactions); + + /// + /// Sent by the when the memory pool is filled. + /// + public record FillCompleted; + + /// + /// Sent to the when inventories need to be re-verified. + /// + /// The inventories to be re-verified. + public record Reverify(IReadOnlyList Inventories); + + /// + /// Sent by the when an is relayed. + /// + /// The that is relayed. + /// The result. + public record RelayResult(IInventory Inventory, VerifyResult Result); + + internal record Initialize; + private class UnverifiedBlocksList + { + public List Blocks { get; } = []; + public HashSet Nodes { get; } = []; + } + + public static event CommittingHandler? Committing; + public static event CommittedHandler? Committed; + + private static readonly Script s_onPersistScript, s_postPersistScript; + private const int MaxTxToReverifyPerIdle = 10; + private readonly NeoSystem _system; + private readonly Dictionary _blockCache = []; + private readonly Dictionary _blockCacheUnverified = []; + private ImmutableHashSet? _extensibleWitnessWhiteList; + + static Blockchain() + { + using (ScriptBuilder sb = new()) + { + sb.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist); + s_onPersistScript = sb.ToArray(); + } + using (ScriptBuilder sb = new()) + { + sb.EmitSysCall(ApplicationEngine.System_Contract_NativePostPersist); + s_postPersistScript = sb.ToArray(); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The object that contains the . + public Blockchain(NeoSystem system) + { + _system = system; + } + + private void OnImport(IEnumerable blocks, bool verify) + { + var currentHeight = NativeContract.Ledger.CurrentIndex(_system.StoreView); + foreach (var block in blocks) + { + if (block.Index <= currentHeight) continue; + if (block.Index != currentHeight + 1) + throw new InvalidOperationException(); + if (verify && !block.Verify(_system.Settings, _system.StoreView)) + throw new InvalidOperationException(); + Persist(block); + ++currentHeight; + } + Sender.Tell(new ImportCompleted()); + } + + private void AddUnverifiedBlockToCache(Block block) + { + // Check if any block proposal for height `block.Index` exists + if (!_blockCacheUnverified.TryGetValue(block.Index, out var list)) + { + // There are no blocks, a new UnverifiedBlocksList is created and, consequently, the current block is added to the list + list = new UnverifiedBlocksList(); + _blockCacheUnverified.Add(block.Index, list); + } + else + { + // Check if any block with the hash being added already exists on possible candidates to be processed + foreach (var unverifiedBlock in list.Blocks) + { + if (block.Hash == unverifiedBlock.Hash) + return; + } + + if (!list.Nodes.Add(Sender)) + { + // Same index with different hash + Sender.Tell(Tcp.Abort.Instance); + return; + } + } + + list.Blocks.Add(block); + } + + private void OnFillMemoryPool(IEnumerable transactions) + { + // Invalidate all the transactions in the memory pool, to avoid any failures when adding new transactions. + _system.MemPool.InvalidateAllTransactions(); + + var snapshot = _system.StoreView; + + // Add the transactions to the memory pool + foreach (var tx in transactions) + { + if (NativeContract.Ledger.ContainsTransaction(snapshot, tx.Hash)) + continue; + if (NativeContract.Ledger.ContainsConflictHash(snapshot, tx.Hash, tx.Signers.Select(s => s.Account), _system.Settings.MaxTraceableBlocks)) + continue; + // First remove the tx if it is unverified in the pool. + _system.MemPool.TryRemoveUnVerified(tx.Hash, out _); + // Add to the memory pool + _system.MemPool.TryAdd(tx, snapshot); + } + // Transactions originally in the pool will automatically be reverified based on their priority. + + Sender.Tell(new FillCompleted()); + } + + private void OnInitialize() + { + if (!NativeContract.Ledger.Initialized(_system.StoreView)) + Persist(_system.GenesisBlock); + Sender.Tell(new object()); + } + + private void OnInventory(IInventory inventory, bool relay = true) + { + var result = inventory switch + { + Block block => OnNewBlock(block), + Transaction transaction => OnNewTransaction(transaction), + ExtensiblePayload payload => OnNewExtensiblePayload(payload), + _ => throw new NotSupportedException() + }; + if (result == VerifyResult.Succeed && relay) + { + _system.LocalNode.Tell(new LocalNode.RelayDirectly(inventory)); + } + SendRelayResult(inventory, result); + } + + private VerifyResult OnNewBlock(Block block) + { + if (!block.TryGetHash(out var blockHash)) return VerifyResult.Invalid; + + var snapshot = _system.StoreView; + var currentHeight = NativeContract.Ledger.CurrentIndex(snapshot); + var headerHeight = _system.HeaderCache.Last?.Index ?? currentHeight; + if (block.Index <= currentHeight) + return VerifyResult.AlreadyExists; + if (block.Index - 1 > headerHeight) + { + AddUnverifiedBlockToCache(block); + return VerifyResult.UnableToVerify; + } + if (block.Index == headerHeight + 1) + { + if (!block.Verify(_system.Settings, snapshot, _system.HeaderCache)) + return VerifyResult.Invalid; + } + else + { + var header = _system.HeaderCache[block.Index]; + if (header == null || !blockHash.Equals(header.Hash)) + return VerifyResult.Invalid; + } + _blockCache.TryAdd(blockHash, block); + if (block.Index == currentHeight + 1) + { + var blockPersist = block; + var blocksToPersistList = new List(); + while (true) + { + blocksToPersistList.Add(blockPersist); + if (blockPersist.Index + 1 > headerHeight) break; + var header = _system.HeaderCache[blockPersist.Index + 1]; + if (header == null) break; + if (!_blockCache.TryGetValue(header.Hash, out blockPersist)) break; + } + + var blocksPersisted = 0; + var extraRelayingBlocks = _system.Settings.MillisecondsPerBlock < ProtocolSettings.Default.MillisecondsPerBlock + ? (ProtocolSettings.Default.MillisecondsPerBlock - _system.Settings.MillisecondsPerBlock) / 1000 + : 0; + foreach (var blockToPersist in blocksToPersistList) + { + _blockCacheUnverified.Remove(blockToPersist.Index); + Persist(blockToPersist); + + if (blocksPersisted++ < blocksToPersistList.Count - (2 + extraRelayingBlocks)) continue; + // Empirically calibrated for relaying the most recent 2 blocks persisted with 15s network + // Increase in the rate of 1 block per second in configurations with faster blocks + + if (blockToPersist.Index + 99 >= headerHeight) + _system.LocalNode.Tell(new LocalNode.RelayDirectly(blockToPersist)); + } + if (_blockCacheUnverified.TryGetValue(currentHeight + 1, out var unverifiedBlocks)) + { + foreach (var unverifiedBlock in unverifiedBlocks.Blocks) + Self.Tell(unverifiedBlock, ActorRefs.NoSender); + _blockCacheUnverified.Remove(block.Index + 1); + } + } + else + { + if (block.Index + 99 >= headerHeight) + _system.LocalNode.Tell(new LocalNode.RelayDirectly(block)); + if (block.Index == headerHeight + 1) + _system.HeaderCache.Add(block.Header); + } + return VerifyResult.Succeed; + } + + private void OnNewHeaders(Header[] headers) + { + if (!_system.HeaderCache.Full) + { + var snapshot = _system.StoreView; + var headerHeight = _system.HeaderCache.Last?.Index ?? NativeContract.Ledger.CurrentIndex(snapshot); + foreach (var header in headers) + { + if (!header.TryGetHash(out _)) continue; + if (header.Index > headerHeight + 1) break; + if (header.Index < headerHeight + 1) continue; + if (!header.Verify(_system.Settings, snapshot, _system.HeaderCache)) break; + if (!_system.HeaderCache.Add(header)) break; + ++headerHeight; + } + } + _system.TaskManager.Tell(headers, Sender); + } + + private VerifyResult OnNewExtensiblePayload(ExtensiblePayload payload) + { + if (!payload.TryGetHash(out _)) return VerifyResult.Invalid; + + var snapshot = _system.StoreView; + _extensibleWitnessWhiteList ??= UpdateExtensibleWitnessWhiteList(_system.Settings, snapshot); + if (!payload.Verify(_system.Settings, snapshot, _extensibleWitnessWhiteList)) return VerifyResult.Invalid; + _system.RelayCache.Add(payload); + return VerifyResult.Succeed; + } + + private VerifyResult OnNewTransaction(Transaction transaction) + { + if (!transaction.TryGetHash(out var hash)) return VerifyResult.Invalid; + + switch (_system.ContainsTransaction(hash)) + { + case ContainsTransactionType.ExistsInPool: return VerifyResult.AlreadyInPool; + case ContainsTransactionType.ExistsInLedger: return VerifyResult.AlreadyExists; + } + + if (_system.ContainsConflictHash(hash, transaction.Signers.Select(s => s.Account))) return VerifyResult.HasConflicts; + return _system.MemPool.TryAdd(transaction, _system.StoreView); + } + + private void OnPreverifyCompleted(TransactionRouter.PreverifyCompleted task) + { + if (task.Result == VerifyResult.Succeed) + OnInventory(task.Transaction, task.Relay); + else + SendRelayResult(task.Transaction, task.Result); + } + + protected override void OnReceive(object message) + { + switch (message) + { + case Initialize: + OnInitialize(); + break; + case Import import: + OnImport(import.Blocks, import.Verify); + break; + case FillMemoryPool fill: + OnFillMemoryPool(fill.Transactions); + break; + case Header[] headers: + OnNewHeaders(headers); + break; + case Block block: + OnInventory(block, false); + break; + case Transaction tx: + OnTransaction(tx); + break; + case IInventory inventory: + OnInventory(inventory); + break; + case TransactionRouter.PreverifyCompleted task: + OnPreverifyCompleted(task); + break; + case Reverify reverify: + foreach (var inventory in reverify.Inventories) + OnInventory(inventory, false); + break; + case Idle _: + if (_system.MemPool.ReVerifyTopUnverifiedTransactionsIfNeeded(MaxTxToReverifyPerIdle, _system.StoreView)) + Self.Tell(Idle.Instance, ActorRefs.NoSender); + break; + } + } + + private void OnTransaction(Transaction tx) + { + if (!tx.TryGetHash(out var hash)) + { + SendRelayResult(tx, VerifyResult.Invalid); + return; + } + + switch (_system.ContainsTransaction(hash)) + { + case ContainsTransactionType.ExistsInPool: + SendRelayResult(tx, VerifyResult.AlreadyInPool); + break; + case ContainsTransactionType.ExistsInLedger: + SendRelayResult(tx, VerifyResult.AlreadyExists); + break; + default: + { + if (_system.ContainsConflictHash(hash, tx.Signers.Select(s => s.Account))) + SendRelayResult(tx, VerifyResult.HasConflicts); + else _system.TxRouter.Forward(new TransactionRouter.Preverify(tx, true)); + break; + } + } + } + + private void Persist(Block block) + { + using (var snapshot = _system.GetSnapshotCache()) + { + var allApplicationExecuted = new List(); + TransactionState[] transactionStates; + using (var engine = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, block, _system.Settings, 0)) + { + engine.LoadScript(s_onPersistScript); + if (engine.Execute() != VMState.HALT) + { + if (engine.FaultException != null) + throw engine.FaultException; + throw new InvalidOperationException(); + } + + var applicationExecuted = new ApplicationExecuted(engine); + Context.System.EventStream.Publish(applicationExecuted); + + allApplicationExecuted.Add(applicationExecuted); + transactionStates = engine.GetState()!; + } + + var clonedSnapshot = snapshot.CloneCache(); + // Warning: Do not write into variable snapshot directly. Write into variable clonedSnapshot and commit instead. + foreach (var transactionState in transactionStates) + { + var tx = transactionState.Transaction!; + using var engine = ApplicationEngine.Create(TriggerType.Application, tx, clonedSnapshot, block, _system.Settings, tx.SystemFee); + engine.LoadScript(tx.Script); + transactionState.State = engine.Execute(); + if (transactionState.State == VMState.HALT) + { + clonedSnapshot.Commit(); + } + else + { + clonedSnapshot = snapshot.CloneCache(); + } + + var applicationExecuted = new ApplicationExecuted(engine); + Context.System.EventStream.Publish(applicationExecuted); + allApplicationExecuted.Add(applicationExecuted); + } + + using (var engine = ApplicationEngine.Create(TriggerType.PostPersist, null, snapshot, block, _system.Settings, 0)) + { + engine.LoadScript(s_postPersistScript); + if (engine.Execute() != VMState.HALT) + { + if (engine.FaultException != null) + throw engine.FaultException; + throw new InvalidOperationException(); + } + + var applicationExecuted = new ApplicationExecuted(engine); + Context.System.EventStream.Publish(applicationExecuted); + allApplicationExecuted.Add(applicationExecuted); + } + + InvokeCommitting(_system, block, snapshot, allApplicationExecuted); + snapshot.Commit(); + } + + InvokeCommitted(_system, block); + _system.MemPool.UpdatePoolForBlockPersisted(block, _system.StoreView); + _extensibleWitnessWhiteList = null; + _blockCache.Remove(block.PrevHash); + Context.System.EventStream.Publish(new PersistCompleted(block)); + if (_system.HeaderCache.TryRemoveFirst(out var header)) + Debug.Assert(header.Index == block.Index); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void InvokeCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + InvokeHandlers(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void InvokeCommitted(NeoSystem system, Block block) + { + InvokeHandlers(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block)); + } + + private static void InvokeHandlers(Delegate[]? handlers, Action handlerAction) + { + if (handlers == null) return; + + foreach (var handler in handlers) + { + try + { + // skip stopped plugin. + if (handler.Target is Plugin { IsStopped: true }) + { + continue; + } + + handlerAction(handler); + } + catch (Exception ex) when (handler.Target is Plugin plugin) + { + var cause = ex.InnerException ?? ex; + Utility.Log(nameof(plugin.Name), LogLevel.Error, $"{plugin.Name} exception: {cause.Message}{Environment.NewLine}{cause.StackTrace}"); + switch (plugin.ExceptionPolicy) + { + case UnhandledExceptionPolicy.StopNode: + throw; + case UnhandledExceptionPolicy.StopPlugin: + //Stop plugin on exception + plugin.IsStopped = true; + break; + case UnhandledExceptionPolicy.Ignore: + // Log the exception and continue with the next handler + break; + default: + throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); + } + } + } + } + + /// + /// Gets a object used for creating the actor. + /// + /// The object that contains the . + /// The object used for creating the actor. + public static Props Props(NeoSystem system) + { + return Akka.Actor.Props.Create(() => new Blockchain(system)).WithMailbox("blockchain-mailbox"); + } + + private void SendRelayResult(IInventory inventory, VerifyResult result) + { + RelayResult rr = new(inventory, result); + Sender.Tell(rr); + Context.System.EventStream.Publish(rr); + } + + private static ImmutableHashSet UpdateExtensibleWitnessWhiteList(ProtocolSettings settings, DataCache snapshot) + { + var currentHeight = NativeContract.Ledger.CurrentIndex(snapshot); + var builder = ImmutableHashSet.CreateBuilder(); + builder.Add(NativeContract.NEO.GetCommitteeAddress(snapshot)); + var validators = NativeContract.NEO.GetNextBlockValidators(snapshot, settings.ValidatorsCount); + builder.Add(Contract.GetBFTAddress(validators)); + builder.UnionWith(validators.Select(u => Contract.CreateSignatureRedeemScript(u).ToScriptHash())); + var stateValidators = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.StateValidator, currentHeight); + if (stateValidators.Length > 0) + { + builder.Add(Contract.GetBFTAddress(stateValidators)); + builder.UnionWith(stateValidators.Select(u => Contract.CreateSignatureRedeemScript(u).ToScriptHash())); + } + return builder.ToImmutable(); + } +} + +internal class BlockchainMailbox : PriorityMailbox +{ + public BlockchainMailbox(Settings settings, Config config) + : base(settings, config) + { + } + + internal protected override bool IsHighPriority(object message) + { + return message switch + { + Header[] or Block or ExtensiblePayload or Terminated => true, + _ => false, + }; + } +} diff --git a/src/Neo/Ledger/HeaderCache.cs b/src/Neo/Ledger/HeaderCache.cs new file mode 100644 index 0000000000..c2adddb36c --- /dev/null +++ b/src/Neo/Ledger/HeaderCache.cs @@ -0,0 +1,150 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// HeaderCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; +using Neo.Network.P2P.Payloads; +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Ledger; + +/// +/// Used to cache the headers of the blocks that have not been received. +/// +public sealed class HeaderCache : IDisposable, IEnumerable
+{ + public const int MaxHeaders = 10_000; + + private readonly IndexedQueue
_headers = new(); + private readonly ReaderWriterLockSlim _readerWriterLock = new(); + + /// + /// Gets the at the specified index in the cache. + /// + /// The zero-based index of the to get. + /// The at the specified index in the cache. + public Header? this[uint index] + { + get + { + _readerWriterLock.EnterReadLock(); + try + { + if (_headers.Count == 0) return null; + var firstIndex = _headers[0].Index; + if (index < firstIndex) return null; + index -= firstIndex; + if (index >= _headers.Count) return null; + return _headers[(int)index]; + } + finally + { + _readerWriterLock.ExitReadLock(); + } + } + } + + /// + /// Gets the number of elements in the cache. + /// + public int Count + { + get + { + _readerWriterLock.EnterReadLock(); + try + { + return _headers.Count; + } + finally + { + _readerWriterLock.ExitReadLock(); + } + } + } + + /// + /// Indicates whether the cache is full. + /// + public bool Full => Count >= MaxHeaders; + + /// + /// Gets the last in the cache. Or if the cache is empty. + /// + public Header? Last + { + get + { + _readerWriterLock.EnterReadLock(); + try + { + if (_headers.Count == 0) return null; + return _headers[^1]; + } + finally + { + _readerWriterLock.ExitReadLock(); + } + } + } + + public void Dispose() + { + _readerWriterLock.Dispose(); + } + + internal bool Add(Header header) + { + _readerWriterLock.EnterWriteLock(); + try + { + // Enforce the cache limit when Full + if (_headers.Count >= MaxHeaders) + return false; + + _headers.Enqueue(header); + } + finally + { + _readerWriterLock.ExitWriteLock(); + } + return true; + } + + internal bool TryRemoveFirst([NotNullWhen(true)] out Header? header) + { + _readerWriterLock.EnterWriteLock(); + try + { + return _headers.TryDequeue(out header); + } + finally + { + _readerWriterLock.ExitWriteLock(); + } + } + + public IEnumerator
GetEnumerator() + { + _readerWriterLock.EnterReadLock(); + try + { + foreach (var header in _headers) + yield return header; + } + finally + { + _readerWriterLock.ExitReadLock(); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/Neo/Ledger/MemoryPool.cs b/src/Neo/Ledger/MemoryPool.cs new file mode 100644 index 0000000000..cd83ba910c --- /dev/null +++ b/src/Neo/Ledger/MemoryPool.cs @@ -0,0 +1,695 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MemoryPool.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Neo.Ledger; + +/// +/// Used to cache verified transactions before being written into the block. +/// +public class MemoryPool : IReadOnlyCollection +{ + public event EventHandler? TransactionAdded; + public event EventHandler? TransactionRemoved; + + // Allow a reverified transaction to be rebroadcast if it has been this many block times since last broadcast. + private const int BlocksTillRebroadcast = 10; + private int RebroadcastMultiplierThreshold => Capacity / 10; + + private readonly double MaxMillisecondsToReverifyTx; + + // These two are not expected to be hit, they are just safeguards. + private readonly double MaxMillisecondsToReverifyTxPerIdle; + + private readonly NeoSystem _system; + + // + /// + /// Guarantees consistency of the pool data structures. + /// + /// Note: The data structures are only modified from the `Blockchain` actor; so operations guaranteed to be + /// performed by the blockchain actor do not need to acquire the read lock; they only need the write + /// lock for write operations. + /// + private readonly ReaderWriterLockSlim _txRwLock = new(LockRecursionPolicy.SupportsRecursion); + + /// + /// Store all verified unsorted transactions currently in the pool. + /// + private readonly Dictionary _unsortedTransactions = new(); + /// + /// Store transaction hashes that conflict with verified mempooled transactions. + /// + private readonly Dictionary> _conflicts = new(); + /// + /// Stores the verified sorted transactions currently in the pool. + /// + private readonly SortedSet _sortedTransactions = new(); + + /// + /// Store the unverified transactions currently in the pool. + /// + /// Transactions in this data structure were valid in some prior block, but may no longer be valid. + /// The top ones that could make it into the next block get verified and moved into the verified data structures + /// (_unsortedTransactions, and _sortedTransactions) after each block. + /// + private readonly Dictionary _unverifiedTransactions = new(); + private readonly SortedSet _unverifiedSortedTransactions = new(); + + // Internal methods to aid in unit testing + internal int SortedTxCount => _sortedTransactions.Count; + internal int UnverifiedSortedTxCount => _unverifiedSortedTransactions.Count; + + /// + /// Total maximum capacity of transactions the pool can hold. + /// + public int Capacity { get; } + + /// + /// Store all verified unsorted transactions' senders' fee currently in the memory pool. + /// + private TransactionVerificationContext VerificationContext = new(); + + /// + /// Total count of transactions in the pool. + /// + public int Count + { + get + { + _txRwLock.EnterReadLock(); + try + { + return _unsortedTransactions.Count + _unverifiedTransactions.Count; + } + finally + { + _txRwLock.ExitReadLock(); + } + } + } + + /// + /// Total count of verified transactions in the pool. + /// + public int VerifiedCount => _unsortedTransactions.Count; // read of 32 bit type is atomic (no lock) + + /// + /// Total count of unverified transactions in the pool. + /// + public int UnVerifiedCount => _unverifiedTransactions.Count; + + /// + /// Initializes a new instance of the class. + /// + /// The object that contains the . + public MemoryPool(NeoSystem system) + { + _system = system; + Capacity = system.Settings.MemoryPoolMaxTransactions; + MaxMillisecondsToReverifyTx = system.Settings.MillisecondsPerBlock / 3; + MaxMillisecondsToReverifyTxPerIdle = system.Settings.MillisecondsPerBlock / 15; + } + + /// + /// Determine whether the pool is holding this transaction and has at some point verified it. + /// + /// The transaction hash. + /// if the contains the transaction; otherwise, . + /// + /// Note: The pool may not have verified it since the last block was persisted. To get only the + /// transactions that have been verified during this block use . + /// + public bool ContainsKey(UInt256 hash) + { + _txRwLock.EnterReadLock(); + try + { + return _unsortedTransactions.ContainsKey(hash) || _unverifiedTransactions.ContainsKey(hash); + } + finally + { + _txRwLock.ExitReadLock(); + } + } + + /// + /// Gets the associated with the specified hash. + /// + /// The hash of the to get. + /// When this method returns, contains the associated with the specified hash, if the hash is found; otherwise, . + /// if the contains a with the specified hash; otherwise, . + public bool TryGetValue(UInt256 hash, [NotNullWhen(true)] out Transaction? tx) + { + _txRwLock.EnterReadLock(); + try + { + _ = _unsortedTransactions.TryGetValue(hash, out var item) + || _unverifiedTransactions.TryGetValue(hash, out item); + tx = item?.Tx; + return tx != null; + } + finally + { + _txRwLock.ExitReadLock(); + } + } + + // Note: This isn't used in Fill during consensus, fill uses GetSortedVerifiedTransactions() + public IEnumerator GetEnumerator() + { + _txRwLock.EnterReadLock(); + try + { + return _unsortedTransactions.Select(p => p.Value.Tx) + .Concat(_unverifiedTransactions.Select(p => p.Value.Tx)) + .ToList() + .GetEnumerator(); + } + finally + { + _txRwLock.ExitReadLock(); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets the verified transactions in the . + /// + /// The verified transactions. + public IEnumerable GetVerifiedTransactions() + { + _txRwLock.EnterReadLock(); + try + { + return _unsortedTransactions.Select(p => p.Value.Tx).ToArray(); + } + finally + { + _txRwLock.ExitReadLock(); + } + } + + /// + /// Gets both the verified and the unverified transactions in the . + /// + /// The verified transactions. + /// The unverified transactions. + public void GetVerifiedAndUnverifiedTransactions(out IEnumerable verifiedTransactions, + out IEnumerable unverifiedTransactions) + { + _txRwLock.EnterReadLock(); + try + { + verifiedTransactions = _sortedTransactions.Reverse().Select(p => p.Tx).ToArray(); + unverifiedTransactions = _unverifiedSortedTransactions.Reverse().Select(p => p.Tx).ToArray(); + } + finally + { + _txRwLock.ExitReadLock(); + } + } + + /// + /// Gets the sorted verified transactions in the . + /// + /// The sorted verified transactions. + public Transaction[] GetSortedVerifiedTransactions(int count = -1) + { + _txRwLock.EnterReadLock(); + try + { + if (count < 0) + { + // Return all results + return _sortedTransactions + .Reverse() + .Select(p => p.Tx) + .ToArray(); + } + + return _sortedTransactions + .Reverse() + .Take(count) + .Select(p => p.Tx) + .ToArray(); + } + finally + { + _txRwLock.ExitReadLock(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private PoolItem? GetLowestFeeTransaction(out SortedSet? sortedPool) + { + var minItem = _unverifiedSortedTransactions.Min; + sortedPool = minItem != null ? _unverifiedSortedTransactions : null; + + var verifiedMin = _sortedTransactions.Min; + if (verifiedMin == null) return minItem; + + if (minItem != null && verifiedMin.CompareTo(minItem) >= 0) + return minItem; + + sortedPool = _sortedTransactions; + minItem = verifiedMin; + + return minItem; + } + + private PoolItem? GetLowestFeeTransaction(out Dictionary unsortedTxPool, out SortedSet? sortedPool) + { + sortedPool = null; + + try + { + return GetLowestFeeTransaction(out sortedPool); + } + finally + { + unsortedTxPool = ReferenceEquals(sortedPool, _unverifiedSortedTransactions) + ? _unverifiedTransactions : _unsortedTransactions; + } + } + + // Note: this must only be called from a single thread (the Blockchain actor) and + // doesn't take into account conflicting transactions. + internal bool CanTransactionFitInPool(Transaction tx) + { + if (Count < Capacity) return true; + + var item = GetLowestFeeTransaction(out _, out _); + if (item == null) return false; + + return item.CompareTo(tx) <= 0; + } + + internal VerifyResult TryAdd(Transaction tx, DataCache snapshot) + { + var poolItem = new PoolItem(tx); + + if (_unsortedTransactions.ContainsKey(tx.Hash)) return VerifyResult.AlreadyInPool; + + List? removedTransactions = null; + _txRwLock.EnterWriteLock(); + try + { + if (!CheckConflicts(tx, out List conflictsToBeRemoved)) return VerifyResult.HasConflicts; + VerifyResult result = tx.VerifyStateDependent(_system.Settings, snapshot, VerificationContext, conflictsToBeRemoved.Select(c => c.Tx)); + if (result != VerifyResult.Succeed) return result; + + _unsortedTransactions.Add(tx.Hash, poolItem); + VerificationContext.AddTransaction(tx); + _sortedTransactions.Add(poolItem); + foreach (var conflict in conflictsToBeRemoved) + { + if (TryRemoveVerified(conflict.Tx.Hash, out var _)) + VerificationContext.RemoveTransaction(conflict.Tx); + } + removedTransactions = conflictsToBeRemoved.Select(itm => itm.Tx).ToList(); + foreach (var attr in tx.GetAttributes()) + { + if (!_conflicts.TryGetValue(attr.Hash, out var pooled)) + { + pooled = new HashSet(); + } + pooled.Add(tx.Hash); + _conflicts[attr.Hash] = pooled; + } + + if (Count > Capacity) + removedTransactions.AddRange(RemoveOverCapacity()); + } + finally + { + _txRwLock.ExitWriteLock(); + } + + TransactionAdded?.Invoke(this, poolItem.Tx); + if (removedTransactions.Count > 0) + TransactionRemoved?.Invoke(this, new() + { + Transactions = removedTransactions, + Reason = TransactionRemovalReason.CapacityExceeded + }); + + if (!_unsortedTransactions.ContainsKey(tx.Hash)) return VerifyResult.OutOfMemory; + return VerifyResult.Succeed; + } + + /// + /// Checks whether there is no mismatch in Conflicts attributes between the current transaction + /// and mempooled unsorted transactions. If true, then these unsorted transactions will be added + /// into conflictsList. + /// + /// The current transaction needs to be checked. + /// The list of conflicting verified transactions that should be removed from the pool if tx fits the pool. + /// True if transaction fits the pool, otherwise false. + private bool CheckConflicts(Transaction tx, out List conflictsList) + { + conflictsList = new(); + long conflictsFeeSum = 0; + // Step 1: check if `tx` was in Conflicts attributes of unsorted transactions. + if (_conflicts.TryGetValue(tx.Hash, out var conflicting)) + { + foreach (var hash in conflicting) + { + var unsortedTx = _unsortedTransactions[hash]; + if (unsortedTx.Tx.Signers.Select(s => s.Account).Contains(tx.Sender)) + conflictsFeeSum += unsortedTx.Tx.NetworkFee; + conflictsList.Add(unsortedTx); + } + } + // Step 2: check if unsorted transactions were in `tx`'s Conflicts attributes. + foreach (var hash in tx.GetAttributes().Select(p => p.Hash)) + { + if (_unsortedTransactions.TryGetValue(hash, out var unsortedTx)) + { + if (!tx.Signers.Select(p => p.Account).Intersect(unsortedTx.Tx.Signers.Select(p => p.Account)).Any()) return false; + conflictsFeeSum += unsortedTx.Tx.NetworkFee; + conflictsList.Add(unsortedTx); + } + } + // Network fee of tx have to be larger than the sum of conflicting txs network fees. + if (conflictsFeeSum != 0 && conflictsFeeSum >= tx.NetworkFee) + return false; + + // Step 3: take into account sender's conflicting transactions while balance check, + // this will be done in VerifyStateDependant. + + return true; + } + + private List RemoveOverCapacity() + { + List removedTransactions = new(); + do + { + var minItem = GetLowestFeeTransaction(out var unsortedPool, out var sortedPool); + if (minItem == null || sortedPool == null) break; + + unsortedPool.Remove(minItem.Tx.Hash); + sortedPool.Remove(minItem); + removedTransactions.Add(minItem.Tx); + + if (ReferenceEquals(sortedPool, _sortedTransactions)) + { + RemoveConflictsOfVerified(minItem); + VerificationContext.RemoveTransaction(minItem.Tx); + } + } while (Count > Capacity); + + return removedTransactions; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryRemoveVerified(UInt256 hash, [MaybeNullWhen(false)] out PoolItem? item) + { + if (!_unsortedTransactions.TryGetValue(hash, out item)) + return false; + + _unsortedTransactions.Remove(hash); + _sortedTransactions.Remove(item); + + RemoveConflictsOfVerified(item); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RemoveConflictsOfVerified(PoolItem item) + { + foreach (var h in item.Tx.GetAttributes().Select(attr => attr.Hash)) + { + if (_conflicts.TryGetValue(h, out var conflicts)) + { + conflicts.Remove(item.Tx.Hash); + if (conflicts.Count == 0) + { + _conflicts.Remove(h); + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryRemoveUnVerified(UInt256 hash, [MaybeNullWhen(false)] out PoolItem? item) + { + _txRwLock.EnterWriteLock(); + try + { + if (!_unverifiedTransactions.TryGetValue(hash, out item)) + return false; + + _unverifiedTransactions.Remove(hash); + _unverifiedSortedTransactions.Remove(item); + return true; + } + finally + { + _txRwLock.ExitWriteLock(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void InvalidateVerifiedTransactions() + { + foreach (PoolItem item in _sortedTransactions) + { + if (_unverifiedTransactions.TryAdd(item.Tx.Hash, item)) + _unverifiedSortedTransactions.Add(item); + } + + // Clear the verified transactions now, since they all must be reverified. + _unsortedTransactions.Clear(); + VerificationContext = new TransactionVerificationContext(); + _sortedTransactions.Clear(); + _conflicts.Clear(); + } + + // Note: this must only be called from a single thread (the Blockchain actor) + internal void UpdatePoolForBlockPersisted(Block block, DataCache snapshot) + { + var conflictingItems = new List(); + _txRwLock.EnterWriteLock(); + try + { + var conflicts = new Dictionary>(); + // First remove the transactions verified in the block. + // No need to modify VerificationContext as it will be reset afterwards. + foreach (Transaction tx in block.Transactions) + { + if (!TryRemoveVerified(tx.Hash, out _)) TryRemoveUnVerified(tx.Hash, out _); + var conflictingSigners = tx.Signers.Select(s => s.Account); + foreach (var h in tx.GetAttributes().Select(a => a.Hash)) + { + if (conflicts.TryGetValue(h, out var signersList)) + { + signersList.AddRange(conflictingSigners); + continue; + } + signersList = conflictingSigners.ToList(); + conflicts.Add(h, signersList); + } + } + + // Then remove the transactions conflicting with the accepted ones. + // No need to modify VerificationContext as it will be reset afterwards. + var persisted = block.Transactions.Select(t => t.Hash); + var stale = new List(); + foreach (var item in _sortedTransactions) + { + if ((conflicts.TryGetValue(item.Tx.Hash, out var signersList) && signersList.Intersect(item.Tx.Signers.Select(s => s.Account)).Any()) || item.Tx.GetAttributes().Select(a => a.Hash).Intersect(persisted).Any()) + { + stale.Add(item.Tx.Hash); + conflictingItems.Add(item.Tx); + } + } + foreach (var h in stale) + { + if (!TryRemoveVerified(h, out _)) TryRemoveUnVerified(h, out _); + } + + // Add all the previously verified transactions back to the unverified transactions and clear mempool conflicts list. + InvalidateVerifiedTransactions(); + } + finally + { + _txRwLock.ExitWriteLock(); + } + if (conflictingItems.Count > 0) + { + TransactionRemoved?.Invoke(this, new() + { + Transactions = conflictingItems, + Reason = TransactionRemovalReason.Conflict, + }); + } + + // If we know about headers of future blocks, no point in verifying transactions from the unverified tx pool + // until we get caught up. + if (block.Index > 0 && _system.HeaderCache.Count > 0) + return; + + ReverifyTransactions((int)_system.Settings.MaxTransactionsPerBlock, MaxMillisecondsToReverifyTx, snapshot); + } + + internal void InvalidateAllTransactions() + { + _txRwLock.EnterWriteLock(); + try + { + InvalidateVerifiedTransactions(); + } + finally + { + _txRwLock.ExitWriteLock(); + } + } + + private int ReverifyTransactions(int count, double millisecondsTimeout, DataCache snapshot) + { + DateTime reverifyCutOffTimeStamp = TimeProvider.Current.UtcNow.AddMilliseconds(millisecondsTimeout); + List reverifiedItems = new(count); + List invalidItems = new(); + + _txRwLock.EnterWriteLock(); + try + { + // Since unverifiedSortedTxPool is ordered in an ascending manner, we take from the end. + foreach (PoolItem item in _unverifiedSortedTransactions.Reverse().Take(count)) + { + if (CheckConflicts(item.Tx, out List conflictsToBeRemoved) && + item.Tx.VerifyStateDependent(_system.Settings, snapshot, VerificationContext, conflictsToBeRemoved.Select(c => c.Tx)) == VerifyResult.Succeed) + { + reverifiedItems.Add(item); + if (_unsortedTransactions.TryAdd(item.Tx.Hash, item)) + { + _sortedTransactions.Add(item); + foreach (var attr in item.Tx.GetAttributes()) + { + if (!_conflicts.TryGetValue(attr.Hash, out var pooled)) + { + pooled = []; + } + pooled.Add(item.Tx.Hash); + _conflicts[attr.Hash] = pooled; + } + VerificationContext.AddTransaction(item.Tx); + foreach (var conflict in conflictsToBeRemoved) + { + if (TryRemoveVerified(conflict.Tx.Hash, out var _)) + VerificationContext.RemoveTransaction(conflict.Tx); + invalidItems.Add(conflict); + } + + } + } + else // Transaction no longer valid -- it will be removed from unverifiedTxPool. + invalidItems.Add(item); + + if (TimeProvider.Current.UtcNow > reverifyCutOffTimeStamp) break; + } + + int blocksTillRebroadcast = BlocksTillRebroadcast; + // Increases, proportionally, blocksTillRebroadcast if mempool has more items than threshold bigger RebroadcastMultiplierThreshold + if (Count > RebroadcastMultiplierThreshold) + blocksTillRebroadcast = blocksTillRebroadcast * Count / RebroadcastMultiplierThreshold; + + var rebroadcastCutOffTime = TimeProvider.Current.UtcNow.AddMilliseconds(-_system.Settings.MillisecondsPerBlock * blocksTillRebroadcast); + foreach (PoolItem item in reverifiedItems) + { + if (_unsortedTransactions.ContainsKey(item.Tx.Hash)) + { + if (item.LastBroadcastTimestamp < rebroadcastCutOffTime) + { + _system.LocalNode.Tell(new LocalNode.RelayDirectly(item.Tx), _system.Blockchain); + item.LastBroadcastTimestamp = TimeProvider.Current.UtcNow; + } + } + + _unverifiedTransactions.Remove(item.Tx.Hash); + _unverifiedSortedTransactions.Remove(item); + } + + foreach (PoolItem item in invalidItems) + { + _unverifiedTransactions.Remove(item.Tx.Hash); + _unverifiedSortedTransactions.Remove(item); + } + } + finally + { + _txRwLock.ExitWriteLock(); + } + + if (invalidItems.Count > 0) + { + TransactionRemoved?.Invoke(this, new() + { + Transactions = invalidItems.Select(p => p.Tx).ToArray(), + Reason = TransactionRemovalReason.NoLongerValid + }); + } + + return reverifiedItems.Count; + } + + /// + /// Reverify up to a given maximum count of transactions. Verifies less at a time once the max that can be + /// persisted per block has been reached. + /// + /// Note: this must only be called from a single thread (the Blockchain actor) + /// + /// Max transactions to reverify, the value passed can be >=1 + /// The snapshot to use for verifying. + /// true if more unsorted messages exist, otherwise false + internal bool ReVerifyTopUnverifiedTransactionsIfNeeded(int maxToVerify, DataCache snapshot) + { + if (_system.HeaderCache.Count > 0) + return false; + + if (_unverifiedSortedTransactions.Count > 0) + { + int verifyCount = _sortedTransactions.Count > _system.Settings.MaxTransactionsPerBlock ? 1 : maxToVerify; + ReverifyTransactions(verifyCount, MaxMillisecondsToReverifyTxPerIdle, snapshot); + } + + return _unverifiedTransactions.Count > 0; + } + + // This method is only for test purpose + // Do not use this method outside of unit tests + internal void Clear() + { + _txRwLock.EnterReadLock(); + try + { + _unsortedTransactions.Clear(); + _conflicts.Clear(); + _sortedTransactions.Clear(); + _unverifiedTransactions.Clear(); + _unverifiedSortedTransactions.Clear(); + } + finally + { + _txRwLock.ExitReadLock(); + } + } +} diff --git a/src/Neo/Ledger/PoolItem.cs b/src/Neo/Ledger/PoolItem.cs new file mode 100644 index 0000000000..1cb0c2d849 --- /dev/null +++ b/src/Neo/Ledger/PoolItem.cs @@ -0,0 +1,65 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// PoolItem.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P.Payloads; + +namespace Neo.Ledger; + +/// +/// Represents an item in the Memory Pool. +/// +/// Note: PoolItem objects don't consider transaction priority (low or high) in their compare CompareTo method. +/// This is because items of differing priority are never added to the same sorted set in MemoryPool. +/// +internal class PoolItem : IComparable +{ + /// + /// Internal transaction for PoolItem + /// + public Transaction Tx { get; } + + /// + /// Timestamp when transaction was stored on PoolItem + /// + public DateTime Timestamp { get; } + + /// + /// Timestamp when this transaction was last broadcast to other nodes + /// + public DateTime LastBroadcastTimestamp { get; set; } + + internal PoolItem(Transaction tx) + { + Tx = tx; + Timestamp = TimeProvider.Current.UtcNow; + LastBroadcastTimestamp = Timestamp; + } + + public int CompareTo(Transaction otherTx) + { + var ret = (Tx.GetAttribute() != null) + .CompareTo(otherTx.GetAttribute() != null); + if (ret != 0) return ret; + // Fees sorted ascending + ret = Tx.FeePerByte.CompareTo(otherTx.FeePerByte); + if (ret != 0) return ret; + ret = Tx.NetworkFee.CompareTo(otherTx.NetworkFee); + if (ret != 0) return ret; + // Transaction hash sorted descending + return otherTx.Hash.CompareTo(Tx.Hash); + } + + public int CompareTo(PoolItem? otherItem) + { + if (otherItem == null) return 1; + return CompareTo(otherItem.Tx); + } +} diff --git a/src/Neo/Ledger/TransactionRemovalReason.cs b/src/Neo/Ledger/TransactionRemovalReason.cs new file mode 100644 index 0000000000..2786245046 --- /dev/null +++ b/src/Neo/Ledger/TransactionRemovalReason.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionRemovalReason.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Ledger; + +/// +/// The reason a transaction was removed. +/// +public enum TransactionRemovalReason : byte +{ + /// + /// The transaction was rejected since it was the lowest priority transaction and the memory pool capacity was exceeded. + /// + CapacityExceeded, + + /// + /// The transaction was rejected due to failing re-validation after a block was persisted. + /// + NoLongerValid, + + /// + /// The transaction was rejected due to conflict with higher priority transactions with Conflicts attribute. + /// + Conflict, +} diff --git a/src/Neo/Ledger/TransactionRemovedEventArgs.cs b/src/Neo/Ledger/TransactionRemovedEventArgs.cs new file mode 100644 index 0000000000..9ffe77e406 --- /dev/null +++ b/src/Neo/Ledger/TransactionRemovedEventArgs.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionRemovedEventArgs.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P.Payloads; + +namespace Neo.Ledger; + +/// +/// Represents the event data of . +/// +public sealed class TransactionRemovedEventArgs +{ + /// + /// The s that is being removed. + /// + public required IReadOnlyCollection Transactions { get; init; } + + /// + /// The reason a transaction was removed. + /// + public TransactionRemovalReason Reason { get; init; } +} diff --git a/src/Neo/Ledger/TransactionRouter.cs b/src/Neo/Ledger/TransactionRouter.cs new file mode 100644 index 0000000000..08be68cd6f --- /dev/null +++ b/src/Neo/Ledger/TransactionRouter.cs @@ -0,0 +1,37 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionRouter.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Routing; +using Neo.Network.P2P.Payloads; + +namespace Neo.Ledger; + +internal class TransactionRouter(NeoSystem system) : UntypedActor +{ + public record Preverify(Transaction Transaction, bool Relay); + public record PreverifyCompleted(Transaction Transaction, bool Relay, VerifyResult Result); + + private readonly NeoSystem _system = system; + + protected override void OnReceive(object message) + { + if (message is not Preverify preverify) return; + var send = new PreverifyCompleted(preverify.Transaction, preverify.Relay, + preverify.Transaction.VerifyStateIndependent(_system.Settings)); + _system.Blockchain.Tell(send, Sender); + } + + internal static Props Props(NeoSystem system) + { + return Akka.Actor.Props.Create(() => new TransactionRouter(system)).WithRouter(new SmallestMailboxPool(Environment.ProcessorCount)); + } +} diff --git a/src/Neo/Ledger/TransactionVerificationContext.cs b/src/Neo/Ledger/TransactionVerificationContext.cs new file mode 100644 index 0000000000..925aa99a59 --- /dev/null +++ b/src/Neo/Ledger/TransactionVerificationContext.cs @@ -0,0 +1,86 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionVerificationContext.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Native; +using System.Numerics; + +namespace Neo.Ledger; + +/// +/// The context used to verify the transaction. +/// +public class TransactionVerificationContext +{ + /// + /// Store all verified unsorted transactions' senders' fee currently in the memory pool. + /// + private readonly Dictionary _senderFee = []; + + /// + /// Store oracle responses + /// + private readonly Dictionary _oracleResponses = []; + + /// + /// Adds a verified to the context. + /// + /// The verified . + public void AddTransaction(Transaction tx) + { + var oracle = tx.GetAttribute(); + if (oracle != null) _oracleResponses.Add(oracle.Id, tx.Hash); + + if (_senderFee.TryGetValue(tx.Sender, out var value)) + _senderFee[tx.Sender] = value + tx.SystemFee + tx.NetworkFee; + else + _senderFee.Add(tx.Sender, tx.SystemFee + tx.NetworkFee); + } + + /// + /// Determine whether the specified conflicts with other transactions. + /// + /// The specified . + /// The list of that conflicts with the specified one and are to be removed from the pool. + /// The snapshot used to verify the . + /// if the passes the check; otherwise, . + public bool CheckTransaction(Transaction tx, IEnumerable conflictingTxs, DataCache snapshot) + { + var balance = NativeContract.GAS.BalanceOf(snapshot, tx.Sender); + _senderFee.TryGetValue(tx.Sender, out var totalSenderFeeFromPool); + + var expectedFee = tx.SystemFee + tx.NetworkFee + totalSenderFeeFromPool; + foreach (var conflictTx in conflictingTxs.Where(c => c.Sender.Equals(tx.Sender))) + expectedFee -= conflictTx.NetworkFee + conflictTx.SystemFee; + if (balance < expectedFee) return false; + + var oracle = tx.GetAttribute(); + if (oracle != null && _oracleResponses.ContainsKey(oracle.Id)) + return false; + + return true; + } + + /// + /// Removes a from the context. + /// + /// The to be removed. + public void RemoveTransaction(Transaction tx) + { + if ((_senderFee[tx.Sender] -= tx.SystemFee + tx.NetworkFee) == 0) + _senderFee.Remove(tx.Sender); + + var oracle = tx.GetAttribute(); + if (oracle != null) + _oracleResponses.Remove(oracle.Id); + } +} diff --git a/src/Neo/Ledger/VerifyResult.cs b/src/Neo/Ledger/VerifyResult.cs new file mode 100644 index 0000000000..1d104b5a44 --- /dev/null +++ b/src/Neo/Ledger/VerifyResult.cs @@ -0,0 +1,95 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// VerifyResult.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P.Payloads; + +namespace Neo.Ledger; + +/// +/// Represents a verifying result of . +/// +public enum VerifyResult : byte +{ + /// + /// Indicates that the verification was successful. + /// + Succeed, + + /// + /// Indicates that an with the same hash already exists. + /// + AlreadyExists, + + /// + /// Indicates that an with the same hash already exists in the memory pool. + /// + AlreadyInPool, + + /// + /// Indicates that the is full and the transaction cannot be verified. + /// + OutOfMemory, + + /// + /// Indicates that the previous block of the current block has not been received, so the block cannot be verified. + /// + UnableToVerify, + + /// + /// Indicates that the is invalid. + /// + Invalid, + + /// + /// Indicates that the has an invalid script. + /// + InvalidScript, + + /// + /// Indicates that the has an invalid attribute. + /// + InvalidAttribute, + + /// + /// Indicates that the has an invalid signature. + /// + InvalidSignature, + + /// + /// Indicates that the size of the is not allowed. + /// + OverSize, + + /// + /// Indicates that the has expired. + /// + Expired, + + /// + /// Indicates that the failed to verify due to insufficient fees. + /// + InsufficientFunds, + + /// + /// Indicates that the failed to verify because it didn't comply with the policy. + /// + PolicyFail, + + /// + /// Indicates that the failed to verify because it conflicts with on-chain or mempooled transactions. + /// + HasConflicts, + + /// + /// Indicates that the failed to verify due to other reasons. + /// + Unknown +} diff --git a/src/Neo/Neo.csproj b/src/Neo/Neo.csproj new file mode 100644 index 0000000000..da0f226c46 --- /dev/null +++ b/src/Neo/Neo.csproj @@ -0,0 +1,35 @@ + + + + NEO;AntShares;Blockchain;Smart Contract + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Neo/NeoSystem.cs b/src/Neo/NeoSystem.cs new file mode 100644 index 0000000000..ee30f53307 --- /dev/null +++ b/src/Neo/NeoSystem.cs @@ -0,0 +1,319 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NeoSystem.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; +using Neo.Ledger; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.Persistence.Providers; +using Neo.Plugins; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using System.Collections.Immutable; + +namespace Neo; + +/// +/// Represents the basic unit that contains all the components required for running of a NEO node. +/// +public class NeoSystem : IDisposable +{ + /// + /// Triggered when a service is added to the . + /// + public event EventHandler? ServiceAdded; + + /// + /// The protocol settings of the . + /// + public ProtocolSettings Settings { get; } + + /// + /// The used to create actors for the . + /// + public ActorSystem ActorSystem { get; } = ActorSystem.Create(nameof(NeoSystem), + $"akka {{ log-dead-letters = off , loglevel = warning, loggers = [ \"{typeof(Utility.Logger).AssemblyQualifiedName}\" ] }}" + + $"blockchain-mailbox {{ mailbox-type: \"{typeof(BlockchainMailbox).AssemblyQualifiedName}\" }}" + + $"task-manager-mailbox {{ mailbox-type: \"{typeof(TaskManagerMailbox).AssemblyQualifiedName}\" }}" + + $"remote-node-mailbox {{ mailbox-type: \"{typeof(RemoteNodeMailbox).AssemblyQualifiedName}\" }}"); + + /// + /// The genesis block of the NEO blockchain. + /// + public Block GenesisBlock { get; } + + /// + /// The actor of the . + /// + public IActorRef Blockchain { get; } + + /// + /// The actor of the . + /// + public IActorRef LocalNode { get; } + + /// + /// The actor of the . + /// + public IActorRef TaskManager { get; } + + /// + /// The transaction router actor of the . + /// + public IActorRef TxRouter { get; } + + /// + /// A readonly view of the store. + /// + /// + /// It doesn't need to be disposed because the inside it is null. + /// + public StoreCache StoreView => new(_store); + + /// + /// The memory pool of the . + /// + public MemoryPool MemPool { get; } + + /// + /// The header cache of the . + /// + public HeaderCache HeaderCache { get; } = []; + + internal RelayCache RelayCache { get; } = new(100); + protected IStoreProvider StorageProvider { get; } + + private ImmutableList _services = ImmutableList.Empty; + private readonly IStore _store; + private ChannelsConfig? _startMessage = null; + private int _suspend = 0; + + static NeoSystem() + { + // Unify unhandled exceptions + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + + Plugin.LoadPlugins(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The protocol settings of the . + /// + /// The storage engine used to create the objects. + /// If this parameter is , a default in-memory storage engine will be used. + /// + /// + /// The path of the storage. + /// If is the default in-memory storage engine, this parameter is ignored. + /// + public NeoSystem(ProtocolSettings settings, string? storageProvider = null, string? storagePath = null) : + this(settings, StoreFactory.GetStoreProvider(storageProvider ?? nameof(MemoryStore)) + ?? throw new ArgumentException($"Can't find the storage provider {storageProvider}", nameof(storageProvider)), storagePath) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The protocol settings of the . + /// The to use. + /// + /// The path of the storage. + /// If is the default in-memory storage engine, this parameter is ignored. + /// + public NeoSystem(ProtocolSettings settings, IStoreProvider storageProvider, string? storagePath = null) + { + Settings = settings; + GenesisBlock = CreateGenesisBlock(settings); + StorageProvider = storageProvider; + _store = storageProvider.GetStore(storagePath); + MemPool = new MemoryPool(this); + Blockchain = ActorSystem.ActorOf(Ledger.Blockchain.Props(this)); + LocalNode = ActorSystem.ActorOf(Network.P2P.LocalNode.Props(this)); + TaskManager = ActorSystem.ActorOf(Network.P2P.TaskManager.Props(this)); + TxRouter = ActorSystem.ActorOf(TransactionRouter.Props(this)); + foreach (var plugin in Plugin.Plugins) + plugin.OnSystemLoaded(this); + Blockchain.Ask(new Blockchain.Initialize()).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Creates the genesis block for the NEO blockchain. + /// + /// The of the NEO system. + /// The genesis block. + public static Block CreateGenesisBlock(ProtocolSettings settings) => new() + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Timestamp = (new DateTime(2016, 7, 15, 15, 8, 21, DateTimeKind.Utc)).ToTimestampMS(), + Nonce = 2083236893, // nonce from the Bitcoin genesis block. + Index = 0, + PrimaryIndex = 0, + NextConsensus = Contract.GetBFTAddress(settings.StandbyValidators), + Witness = new Witness + { + InvocationScript = ReadOnlyMemory.Empty, + VerificationScript = new[] { (byte)OpCode.PUSH1 } + }, + }, + Transactions = [], + }; + + private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + Utility.Log("UnhandledException", LogLevel.Fatal, e.ExceptionObject); + } + + public void Dispose() + { + EnsureStopped(LocalNode); + EnsureStopped(Blockchain); + foreach (var p in Plugin.Plugins) + p.Dispose(); + // Dispose will call ActorSystem.Terminate() + ActorSystem.Dispose(); + ActorSystem.WhenTerminated.Wait(); + HeaderCache.Dispose(); + _store.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// Adds a service to the . + /// + /// The service object to be added. + public void AddService(object service) + { + ImmutableInterlocked.Update(ref _services, p => p.Add(service)); + ServiceAdded?.Invoke(this, service); + } + + /// + /// Gets a specified type of service object from the . + /// + /// The type of the service object. + /// + /// An action used to filter the service objects. his parameter can be . + /// + /// The service object found. + public T? GetService(Func? filter = null) + { + var result = _services.OfType(); + if (filter is null) + return result.FirstOrDefault(); + return result.FirstOrDefault(filter); + } + + /// + /// Blocks the current thread until the specified actor has stopped. + /// + /// The actor to wait. + public void EnsureStopped(IActorRef actor) + { + using var inbox = Inbox.Create(ActorSystem); + inbox.Watch(actor); + ActorSystem.Stop(actor); + inbox.Receive(TimeSpan.FromSeconds(30)); + } + + /// + /// Loads an at the specified path. + /// + /// The path of the storage. + /// The loaded . + public IStore LoadStore(string path) + { + return StorageProvider.GetStore(path); + } + + /// + /// Resumes the startup process of . + /// + /// if the startup process is resumed; otherwise, . + public bool ResumeNodeStartup() + { + if (Interlocked.Decrement(ref _suspend) != 0) + return false; + if (_startMessage != null) + { + LocalNode.Tell(_startMessage); + _startMessage = null; + } + return true; + } + + /// + /// Starts the with the specified configuration. + /// + /// The configuration used to start the . + public void StartNode(ChannelsConfig config) + { + _startMessage = config; + + if (_suspend == 0) + { + LocalNode.Tell(_startMessage); + _startMessage = null; + } + } + + /// + /// Suspends the startup process of . + /// + public void SuspendNodeStartup() + { + Interlocked.Increment(ref _suspend); + } + + /// + /// Gets a snapshot of the blockchain storage with an execution cache. + /// With the snapshot, we have the latest state of the blockchain, with the cache, + /// we can run transactions in a sandboxed environment. + /// + /// An instance of + public StoreCache GetSnapshotCache() + { + return new StoreCache(_store.GetSnapshot()); + } + + /// + /// Determines whether the specified transaction exists in the memory pool or storage. + /// + /// The hash of the transaction + /// if the transaction exists; otherwise, . + public ContainsTransactionType ContainsTransaction(UInt256 hash) + { + if (MemPool.ContainsKey(hash)) return ContainsTransactionType.ExistsInPool; + return NativeContract.Ledger.ContainsTransaction(StoreView, hash) ? + ContainsTransactionType.ExistsInLedger : ContainsTransactionType.NotExist; + } + + /// + /// Determines whether the specified transaction conflicts with some on-chain transaction. + /// + /// The hash of the transaction + /// The list of signer accounts of the transaction + /// + /// if the transaction conflicts with on-chain transaction; otherwise, . + /// + public bool ContainsConflictHash(UInt256 hash, IEnumerable signers) + { + return NativeContract.Ledger.ContainsConflictHash(StoreView, hash, signers, Settings.MaxTraceableBlocks); + } +} diff --git a/src/Neo/Network/P2P/Capabilities/ArchivalNodeCapability.cs b/src/Neo/Network/P2P/Capabilities/ArchivalNodeCapability.cs new file mode 100644 index 0000000000..c7c2620a22 --- /dev/null +++ b/src/Neo/Network/P2P/Capabilities/ArchivalNodeCapability.cs @@ -0,0 +1,45 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ArchivalNodeCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Network.P2P.Capabilities; + +/// +/// Indicates that a node stores full block history. These nodes can be used +/// for P2P synchronization from genesis (other ones can cut the tail and +/// won't respond to requests of old (wrt MaxTraceableBlocks) blocks. +/// +public class ArchivalNodeCapability : NodeCapability +{ + public override int Size => + base.Size + // Type + 1; // Zero (empty VarBytes or String) + + /// + /// Initializes a new instance of the class. + /// + public ArchivalNodeCapability() : base(NodeCapabilityType.ArchivalNode) + { + } + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + var zero = reader.ReadByte(); // Zero-length byte array or string (see UnknownCapability). + if (zero != 0) + throw new FormatException("ArchivalNode has some data"); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write((byte)0); + } +} diff --git a/src/Neo/Network/P2P/Capabilities/DisableCompressionCapability.cs b/src/Neo/Network/P2P/Capabilities/DisableCompressionCapability.cs new file mode 100644 index 0000000000..4c01736865 --- /dev/null +++ b/src/Neo/Network/P2P/Capabilities/DisableCompressionCapability.cs @@ -0,0 +1,41 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// DisableCompressionCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Network.P2P.Capabilities; + +/// +/// This capability disable the compression p2p mechanism. +/// +public class DisableCompressionCapability : NodeCapability +{ + public override int Size => + base.Size + // Type + 1; // Zero (empty VarBytes or String) + + /// + /// Initializes a new instance of the class. + /// + public DisableCompressionCapability() : base(NodeCapabilityType.DisableCompression) { } + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + var zero = reader.ReadByte(); // Zero-length byte array or string (see UnknownCapability). + if (zero != 0) + throw new FormatException("DisableCompression has some data"); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write((byte)0); + } +} diff --git a/src/Neo/Network/P2P/Capabilities/FullNodeCapability.cs b/src/Neo/Network/P2P/Capabilities/FullNodeCapability.cs new file mode 100644 index 0000000000..d4ea9cbbe8 --- /dev/null +++ b/src/Neo/Network/P2P/Capabilities/FullNodeCapability.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FullNodeCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Network.P2P.Capabilities; + +/// +/// Indicates that a node has complete current state. +/// +public class FullNodeCapability : NodeCapability +{ + /// + /// Indicates the current block height of the node. + /// + public uint StartHeight; + + public override int Size => + base.Size + // Type + sizeof(uint); // Start Height + + /// + /// Initializes a new instance of the class. + /// + /// The current block height of the node. + public FullNodeCapability(uint startHeight = 0) : base(NodeCapabilityType.FullNode) + { + StartHeight = startHeight; + } + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + StartHeight = reader.ReadUInt32(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(StartHeight); + } +} diff --git a/src/Neo/Network/P2P/Capabilities/NodeCapability.cs b/src/Neo/Network/P2P/Capabilities/NodeCapability.cs new file mode 100644 index 0000000000..85bcc742fb --- /dev/null +++ b/src/Neo/Network/P2P/Capabilities/NodeCapability.cs @@ -0,0 +1,92 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NodeCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Network.P2P.Capabilities; + +/// +/// Represents the capabilities of a Neo node. Each capability has a type +/// and some type-specific node metadata to other peers that can take it +/// into account when interacting with this node. This metadata is +/// serialized in a type-specific way, but for compatibility reasons any +/// new capabilities MUST be compatible with UnknownCapability serialization +/// scheme. +/// +public abstract class NodeCapability : ISerializable +{ + /// + /// Indicates the type of the . + /// + public readonly NodeCapabilityType Type; + + public virtual int Size => sizeof(NodeCapabilityType); // Type + + /// + /// Initializes a new instance of the class. + /// + /// The type of the . + protected NodeCapability(NodeCapabilityType type) + { + Type = type; + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + var readType = reader.ReadByte(); + if (readType != (byte)Type) + { + throw new FormatException($"ReadType({readType}) does not match NodeCapabilityType({Type})"); + } + + DeserializeWithoutType(ref reader); + } + + /// + /// Deserializes an object from a . + /// + /// The for reading data. + /// The deserialized . + public static NodeCapability DeserializeFrom(ref MemoryReader reader) + { + NodeCapabilityType type = (NodeCapabilityType)reader.ReadByte(); + NodeCapability capability = type switch + { +#pragma warning disable CS0618 // Type or member is obsolete + NodeCapabilityType.TcpServer or NodeCapabilityType.WsServer => new ServerCapability(type), +#pragma warning restore CS0618 // Type or member is obsolete + NodeCapabilityType.DisableCompression => new DisableCompressionCapability(), + NodeCapabilityType.FullNode => new FullNodeCapability(), + NodeCapabilityType.ArchivalNode => new ArchivalNodeCapability(), + _ => new UnknownCapability(type), + }; + capability.DeserializeWithoutType(ref reader); + return capability; + } + + /// + /// Deserializes the object from a . + /// + /// The for reading data. + protected abstract void DeserializeWithoutType(ref MemoryReader reader); + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write((byte)Type); + SerializeWithoutType(writer); + } + + /// + /// Serializes the object to a . + /// + /// The for writing data. + protected abstract void SerializeWithoutType(BinaryWriter writer); +} diff --git a/src/Neo/Network/P2P/Capabilities/NodeCapabilityType.cs b/src/Neo/Network/P2P/Capabilities/NodeCapabilityType.cs new file mode 100644 index 0000000000..ae4e8e07c7 --- /dev/null +++ b/src/Neo/Network/P2P/Capabilities/NodeCapabilityType.cs @@ -0,0 +1,64 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NodeCapabilityType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Network.P2P.Capabilities; + +/// +/// Represents the type of . +/// +public enum NodeCapabilityType : byte +{ + #region Servers + + /// + /// Indicates that the node is listening on a Tcp port. + /// + TcpServer = 0x01, + + /// + /// Indicates that the node is listening on a WebSocket port. + /// + [Obsolete("WebSocket is no longer supported.")] + WsServer = 0x02, + + /// + /// Disable p2p compression + /// + DisableCompression = 0x03, + + #endregion + + #region Data availability + + /// + /// Indicates that the node has complete current state. + /// + FullNode = 0x10, + + /// + /// Indicates that the node stores full block history. These nodes can be used + /// for P2P synchronization from genesis (other ones can cut the tail and + /// won't respond to requests for old (wrt MaxTraceableBlocks) blocks). + /// + ArchivalNode = 0x11, + + #endregion + + #region Private extensions + + /// + /// The first extension ID. Any subsequent can be used in an + /// implementation-specific way. + /// + Extension0 = 0xf0 + + #endregion +} diff --git a/src/Neo/Network/P2P/Capabilities/ServerCapability.cs b/src/Neo/Network/P2P/Capabilities/ServerCapability.cs new file mode 100644 index 0000000000..e87cca8e2b --- /dev/null +++ b/src/Neo/Network/P2P/Capabilities/ServerCapability.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ServerCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Network.P2P.Capabilities; + +/// +/// Indicates that the node is a server. +/// +public class ServerCapability : NodeCapability +{ + /// + /// Indicates the port that the node is listening on. + /// + public ushort Port; + + public override int Size => + base.Size + // Type + sizeof(ushort); // Port + + /// + /// Initializes a new instance of the class. + /// + /// The type of the . It must be or + /// The port that the node is listening on. + public ServerCapability(NodeCapabilityType type, ushort port = 0) : base(type) + { +#pragma warning disable CS0618 // Type or member is obsolete + if (type != NodeCapabilityType.TcpServer && type != NodeCapabilityType.WsServer) +#pragma warning restore CS0618 // Type or member is obsolete + { + throw new ArgumentException($"Invalid type: {type}", nameof(type)); + } + + Port = port; + } + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + Port = reader.ReadUInt16(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Port); + } +} diff --git a/src/Neo/Network/P2P/Capabilities/UnknownCapability.cs b/src/Neo/Network/P2P/Capabilities/UnknownCapability.cs new file mode 100644 index 0000000000..4b5201c9b6 --- /dev/null +++ b/src/Neo/Network/P2P/Capabilities/UnknownCapability.cs @@ -0,0 +1,52 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UnknownCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.IO; + +namespace Neo.Network.P2P.Capabilities; + +/// +/// This capability implements a generic extensible format for new +/// capabilities. This capability has no real type and can not be +/// serialized. But it allows to ignore any new/unknown types for old nodes +/// in a safe way. +/// +public class UnknownCapability : NodeCapability +{ + /// + /// Indicates the maximum size of the field. + /// + public const int MaxDataSize = 1024; + + public ReadOnlyMemory Data; + + public override int Size => + base.Size + // Type + Data.GetVarSize(); // Any kind of data enclosed in a single string. + + /// + /// Initializes a new instance of the class. + /// + /// The type of the . + public UnknownCapability(NodeCapabilityType type) : base(type) { } + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + Data = reader.ReadVarMemory(MaxDataSize); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.WriteVarBytes(Data.Span); + } +} diff --git a/src/Neo/Network/P2P/ChannelsConfig.cs b/src/Neo/Network/P2P/ChannelsConfig.cs new file mode 100644 index 0000000000..34a6e8955d --- /dev/null +++ b/src/Neo/Network/P2P/ChannelsConfig.cs @@ -0,0 +1,75 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ChannelsConfig.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Net; + +namespace Neo.Network.P2P; + +/// +/// Represents the settings to start . +/// +public class ChannelsConfig +{ + /// + /// The default value for enable compression. + /// + public const bool DefaultEnableCompression = true; + + /// + /// The default minimum number of desired connections. + /// + public const int DefaultMinDesiredConnections = 10; + + /// + /// The default maximum number of desired connections. + /// + public const int DefaultMaxConnections = DefaultMinDesiredConnections * 4; + + /// + /// The default maximum allowed connections per address. + /// + public const int DefaultMaxConnectionsPerAddress = 3; + + /// + /// The default maximum knwon hashes. + /// + public const int DefaultMaxKnownHashes = 1000; + + /// + /// Tcp configuration. + /// + public IPEndPoint? Tcp { get; set; } + + /// + /// Enable compression. + /// + public bool EnableCompression { get; set; } = DefaultEnableCompression; + + /// + /// Minimum desired connections. + /// + public int MinDesiredConnections { get; set; } = DefaultMinDesiredConnections; + + /// + /// Max allowed connections. + /// + public int MaxConnections { get; set; } = DefaultMaxConnections; + + /// + /// Max allowed connections per address. + /// + public int MaxConnectionsPerAddress { get; set; } = DefaultMaxConnectionsPerAddress; + + /// + /// Max known hashes + /// + public int MaxKnownHashes { get; set; } = DefaultMaxKnownHashes; +} diff --git a/src/Neo/Network/P2P/Connection.cs b/src/Neo/Network/P2P/Connection.cs new file mode 100644 index 0000000000..874e332518 --- /dev/null +++ b/src/Neo/Network/P2P/Connection.cs @@ -0,0 +1,136 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Connection.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO; +using Neo.Exceptions; +using System.Net; + +namespace Neo.Network.P2P; + +/// +/// Represents a connection of the P2P network. +/// +public abstract class Connection : UntypedActor +{ + internal class Close { public bool Abort; } + internal class Ack : Tcp.Event { public static Ack Instance = new(); } + + /// + /// connection initial timeout (in seconds) before any package has been accepted. + /// + private const int connectionTimeoutLimitStart = 10; + + /// + /// connection timeout (in seconds) after every `OnReceived(ByteString data)` event. + /// + private const int connectionTimeoutLimit = 60; + + /// + /// The address of the remote node. + /// + public IPEndPoint Remote { get; } + + /// + /// The address of the local node. + /// + public IPEndPoint Local { get; } + + private ICancelable timer; + private readonly IActorRef? tcp; + private bool disconnected = false; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying connection object. + /// The address of the remote node. + /// The address of the local node. + protected Connection(object connection, IPEndPoint remote, IPEndPoint local) + { + Remote = remote; + Local = local; + timer = Context.System.Scheduler.ScheduleTellOnceCancelable(TimeSpan.FromSeconds(connectionTimeoutLimitStart), Self, new Close { Abort = true }, ActorRefs.NoSender); + switch (connection) + { + case IActorRef tcp: + this.tcp = tcp; + break; + } + } + + /// + /// Disconnect from the remote node. + /// + /// Indicates whether the TCP ABORT command should be sent. + public void Disconnect(bool abort = false) + { + disconnected = true; + tcp?.Tell(abort ? Tcp.Abort.Instance : Tcp.Close.Instance); + Context.Stop(Self); + } + + /// + /// Called when a TCP ACK message is received. + /// + protected virtual void OnAck() + { + } + + /// + /// Called when data is received. + /// + /// The received data. + protected abstract void OnData(ByteString data); + + protected override void OnReceive(object message) + { + switch (message) + { + case Close close: + Disconnect(close.Abort); + break; + case Ack _: + OnAck(); + break; + case Tcp.Received received: + OnReceived(received.Data); + break; + case Tcp.ConnectionClosed _: + Context.Stop(Self); + break; + } + } + + private void OnReceived(ByteString data) + { + timer.CancelIfNotNull(); + timer = Context.System.Scheduler.ScheduleTellOnceCancelable(TimeSpan.FromSeconds(connectionTimeoutLimit), Self, new Close { Abort = true }, ActorRefs.NoSender); + data.TryCatch(OnData, (_, _) => Disconnect(true)); + } + + protected override void PostStop() + { + if (!disconnected) + tcp?.Tell(Tcp.Close.Instance); + timer.CancelIfNotNull(); + base.PostStop(); + } + + /// + /// Sends data to the remote node. + /// + /// + protected void SendData(ByteString data) + { + tcp?.Tell(Tcp.Write.Create(data, Ack.Instance)); + } +} diff --git a/src/Neo/Network/P2P/Helper.cs b/src/Neo/Network/P2P/Helper.cs new file mode 100644 index 0000000000..99bef4bf3e --- /dev/null +++ b/src/Neo/Network/P2P/Helper.cs @@ -0,0 +1,95 @@ +// Copyright (C) 2015-2025 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.Cryptography; +using Neo.Network.P2P.Payloads; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Network.P2P; + +/// +/// A helper class for . +/// +public static class Helper +{ + private const int SignDataLength = sizeof(uint) + UInt256.Length; + + /// + /// Calculates the hash of a . + /// + /// The object to hash. + /// The hash of the object. + public static UInt256 CalculateHash(this IVerifiable verifiable) + { + using MemoryStream ms = new(); + using BinaryWriter writer = new(ms); + verifiable.SerializeUnsigned(writer); + writer.Flush(); + return new UInt256(ms.ToArray().Sha256()); + } + + /// + /// Tries to get the hash of the transaction. + /// If this IVerifiable is not valid, the hash may be . + /// + /// The object to hash. + /// The hash of the transaction. + /// + /// if the hash was successfully retrieved; otherwise, . + /// + public static bool TryGetHash(this IVerifiable verifiable, [NotNullWhen(true)] out UInt256? hash) + { + try + { + hash = verifiable.Hash; + return true; + } + catch + { + hash = null; + return false; + } + } + + /// + /// Gets the data of a object to be hashed. + /// + /// The object to hash. + /// The magic number of the network. + /// The data to hash. + public static byte[] GetSignData(this IVerifiable verifiable, uint network) => GetSignData(verifiable.Hash, network); + + /// + /// Gets the data to be hashed. + /// + /// Message. + /// The magic number of the network. + /// The data to hash. + public static byte[] GetSignData(this UInt256 messageHash, uint network) + { + /* Same as: + using MemoryStream ms = new(); + using BinaryWriter writer = new(ms); + writer.Write(network); + writer.Write(verifiable.Hash); + writer.Flush(); + return ms.ToArray(); + */ + + var buffer = new byte[SignDataLength]; + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, network); + messageHash.Serialize(buffer.AsSpan(sizeof(uint))); + + return buffer; + } +} diff --git a/src/Neo/Network/P2P/LocalNode.cs b/src/Neo/Network/P2P/LocalNode.cs new file mode 100644 index 0000000000..ec0cba77ff --- /dev/null +++ b/src/Neo/Network/P2P/LocalNode.cs @@ -0,0 +1,294 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// LocalNode.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Exceptions; +using Neo.Factories; +using Neo.IO; +using Neo.Network.P2P.Capabilities; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract.Native; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using System.Reflection; + +namespace Neo.Network.P2P; + +/// +/// Actor used to manage the connections of the local node. +/// +public class LocalNode : Peer +{ + /// + /// Sent to to relay an . + /// + public record RelayDirectly(IInventory Inventory); + + /// + /// Sent to to send an . + /// + public record SendDirectly(IInventory Inventory); + + /// + /// Sent to to request for an instance of . + /// + public record GetInstance; + + /// + /// Indicates the protocol version of the local node. + /// + public const uint ProtocolVersion = 0; + + private const int MaxCountFromSeedList = 5; + private readonly IPEndPoint?[] SeedList; + + private readonly NeoSystem system; + internal readonly ConcurrentDictionary RemoteNodes = new(); + + /// + /// Indicates the number of connected nodes. + /// + public int ConnectedCount => RemoteNodes.Count; + + /// + /// Indicates the number of unconnected nodes. When the number of connections is not enough, it will automatically connect to these nodes. + /// + public int UnconnectedCount => UnconnectedPeers.Count; + + /// + /// The random number used to identify the local node. + /// + public static readonly uint Nonce; + + /// + /// The identifier of the client software of the local node. + /// + public static string UserAgent { get; set; } + + static LocalNode() + { + Nonce = RandomNumberFactory.NextUInt32(); + UserAgent = $"/{Assembly.GetExecutingAssembly().GetName().Name}:{Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)}/"; + } + + /// + /// Initializes a new instance of the class. + /// + /// The object that contains the . + public LocalNode(NeoSystem system) + { + this.system = system; + SeedList = new IPEndPoint[system.Settings.SeedList.Length]; + + // Start dns resolution in parallel + var seedList = system.Settings.SeedList; + for (var i = 0; i < seedList.Length; i++) + { + var index = i; + Task.Run(() => SeedList[index] = GetIpEndPoint(seedList[index])); + } + } + + /// + /// Packs a MessageCommand to a full Message with an optional ISerializable payload. + /// Forwards it to . + /// + /// The message command to be packed. + /// Optional payload to be Serialized along the message. + private void BroadcastMessage(MessageCommand command, ISerializable? payload = null) + { + BroadcastMessage(Message.Create(command, payload)); + } + + /// + /// Broadcast a message to all connected nodes. + /// + /// The message to be broadcast. + private void BroadcastMessage(Message message) => SendToRemoteNodes(message); + + /// + /// Send message to all the RemoteNodes connected to other nodes, faster than ActorSelection. + /// + private void SendToRemoteNodes(object message) + { + foreach (var connection in RemoteNodes.Keys) + { + connection.Tell(message); + } + } + + private static IPEndPoint GetIPEndpointFromHostPort(string hostNameOrAddress, int port) + { + if (IPAddress.TryParse(hostNameOrAddress, out var ipAddress)) + return new IPEndPoint(ipAddress, port); + var entry = hostNameOrAddress.TryCatchThrow(Dns.GetHostEntry); + ipAddress = entry.AddressList.FirstOrDefault(p => p.AddressFamily == AddressFamily.InterNetwork || p.IsIPv6Teredo); + if (ipAddress == null) throw new ArgumentException("Can not resolve DNS name or IP address."); + return new IPEndPoint(ipAddress, port); + } + + internal static IPEndPoint? GetIpEndPoint(string hostAndPort) + { + if (string.IsNullOrEmpty(hostAndPort)) return null; + + return hostAndPort.Split(':') + .TryCatch, Exception, IPEndPoint?>( + t => GetIPEndpointFromHostPort(t[0], int.Parse(t[1])), static (_, _) => null); + } + + /// + /// Checks the new connection. + /// If it is equal to the nonce of local or any remote node, it'll return false, + /// else we'll return true and update the Listener address of the connected remote node. + /// + /// Remote node actor. + /// Remote node object. + /// if the new connection is allowed; otherwise, . + public bool AllowNewConnection(IActorRef actor, RemoteNode node) + { + if (node.Version!.Network != system.Settings.Network) return false; + if (node.Version.Nonce == Nonce) return false; + + // filter duplicate connections + foreach (var other in RemoteNodes.Values) + if (other != node && other.Remote.Address.Equals(node.Remote.Address) && other.Version?.Nonce == node.Version.Nonce) + return false; + + if (node.Remote.Port != node.ListenerTcpPort && node.ListenerTcpPort != 0) + ConnectedPeers.TryUpdate(actor, node.Listener, node.Remote); + + return true; + } + + /// + /// Gets the connected remote nodes. + /// + /// + public IEnumerable GetRemoteNodes() + { + return RemoteNodes.Values; + } + + /// + /// Gets the unconnected nodes. + /// + /// + public IEnumerable GetUnconnectedPeers() + { + return UnconnectedPeers; + } + + /// + /// Performs a broadcast with the command , + /// which, eventually, tells all known connections. + /// If there are no connected peers it will try with the default, + /// respecting limit. + /// + /// Number of peers that are being requested. + protected override void NeedMorePeers(int count) + { + count = Math.Max(count, MaxCountFromSeedList); + if (!ConnectedPeers.IsEmpty) + { + BroadcastMessage(MessageCommand.GetAddr); + } + else + { + // Will call AddPeers with default SeedList set cached on . + // It will try to add those, sequentially, to the list of currently unconnected ones. + + AddPeers(SeedList + .Where(p => p != null) + .Select(p => p!) + .OrderBy(p => RandomNumberFactory.NextInt32()) + .Take(count)); + } + } + + protected override void OnReceive(object message) + { + base.OnReceive(message); + switch (message) + { + case Message msg: + BroadcastMessage(msg); + break; + case RelayDirectly relay: + OnRelayDirectly(relay.Inventory); + break; + case SendDirectly send: + OnSendDirectly(send.Inventory); + break; + case GetInstance _: + Sender.Tell(this); + break; + } + } + + private void OnRelayDirectly(IInventory inventory) + { + var message = new RemoteNode.Relay(inventory); + // When relaying a block, if the block's index is greater than + // 'LastBlockIndex' of the RemoteNode, relay the block; + // otherwise, don't relay. + if (inventory is Block block) + { + foreach (KeyValuePair kvp in RemoteNodes) + { + if (block.Index > kvp.Value.LastBlockIndex) + kvp.Key.Tell(message); + } + } + else + SendToRemoteNodes(message); + } + + public NodeCapability[] GetNodeCapabilities() + { + var capabilities = new List + { + new FullNodeCapability(NativeContract.Ledger.CurrentIndex(system.StoreView)), + new ArchivalNodeCapability(), + }; + + if (!Config.EnableCompression) + { + capabilities.Add(new DisableCompressionCapability()); + } + + if (ListenerTcpPort > 0) capabilities.Add(new ServerCapability(NodeCapabilityType.TcpServer, (ushort)ListenerTcpPort)); + + return [.. capabilities]; + } + + private void OnSendDirectly(IInventory inventory) => SendToRemoteNodes(inventory); + + protected override void OnTcpConnected(IActorRef connection) + { + connection.Tell(new RemoteNode.StartProtocol()); + } + + /// + /// Gets a object used for creating the actor. + /// + /// The object that contains the . + /// The object used for creating the actor. + public static Props Props(NeoSystem system) + { + return Akka.Actor.Props.Create(() => new LocalNode(system)); + } + + protected override Props ProtocolProps(object connection, IPEndPoint remote, IPEndPoint local) + { + return RemoteNode.Props(system, this, connection, remote, local, Config); + } +} diff --git a/src/Neo/Network/P2P/Message.cs b/src/Neo/Network/P2P/Message.cs new file mode 100644 index 0000000000..4a7e91ce8f --- /dev/null +++ b/src/Neo/Network/P2P/Message.cs @@ -0,0 +1,202 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Message.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO; +using Neo; +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.IO.Caching; +using System.Buffers.Binary; + +namespace Neo.Network.P2P; + +/// +/// Represents a message on the NEO network. +/// +public class Message : ISerializable +{ + /// + /// Indicates the maximum size of . + /// + public const int PayloadMaxSize = 0x02000000; + + private const int CompressionMinSize = 128; + private const int CompressionThreshold = 64; + + /// + /// The flags of the message. + /// + public MessageFlags Flags; + + /// + /// The command of the message. + /// + public MessageCommand Command; + + /// + /// The payload of the message. + /// + public ISerializable? Payload; + + private ReadOnlyMemory _payloadRaw; + + private ReadOnlyMemory _payloadCompressed; + + /// + /// True if the message is compressed + /// + public bool IsCompressed => Flags.HasFlag(MessageFlags.Compressed); + + public int Size => sizeof(MessageFlags) + sizeof(MessageCommand) + _payloadCompressed.GetVarSize(); + + /// + /// True if the message should be compressed + /// + /// Command + /// True if allow the compression + private static bool ShallICompress(MessageCommand command) + { + return + command == MessageCommand.Block || + command == MessageCommand.Extensible || + command == MessageCommand.Transaction || + command == MessageCommand.Headers || + command == MessageCommand.Addr || + command == MessageCommand.MerkleBlock || + command == MessageCommand.FilterLoad || + command == MessageCommand.FilterAdd; + } + + /// + /// Creates a new instance of the class. + /// + /// The command of the message. + /// The payload of the message. For the messages that don't require a payload, it should be . + /// + public static Message Create(MessageCommand command, ISerializable? payload = null) + { + var tryCompression = ShallICompress(command); + + Message message = new() + { + Flags = MessageFlags.None, + Command = command, + Payload = payload, + _payloadRaw = payload?.ToArray() ?? Array.Empty() + }; + + message._payloadCompressed = message._payloadRaw; + + // Try compression + if (tryCompression && message._payloadCompressed.Length > CompressionMinSize) + { + var compressed = message._payloadCompressed.Span.CompressLz4(); + if (compressed.Length < message._payloadCompressed.Length - CompressionThreshold) + { + message._payloadCompressed = compressed; + message.Flags |= MessageFlags.Compressed; + } + } + + return message; + } + + private void DecompressPayload() + { + if (_payloadCompressed.Length == 0) return; + var decompressed = Flags.HasFlag(MessageFlags.Compressed) + ? _payloadCompressed.Span.DecompressLz4(PayloadMaxSize) + : _payloadCompressed; + Payload = ReflectionCache.CreateSerializable(Command, decompressed); + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Flags = (MessageFlags)reader.ReadByte(); + Command = (MessageCommand)reader.ReadByte(); + _payloadCompressed = reader.ReadVarMemory(PayloadMaxSize); + DecompressPayload(); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write((byte)Flags); + writer.Write((byte)Command); + writer.WriteVarBytes(_payloadCompressed.Span); + } + + public byte[] ToArray(bool enablecompression) + { + if (enablecompression || !IsCompressed) + { + return this.ToArray(); + } + else + { + // Avoid compression + + using MemoryStream ms = new(); + using BinaryWriter writer = new(ms, Utility.StrictUTF8, true); + + writer.Write((byte)(Flags & ~MessageFlags.Compressed)); + writer.Write((byte)Command); + writer.WriteVarBytes(_payloadRaw.Span); + + writer.Flush(); + return ms.ToArray(); + } + } + + internal static int TryDeserialize(ByteString data, out Message? msg) + { + msg = null; + if (data.Count < 3) return 0; + + var header = data.Slice(0, 3).ToArray(); + var flags = (MessageFlags)header[0]; + ulong length = header[2]; + var payloadIndex = 3; + + if (length == 0xFD) + { + if (data.Count < 5) return 0; + length = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(payloadIndex, 2).ToArray()); + payloadIndex += 2; + } + else if (length == 0xFE) + { + if (data.Count < 7) return 0; + length = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(payloadIndex, 4).ToArray()); + payloadIndex += 4; + } + else if (length == 0xFF) + { + if (data.Count < 11) return 0; + length = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(payloadIndex, 8).ToArray()); + payloadIndex += 8; + } + + if (length > PayloadMaxSize) throw new FormatException($"Invalid payload length: {length}. The payload size exceeds the maximum allowed size of {PayloadMaxSize} bytes."); + + if (data.Count < (int)length + payloadIndex) return 0; + + msg = new Message() + { + Flags = flags, + Command = (MessageCommand)header[1], + _payloadCompressed = length <= 0 ? ReadOnlyMemory.Empty : data.Slice(payloadIndex, (int)length).ToArray() + }; + msg.DecompressPayload(); + + return payloadIndex + (int)length; + } +} diff --git a/src/Neo/Network/P2P/MessageCommand.cs b/src/Neo/Network/P2P/MessageCommand.cs new file mode 100644 index 0000000000..2e8324f9a4 --- /dev/null +++ b/src/Neo/Network/P2P/MessageCommand.cs @@ -0,0 +1,174 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MessageCommand.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO.Caching; +using Neo.Network.P2P.Payloads; + +namespace Neo.Network.P2P; + +/// +/// Represents the command of a message. +/// +public enum MessageCommand : byte +{ + #region handshaking + + /// + /// Sent when a connection is established. + /// + [ReflectionCache(typeof(VersionPayload))] + Version = 0x00, + + /// + /// Sent to respond to messages. + /// + Verack = 0x01, + + #endregion + + #region connectivity + + /// + /// Sent to request for remote nodes. + /// + GetAddr = 0x10, + + /// + /// Sent to respond to messages. + /// + [ReflectionCache(typeof(AddrPayload))] + Addr = 0x11, + + /// + /// Sent to detect whether the connection has been disconnected. + /// + [ReflectionCache(typeof(PingPayload))] + Ping = 0x18, + + /// + /// Sent to respond to messages. + /// + [ReflectionCache(typeof(PingPayload))] + Pong = 0x19, + + #endregion + + #region synchronization + + /// + /// Sent to request for headers. + /// + [ReflectionCache(typeof(GetBlockByIndexPayload))] + GetHeaders = 0x20, + + /// + /// Sent to respond to messages. + /// + [ReflectionCache(typeof(HeadersPayload))] + Headers = 0x21, + + /// + /// Sent to request for blocks. + /// + [ReflectionCache(typeof(GetBlocksPayload))] + GetBlocks = 0x24, + + /// + /// Sent to request for memory pool. + /// + Mempool = 0x25, + + /// + /// Sent to relay inventories. + /// + [ReflectionCache(typeof(InvPayload))] + Inv = 0x27, + + /// + /// Sent to request for inventories. + /// + [ReflectionCache(typeof(InvPayload))] + GetData = 0x28, + + /// + /// Sent to request for blocks. + /// + [ReflectionCache(typeof(GetBlockByIndexPayload))] + GetBlockByIndex = 0x29, + + /// + /// Sent to respond to messages when the inventories are not found. + /// + [ReflectionCache(typeof(InvPayload))] + NotFound = 0x2a, + + /// + /// Sent to send a transaction. + /// + [ReflectionCache(typeof(Transaction))] + Transaction = 0x2b, + + /// + /// Sent to send a block. + /// + [ReflectionCache(typeof(Block))] + Block = 0x2c, + + /// + /// Sent to send an . + /// + [ReflectionCache(typeof(ExtensiblePayload))] + Extensible = 0x2e, + + /// + /// Sent to reject an inventory. + /// + Reject = 0x2f, + + #endregion + + #region SPV protocol + + /// + /// Sent to load the . + /// + [ReflectionCache(typeof(FilterLoadPayload))] + FilterLoad = 0x30, + + /// + /// Sent to update the items for the . + /// + [ReflectionCache(typeof(FilterAddPayload))] + FilterAdd = 0x31, + + /// + /// Sent to clear the . + /// + FilterClear = 0x32, + + /// + /// Sent to send a filtered block. + /// + [ReflectionCache(typeof(MerkleBlockPayload))] + MerkleBlock = 0x38, + + #endregion + + #region others + + /// + /// Sent to send an alert. + /// + Alert = 0x40, + + #endregion +} diff --git a/src/Neo/Network/P2P/MessageFlags.cs b/src/Neo/Network/P2P/MessageFlags.cs new file mode 100644 index 0000000000..c34907f5b8 --- /dev/null +++ b/src/Neo/Network/P2P/MessageFlags.cs @@ -0,0 +1,29 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MessageFlags.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Network.P2P; + +/// +/// Represents the flags of a message. +/// +[Flags] +public enum MessageFlags : byte +{ + /// + /// No flag is set for the message. + /// + None = 0, + + /// + /// Indicates that the message is compressed. + /// + Compressed = 1 << 0 +} diff --git a/src/Neo/Network/P2P/Payloads/AddrPayload.cs b/src/Neo/Network/P2P/Payloads/AddrPayload.cs new file mode 100644 index 0000000000..ebbf88171e --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/AddrPayload.cs @@ -0,0 +1,60 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// AddrPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; + +namespace Neo.Network.P2P.Payloads; + +/// +/// This message is sent to respond to messages. +/// +public class AddrPayload : ISerializable +{ + /// + /// Indicates the maximum number of nodes sent each time. + /// + public const int MaxCountToSend = 200; + + /// + /// The list of nodes. + /// + public required NetworkAddressWithTime[] AddressList; + + public int Size => AddressList.GetVarSize(); + + /// + /// Creates a new instance of the class. + /// + /// The list of nodes. + /// The created payload. + public static AddrPayload Create(params NetworkAddressWithTime[] addresses) + { + return new AddrPayload + { + AddressList = addresses + }; + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + AddressList = reader.ReadSerializableArray(MaxCountToSend); + if (AddressList.Length == 0) + throw new FormatException("`AddressList` in AddrPayload is empty"); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(AddressList); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Block.cs b/src/Neo/Network/P2P/Payloads/Block.cs new file mode 100644 index 0000000000..cfb4e1b355 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Block.cs @@ -0,0 +1,177 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Block.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Ledger; +using Neo.Persistence; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents a block. +/// +public sealed class Block : IEquatable, IInventory +{ + /// + /// The header of the block. + /// + public required Header Header; + + /// + /// The transaction list of the block. + /// + public required Transaction[] Transactions; + + /// + public UInt256 Hash => Header.Hash; + + /// + /// The version of the block. + /// + public uint Version => Header.Version; + + /// + /// The hash of the previous block. + /// + public UInt256 PrevHash => Header.PrevHash; + + /// + /// The merkle root of the transactions. + /// + public UInt256 MerkleRoot => Header.MerkleRoot; + + /// + /// The timestamp of the block. + /// + public ulong Timestamp => Header.Timestamp; + + /// + /// The random number of the block. + /// + public ulong Nonce => Header.Nonce; + + /// + /// The index of the block. + /// + public uint Index => Header.Index; + + /// + /// The primary index of the consensus node that generated this block. + /// + public byte PrimaryIndex => Header.PrimaryIndex; + + /// + /// The multi-signature address of the consensus nodes that generates the next block. + /// + public UInt160 NextConsensus => Header.NextConsensus; + + /// + /// The witness of the block. + /// + public Witness Witness => Header.Witness; + + InventoryType IInventory.InventoryType => InventoryType.Block; + + public int Size => Header.Size + Transactions.GetVarSize(); + + Witness[] IVerifiable.Witnesses + { + get => ((IVerifiable)Header).Witnesses; + set => throw new NotSupportedException(); + } + + public void Deserialize(ref MemoryReader reader) + { + Header = reader.ReadSerializable
(); + Transactions = DeserializeTransactions(ref reader, ushort.MaxValue, Header.MerkleRoot); + } + + private static Transaction[] DeserializeTransactions(ref MemoryReader reader, int maxCount, UInt256 merkleRoot) + { + var count = (int)reader.ReadVarInt((ulong)maxCount); + var hashes = new UInt256[count]; + var txs = new Transaction[count]; + + if (count > 0) + { + var hashset = new HashSet(); + for (var i = 0; i < count; i++) + { + var tx = reader.ReadSerializable(); + if (!hashset.Add(tx.Hash)) + throw new FormatException($"TxHash({tx.Hash}) in Block is duplicate"); + txs[i] = tx; + hashes[i] = tx.Hash; + } + } + + if (MerkleTree.ComputeRoot(hashes) != merkleRoot) + throw new FormatException("The computed Merkle root does not match the expected value."); + return txs; + } + + void IVerifiable.DeserializeUnsigned(ref MemoryReader reader) => throw new NotSupportedException(); + + public bool Equals(Block? other) + { + if (ReferenceEquals(this, other)) return true; + if (other is null) return false; + return Hash.Equals(other.Hash); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Block); + } + + public override int GetHashCode() + { + return Hash.GetHashCode(); + } + + UInt160[] IVerifiable.GetScriptHashesForVerifying(IReadOnlyStore? snapshot) => ((IVerifiable)Header).GetScriptHashesForVerifying(snapshot); + + public void Serialize(BinaryWriter writer) + { + writer.Write(Header); + writer.Write(Transactions); + } + + void IVerifiable.SerializeUnsigned(BinaryWriter writer) => ((IVerifiable)Header).SerializeUnsigned(writer); + + /// + /// Converts the block to a JSON object. + /// + /// The used during the conversion. + /// The block represented by a JSON object. + public JObject ToJson(ProtocolSettings settings) + { + var json = Header.ToJson(settings); + json["size"] = Size; + json["tx"] = Transactions.Select(p => p.ToJson(settings)).ToArray(); + return json; + } + + internal bool Verify(ProtocolSettings settings, DataCache snapshot) + { + return Header.Verify(settings, snapshot); + } + + internal bool Verify(ProtocolSettings settings, DataCache snapshot, HeaderCache headerCache) + { + return Header.Verify(settings, snapshot, headerCache); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/AndCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/AndCondition.cs new file mode 100644 index 0000000000..78e85aa457 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/AndCondition.cs @@ -0,0 +1,117 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// AndCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads.Conditions; + +/// +/// Represents the condition that all conditions must be met. +/// +public class AndCondition : WitnessCondition, IEquatable +{ + /// + /// The expressions of the condition. + /// + public required WitnessCondition[] Expressions; + + public override int Size => base.Size + Expressions.GetVarSize(); + public override WitnessConditionType Type => WitnessConditionType.And; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(AndCondition? other) + { + if (ReferenceEquals(this, other)) + return true; + if (other is null) return false; + return + Type == other.Type && + Expressions.SequenceEqual(other.Expressions); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is AndCondition ac && Equals(ac); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, Expressions); + } + + protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) + { + Expressions = DeserializeConditions(ref reader, maxNestDepth); + if (Expressions.Length == 0) throw new FormatException("`Expressions` in AndCondition is empty"); + } + + public override bool Match(ApplicationEngine engine) + { + return Expressions.All(p => p.Match(engine)); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Expressions); + } + + private protected override void ParseJson(JObject json, int maxNestDepth) + { + JArray expressions = (JArray)json["expressions"]!; + if (expressions.Count > MaxSubitems) + throw new FormatException($"`expressions`({expressions.Count}) in AndCondition is out of range (max:{MaxSubitems})"); + Expressions = expressions.Select(p => FromJson((JObject)p!, maxNestDepth - 1)).ToArray(); + if (Expressions.Length == 0) throw new FormatException("`Expressions` in AndCondition is empty"); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["expressions"] = Expressions.Select(p => p.ToJson()).ToArray(); + return json; + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + var result = (Array)base.ToStackItem(referenceCounter); + result.Add(new Array(referenceCounter, Expressions.Select(p => p.ToStackItem(referenceCounter)))); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(AndCondition left, AndCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(AndCondition left, AndCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/BooleanCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/BooleanCondition.cs new file mode 100644 index 0000000000..bddf83f39b --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/BooleanCondition.cs @@ -0,0 +1,106 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BooleanCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads.Conditions; + +public class BooleanCondition : WitnessCondition, IEquatable +{ + /// + /// The expression of the . + /// + public bool Expression; + + public override int Size => base.Size + sizeof(bool); + public override WitnessConditionType Type => WitnessConditionType.Boolean; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(BooleanCondition? other) + { + if (ReferenceEquals(this, other)) + return true; + if (other is null) return false; + return + Type == other.Type && + Expression == other.Expression; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is BooleanCondition bc && Equals(bc); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, Expression); + } + + protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) + { + Expression = reader.ReadBoolean(); + } + + public override bool Match(ApplicationEngine engine) + { + return Expression; + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Expression); + } + + private protected override void ParseJson(JObject json, int maxNestDepth) + { + Expression = json["expression"]!.GetBoolean(); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["expression"] = Expression; + return json; + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + var result = (Array)base.ToStackItem(referenceCounter); + result.Add(Expression); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(BooleanCondition left, BooleanCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(BooleanCondition left, BooleanCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/CalledByContractCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/CalledByContractCondition.cs new file mode 100644 index 0000000000..eb55d0b3b3 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/CalledByContractCondition.cs @@ -0,0 +1,107 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CalledByContractCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads.Conditions; + +public class CalledByContractCondition : WitnessCondition, IEquatable +{ + /// + /// The script hash to be checked. + /// + public required UInt160 Hash; + + public override int Size => base.Size + UInt160.Length; + public override WitnessConditionType Type => WitnessConditionType.CalledByContract; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CalledByContractCondition? other) + { + if (ReferenceEquals(this, other)) + return true; + if (other is null) return false; + return + Type == other.Type && + Hash == other.Hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is CalledByContractCondition cc && Equals(cc); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, Hash.GetHashCode()); + } + + protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) + { + Hash = reader.ReadSerializable(); + } + + public override bool Match(ApplicationEngine engine) + { + return engine.CallingScriptHash == Hash; + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Hash); + } + + private protected override void ParseJson(JObject json, int maxNestDepth) + { + Hash = UInt160.Parse(json["hash"]!.GetString()); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["hash"] = Hash.ToString(); + return json; + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + var result = (Array)base.ToStackItem(referenceCounter); + result.Add(Hash.ToArray()); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(CalledByContractCondition left, CalledByContractCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(CalledByContractCondition left, CalledByContractCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/CalledByEntryCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/CalledByEntryCondition.cs new file mode 100644 index 0000000000..80346657fb --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/CalledByEntryCondition.cs @@ -0,0 +1,75 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CalledByEntryCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract; +using System.Runtime.CompilerServices; + +namespace Neo.Network.P2P.Payloads.Conditions; + +public class CalledByEntryCondition : WitnessCondition, IEquatable +{ + public override WitnessConditionType Type => WitnessConditionType.CalledByEntry; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CalledByEntryCondition? other) + { + if (ReferenceEquals(this, other)) + return true; + if (other is null) return false; + return Type == other.Type; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is CalledByEntryCondition cc && Equals(cc); + } + + public override int GetHashCode() + { + return (byte)Type; + } + + public override bool Match(ApplicationEngine engine) + { + var state = engine.CurrentContext!.GetState(); + if (state.CallingContext is null) return true; + state = state.CallingContext.GetState(); + return state.CallingContext is null; + } + + protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) { } + + protected override void SerializeWithoutType(BinaryWriter writer) { } + + private protected override void ParseJson(JObject json, int maxNestDepth) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(CalledByEntryCondition left, CalledByEntryCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(CalledByEntryCondition left, CalledByEntryCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/CalledByGroupCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/CalledByGroupCondition.cs new file mode 100644 index 0000000000..8d471d189f --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/CalledByGroupCondition.cs @@ -0,0 +1,112 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CalledByGroupCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads.Conditions; + +public class CalledByGroupCondition : WitnessCondition, IEquatable +{ + /// + /// The group to be checked. + /// + public required ECPoint Group; + + public override int Size => base.Size + Group.Size; + public override WitnessConditionType Type => WitnessConditionType.CalledByGroup; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CalledByGroupCondition? other) + { + if (ReferenceEquals(this, other)) + return true; + if (other is null) return false; + return + Type == other.Type && + Group == other.Group; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is CalledByGroupCondition cc && Equals(cc); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, Group); + } + + protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) + { + Group = reader.ReadSerializable(); + } + + public override bool Match(ApplicationEngine engine) + { + engine.ValidateCallFlags(CallFlags.ReadStates); + if (engine.CallingScriptHash is null) return false; + ContractState? contract = NativeContract.ContractManagement.GetContract(engine.SnapshotCache, engine.CallingScriptHash); + return contract is not null && contract.Manifest.Groups.Any(p => p.PubKey.Equals(Group)); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Group); + } + + private protected override void ParseJson(JObject json, int maxNestDepth) + { + Group = ECPoint.Parse(json["group"]!.GetString(), ECCurve.Secp256r1); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["group"] = Group.ToString(); + return json; + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + var result = (Array)base.ToStackItem(referenceCounter); + result.Add(Group.ToArray()); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(CalledByGroupCondition left, CalledByGroupCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(CalledByGroupCondition left, CalledByGroupCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/GroupCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/GroupCondition.cs new file mode 100644 index 0000000000..318a33c83c --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/GroupCondition.cs @@ -0,0 +1,111 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// GroupCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads.Conditions; + +public class GroupCondition : WitnessCondition, IEquatable +{ + /// + /// The group to be checked. + /// + public required ECPoint Group; + + public override int Size => base.Size + Group.Size; + public override WitnessConditionType Type => WitnessConditionType.Group; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(GroupCondition? other) + { + if (ReferenceEquals(this, other)) + return true; + if (other is null) return false; + return + Type == other.Type && + Group == other.Group; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is GroupCondition gc && Equals(gc); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, Group); + } + + protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) + { + Group = reader.ReadSerializable(); + } + + public override bool Match(ApplicationEngine engine) + { + engine.ValidateCallFlags(CallFlags.ReadStates); + ContractState? contract = NativeContract.ContractManagement.GetContract(engine.SnapshotCache, engine.CurrentScriptHash!); + return contract is not null && contract.Manifest.Groups.Any(p => p.PubKey.Equals(Group)); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Group); + } + + private protected override void ParseJson(JObject json, int maxNestDepth) + { + Group = ECPoint.Parse(json["group"]!.GetString(), ECCurve.Secp256r1); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["group"] = Group.ToString(); + return json; + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + var result = (Array)base.ToStackItem(referenceCounter); + result.Add(Group.ToArray()); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(GroupCondition left, GroupCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(GroupCondition left, GroupCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/NotCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/NotCondition.cs new file mode 100644 index 0000000000..bc252d34a4 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/NotCondition.cs @@ -0,0 +1,110 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NotCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads.Conditions; + +/// +/// Reverse another condition. +/// +public class NotCondition : WitnessCondition, IEquatable +{ + /// + /// The expression of the condition to be reversed. + /// + public required WitnessCondition Expression; + + public override int Size => base.Size + Expression.Size; + public override WitnessConditionType Type => WitnessConditionType.Not; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(NotCondition? other) + { + if (ReferenceEquals(this, other)) + return true; + if (other is null) return false; + return + Type == other.Type && + Expression.Equals(other.Expression); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is NotCondition nc && Equals(nc); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, Expression.GetHashCode()); + } + + protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) + { + Expression = DeserializeFrom(ref reader, maxNestDepth - 1); + } + + public override bool Match(ApplicationEngine engine) + { + return !Expression.Match(engine); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Expression); + } + + private protected override void ParseJson(JObject json, int maxNestDepth) + { + Expression = FromJson((JObject)json["expression"]!, maxNestDepth - 1); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["expression"] = Expression.ToJson(); + return json; + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + var result = (Array)base.ToStackItem(referenceCounter); + result.Add(Expression.ToStackItem(referenceCounter)); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(NotCondition left, NotCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(NotCondition left, NotCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/OrCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/OrCondition.cs new file mode 100644 index 0000000000..5b29c9388c --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/OrCondition.cs @@ -0,0 +1,117 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// OrCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads.Conditions; + +/// +/// Represents the condition that any of the conditions meets. +/// +public class OrCondition : WitnessCondition, IEquatable +{ + /// + /// The expressions of the condition. + /// + public required WitnessCondition[] Expressions; + + public override int Size => base.Size + Expressions.GetVarSize(); + public override WitnessConditionType Type => WitnessConditionType.Or; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(OrCondition? other) + { + if (ReferenceEquals(this, other)) + return true; + if (other is null) return false; + return + Type == other.Type && + Expressions.SequenceEqual(other.Expressions); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is OrCondition oc && Equals(oc); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, Expressions); + } + + protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) + { + Expressions = DeserializeConditions(ref reader, maxNestDepth); + if (Expressions.Length == 0) throw new FormatException("`Expressions` in OrCondition is empty"); + } + + public override bool Match(ApplicationEngine engine) + { + return Expressions.Any(p => p.Match(engine)); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Expressions); + } + + private protected override void ParseJson(JObject json, int maxNestDepth) + { + JArray expressions = (JArray)json["expressions"]!; + if (expressions.Count > MaxSubitems) + throw new FormatException($"`expressions`({expressions.Count}) in OrCondition is out of range (max:{MaxSubitems})"); + Expressions = expressions.Select(p => FromJson((JObject)p!, maxNestDepth - 1)).ToArray(); + if (Expressions.Length == 0) throw new FormatException("`Expressions` in OrCondition is empty"); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["expressions"] = Expressions.Select(p => p.ToJson()).ToArray(); + return json; + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + var result = (Array)base.ToStackItem(referenceCounter); + result.Add(new Array(referenceCounter, Expressions.Select(p => p.ToStackItem(referenceCounter)))); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(OrCondition left, OrCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(OrCondition left, OrCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/ScriptHashCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/ScriptHashCondition.cs new file mode 100644 index 0000000000..2372683e30 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/ScriptHashCondition.cs @@ -0,0 +1,107 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ScriptHashCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads.Conditions; + +public class ScriptHashCondition : WitnessCondition, IEquatable +{ + /// + /// The script hash to be checked. + /// + public required UInt160 Hash; + + public override int Size => base.Size + UInt160.Length; + public override WitnessConditionType Type => WitnessConditionType.ScriptHash; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(ScriptHashCondition? other) + { + if (ReferenceEquals(this, other)) + return true; + if (other is null) return false; + return + Type == other.Type && + Hash == other.Hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is ScriptHashCondition sc && Equals(sc); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, Hash); + } + + protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) + { + Hash = reader.ReadSerializable(); + } + + public override bool Match(ApplicationEngine engine) + { + return engine.CurrentScriptHash == Hash; + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Hash); + } + + private protected override void ParseJson(JObject json, int maxNestDepth) + { + Hash = UInt160.Parse(json["hash"]!.GetString()); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["hash"] = Hash.ToString(); + return json; + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + var result = (Array)base.ToStackItem(referenceCounter); + result.Add(Hash.ToArray()); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(ScriptHashCondition left, ScriptHashCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(ScriptHashCondition left, ScriptHashCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/WitnessCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/WitnessCondition.cs new file mode 100644 index 0000000000..051f2e9c73 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/WitnessCondition.cs @@ -0,0 +1,162 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WitnessCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO.Caching; +using Neo.Json; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads.Conditions; + +public abstract class WitnessCondition : IInteroperable, ISerializable +{ + internal const int MaxSubitems = 16; + internal const int MaxNestingDepth = 3; + + /// + /// The type of the . + /// + public abstract WitnessConditionType Type { get; } + + public virtual int Size => sizeof(WitnessConditionType); + + public abstract override bool Equals(object? obj); + + public abstract override int GetHashCode(); + + void ISerializable.Deserialize(ref MemoryReader reader) + { + var readType = reader.ReadByte(); + if (readType != (byte)Type) + throw new FormatException($"Read type({readType}) does not match WitnessConditionType({Type})"); + DeserializeWithoutType(ref reader, MaxNestingDepth); + } + + /// + /// Deserializes an array from a . + /// + /// The for reading data. + /// The maximum nesting depth allowed during deserialization. + /// The deserialized array. + protected static WitnessCondition[] DeserializeConditions(ref MemoryReader reader, int maxNestDepth) + { + WitnessCondition[] conditions = new WitnessCondition[reader.ReadVarInt(MaxSubitems)]; + for (int i = 0; i < conditions.Length; i++) + conditions[i] = DeserializeFrom(ref reader, maxNestDepth - 1); + return conditions; + } + + /// + /// Deserializes an object from a . + /// + /// The for reading data. + /// The maximum nesting depth allowed during deserialization. + /// The deserialized . + public static WitnessCondition DeserializeFrom(ref MemoryReader reader, int maxNestDepth) + { + if (maxNestDepth <= 0) + throw new FormatException($"`maxNestDepth`({maxNestDepth}) in WitnessCondition is out of range (min:1)"); + WitnessConditionType type = (WitnessConditionType)reader.ReadByte(); + if (ReflectionCache.CreateInstance(type) is not WitnessCondition condition) + throw new FormatException($"Invalid WitnessConditionType({type})"); + condition.DeserializeWithoutType(ref reader, maxNestDepth); + return condition; + } + + /// + /// Deserializes the object from a . + /// + /// The for reading data. + /// The maximum nesting depth allowed during deserialization. + protected abstract void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth); + + /// + /// Checks whether the current context matches the condition. + /// + /// The that is executing CheckWitness. + /// if the condition matches; otherwise, . + public abstract bool Match(ApplicationEngine engine); + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write((byte)Type); + SerializeWithoutType(writer); + } + + /// + /// Serializes the object to a . + /// + /// The for writing data. + protected abstract void SerializeWithoutType(BinaryWriter writer); + + private protected abstract void ParseJson(JObject json, int maxNestDepth); + + /// + /// Converts the from a JSON object. + /// + /// The represented by a JSON object. + /// The maximum nesting depth allowed during deserialization. + /// The converted . + public static WitnessCondition FromJson(JObject json, int maxNestDepth) + { + if (maxNestDepth <= 0) + throw new FormatException($"`maxNestDepth`({maxNestDepth}) in WitnessCondition is out of range (min:1)"); + WitnessConditionType type = Enum.Parse(json["type"]!.GetString()); + if (ReflectionCache.CreateInstance(type) is not WitnessCondition condition) + throw new FormatException($"Invalid WitnessConditionType({type})"); + condition.ParseJson(json, maxNestDepth); + return condition; + } + + /// + /// Converts the condition to a JSON object. + /// + /// The condition represented by a JSON object. + public virtual JObject ToJson() + { + return new JObject + { + ["type"] = Type + }; + } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + throw new NotSupportedException(); + } + + public virtual StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Array(referenceCounter, new StackItem[] { (byte)Type }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(WitnessCondition left, WitnessCondition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(WitnessCondition left, WitnessCondition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Conditions/WitnessConditionType.cs b/src/Neo/Network/P2P/Payloads/Conditions/WitnessConditionType.cs new file mode 100644 index 0000000000..402b38e431 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conditions/WitnessConditionType.cs @@ -0,0 +1,74 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WitnessConditionType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; + +namespace Neo.Network.P2P.Payloads.Conditions; + +/// +/// Represents the type of . +/// +public enum WitnessConditionType : byte +{ + /// + /// Indicates that the condition will always be met or not met. + /// + [ReflectionCache(typeof(BooleanCondition))] + Boolean = 0x00, + + /// + /// Reverse another condition. + /// + [ReflectionCache(typeof(NotCondition))] + Not = 0x01, + + /// + /// Indicates that all conditions must be met. + /// + [ReflectionCache(typeof(AndCondition))] + And = 0x02, + + /// + /// Indicates that any of the conditions meets. + /// + [ReflectionCache(typeof(OrCondition))] + Or = 0x03, + + /// + /// Indicates that the condition is met when the current context has the specified script hash. + /// + [ReflectionCache(typeof(ScriptHashCondition))] + ScriptHash = 0x18, + + /// + /// Indicates that the condition is met when the current context has the specified group. + /// + [ReflectionCache(typeof(GroupCondition))] + Group = 0x19, + + /// + /// Indicates that the condition is met when the current context is the entry point or is called by the entry point. + /// + [ReflectionCache(typeof(CalledByEntryCondition))] + CalledByEntry = 0x20, + + /// + /// Indicates that the condition is met when the current context is called by the specified contract. + /// + [ReflectionCache(typeof(CalledByContractCondition))] + CalledByContract = 0x28, + + /// + /// Indicates that the condition is met when the current context is called by the specified group. + /// + [ReflectionCache(typeof(CalledByGroupCondition))] + CalledByGroup = 0x29 +} diff --git a/src/Neo/Network/P2P/Payloads/Conflicts.cs b/src/Neo/Network/P2P/Payloads/Conflicts.cs new file mode 100644 index 0000000000..55346ca97e --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Conflicts.cs @@ -0,0 +1,62 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Conflicts.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Persistence; +using Neo.SmartContract.Native; + +namespace Neo.Network.P2P.Payloads; + +public class Conflicts : TransactionAttribute +{ + /// + /// Indicates the conflict transaction hash. + /// + public required UInt256 Hash; + + public override TransactionAttributeType Type => TransactionAttributeType.Conflicts; + + public override bool AllowMultiple => true; + + public override int Size => base.Size + Hash.Size; + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + Hash = reader.ReadSerializable(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Hash); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["hash"] = Hash.ToString(); + return json; + } + + public override bool Verify(DataCache snapshot, Transaction tx) + { + // Only check if conflicting transaction is on chain. It's OK if the + // conflicting transaction was in the Conflicts attribute of some other + // on-chain transaction. + return !NativeContract.Ledger.ContainsTransaction(snapshot, Hash); + } + + public override long CalculateNetworkFee(DataCache snapshot, Transaction tx) + { + return tx.Signers.Length * base.CalculateNetworkFee(snapshot, tx); + } +} diff --git a/src/Neo/Network/P2P/Payloads/ExtensiblePayload.cs b/src/Neo/Network/P2P/Payloads/ExtensiblePayload.cs new file mode 100644 index 0000000000..1183bfab94 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/ExtensiblePayload.cs @@ -0,0 +1,134 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ExtensiblePayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents an extensible message that can be relayed. +/// +public class ExtensiblePayload : IInventory +{ + /// + /// The category of the extension. + /// + public required string Category; + + /// + /// Indicates that the payload is only valid when the block height is greater than or equal to this value. + /// + public uint ValidBlockStart; + + /// + /// Indicates that the payload is only valid when the block height is less than this value. + /// + public uint ValidBlockEnd; + + /// + /// The sender of the payload. + /// + public required UInt160 Sender; + + /// + /// The data of the payload. + /// + public ReadOnlyMemory Data; + + /// + /// The witness of the payload. It must match the . + /// + public required Witness Witness; + + private UInt256? _hash = null; + + /// + public UInt256 Hash => _hash ??= this.CalculateHash(); + + InventoryType IInventory.InventoryType => InventoryType.Extensible; + + public int Size => + Category.GetVarSize() + // Category + sizeof(uint) + // ValidBlockStart + sizeof(uint) + // ValidBlockEnd + UInt160.Length + // Sender + Data.GetVarSize() + // Data + (Witness is null ? 1 : 1 + Witness.Size); // Witness, cannot be null for valid payload + + Witness[] IVerifiable.Witnesses + { + get + { + return new[] { Witness }; + } + set + { + if (value.Length != 1) + throw new ArgumentException($"Expected 1 witness, got {value.Length}.", nameof(value)); + Witness = value[0]; + } + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + ((IVerifiable)this).DeserializeUnsigned(ref reader); + var count = reader.ReadByte(); + if (count != 1) + throw new FormatException($"Expected 1 witness, got {count}."); + Witness = reader.ReadSerializable(); + } + + void IVerifiable.DeserializeUnsigned(ref MemoryReader reader) + { + Category = reader.ReadVarString(32); + ValidBlockStart = reader.ReadUInt32(); + ValidBlockEnd = reader.ReadUInt32(); + if (ValidBlockStart >= ValidBlockEnd) + throw new FormatException($"Invalid valid block range: {ValidBlockStart} >= {ValidBlockEnd}."); + Sender = reader.ReadSerializable(); + Data = reader.ReadVarMemory(); + } + + UInt160[] IVerifiable.GetScriptHashesForVerifying(IReadOnlyStore? snapshot) + { + return new[] { Sender }; // This address should be checked by consumer + } + + void ISerializable.Serialize(BinaryWriter writer) + { + ((IVerifiable)this).SerializeUnsigned(writer); + writer.Write((byte)1); + writer.Write(Witness); + } + + void IVerifiable.SerializeUnsigned(BinaryWriter writer) + { + writer.WriteVarString(Category); + writer.Write(ValidBlockStart); + writer.Write(ValidBlockEnd); + writer.Write(Sender); + writer.WriteVarBytes(Data.Span); + } + + internal bool Verify(ProtocolSettings settings, DataCache snapshot, ISet extensibleWitnessWhiteList) + { + uint height = NativeContract.Ledger.CurrentIndex(snapshot); + if (height < ValidBlockStart || height >= ValidBlockEnd) return false; + if (!extensibleWitnessWhiteList.Contains(Sender)) return false; + return this.VerifyWitnesses(settings, snapshot, 0_06000000L); + } +} diff --git a/src/Neo/Network/P2P/Payloads/FilterAddPayload.cs b/src/Neo/Network/P2P/Payloads/FilterAddPayload.cs new file mode 100644 index 0000000000..fd0a9b6228 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/FilterAddPayload.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FilterAddPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.IO; +using Neo.IO; + +namespace Neo.Network.P2P.Payloads; + +/// +/// This message is sent to update the items for the . +/// +public class FilterAddPayload : ISerializable +{ + /// + /// The items to be added. + /// + public ReadOnlyMemory Data; + + public int Size => Data.GetVarSize(); + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Data = reader.ReadVarMemory(520); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.WriteVarBytes(Data.Span); + } +} diff --git a/src/Neo/Network/P2P/Payloads/FilterLoadPayload.cs b/src/Neo/Network/P2P/Payloads/FilterLoadPayload.cs new file mode 100644 index 0000000000..1a59f19b6c --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/FilterLoadPayload.cs @@ -0,0 +1,72 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FilterLoadPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.IO; +using Neo.IO; + +namespace Neo.Network.P2P.Payloads; + +/// +/// This message is sent to load the . +/// +public class FilterLoadPayload : ISerializable +{ + /// + /// The data of the . + /// + public ReadOnlyMemory Filter; + + /// + /// The number of hash functions used by the . + /// + public byte K; + + /// + /// Used to generate the seeds of the murmur hash functions. + /// + public uint Tweak; + + public int Size => Filter.GetVarSize() + sizeof(byte) + sizeof(uint); + + /// + /// Creates a new instance of the class. + /// + /// The fields in the filter will be copied to the payload. + /// The created payload. + public static FilterLoadPayload Create(BloomFilter filter) + { + byte[] buffer = new byte[filter.M / 8]; + filter.GetBits(buffer); + return new FilterLoadPayload + { + Filter = buffer, + K = (byte)filter.K, + Tweak = filter.Tweak + }; + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Filter = reader.ReadVarMemory(36000); + K = reader.ReadByte(); + if (K > 50) throw new FormatException($"`K`({K}) is out of range [0, 50]"); + Tweak = reader.ReadUInt32(); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.WriteVarBytes(Filter.Span); + writer.Write(K); + writer.Write(Tweak); + } +} diff --git a/src/Neo/Network/P2P/Payloads/GetBlockByIndexPayload.cs b/src/Neo/Network/P2P/Payloads/GetBlockByIndexPayload.cs new file mode 100644 index 0000000000..375de4bf17 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/GetBlockByIndexPayload.cs @@ -0,0 +1,61 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// GetBlockByIndexPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Network.P2P.Payloads; + +/// +/// This message is sent to request for blocks by index. +/// +public class GetBlockByIndexPayload : ISerializable +{ + /// + /// The starting index of the blocks to request. + /// + public uint IndexStart; + + /// + /// The number of blocks to request. + /// + public short Count; + + public int Size => sizeof(uint) + sizeof(short); + + /// + /// Creates a new instance of the class. + /// + /// The starting index of the blocks to request. + /// The number of blocks to request. Set this parameter to -1 to request as many blocks as possible. + /// The created payload. + public static GetBlockByIndexPayload Create(uint indexStart, short count = -1) + { + return new GetBlockByIndexPayload + { + IndexStart = indexStart, + Count = count + }; + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + IndexStart = reader.ReadUInt32(); + Count = reader.ReadInt16(); + if (Count < -1 || Count == 0 || Count > HeadersPayload.MaxHeadersCount) + throw new FormatException($"Invalid count: {Count}/{HeadersPayload.MaxHeadersCount}."); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(IndexStart); + writer.Write(Count); + } +} diff --git a/src/Neo/Network/P2P/Payloads/GetBlocksPayload.cs b/src/Neo/Network/P2P/Payloads/GetBlocksPayload.cs new file mode 100644 index 0000000000..93da4f4a10 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/GetBlocksPayload.cs @@ -0,0 +1,62 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// GetBlocksPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; + +namespace Neo.Network.P2P.Payloads; + +/// +/// This message is sent to request for blocks by hash. +/// +public class GetBlocksPayload : ISerializable +{ + /// + /// The starting hash of the blocks to request. + /// + public required UInt256 HashStart; + + /// + /// The number of blocks to request. + /// + public short Count; + + public int Size => sizeof(short) + HashStart.Size; + + /// + /// Creates a new instance of the class. + /// + /// The starting hash of the blocks to request. + /// The number of blocks to request. Set this parameter to -1 to request as many blocks as possible. + /// The created payload. + public static GetBlocksPayload Create(UInt256 hashStart, short count = -1) + { + return new GetBlocksPayload + { + HashStart = hashStart, + Count = count + }; + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + HashStart = reader.ReadSerializable(); + Count = reader.ReadInt16(); + if (Count < -1 || Count == 0) + throw new FormatException($"Invalid count: {Count}."); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(HashStart); + writer.Write(Count); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Header.cs b/src/Neo/Network/P2P/Payloads/Header.cs new file mode 100644 index 0000000000..ac0d292a90 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Header.cs @@ -0,0 +1,243 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Header.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Ledger; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.Wallets; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents the header of a block. +/// +public sealed class Header : IEquatable
, IVerifiable +{ + /// + /// The version of the block. + /// + public uint Version { get; set { field = value; _hash = null; } } + + /// + /// The hash of the previous block. + /// + public required UInt256 PrevHash { get; set { field = value; _hash = null; } } + + /// + /// The merkle root of the transactions. + /// + public required UInt256 MerkleRoot { get; set { field = value; _hash = null; } } + + /// + /// The timestamp of the block. + /// + public ulong Timestamp { get; set { field = value; _hash = null; } } + + /// + /// The first eight bytes of random number generated. + /// + public ulong Nonce { get; set { field = value; _hash = null; } } + + /// + /// The index of the block. + /// + public uint Index { get; set { field = value; _hash = null; } } + + /// + /// The primary index of the consensus node that generated this block. + /// + public byte PrimaryIndex { get; set { field = value; _hash = null; } } + + /// + /// The multi-signature address of the consensus nodes that generates the next block. + /// + public required UInt160 NextConsensus { get; set { field = value; _hash = null; } } + + /// + /// The witness of the block. + /// + public required Witness Witness; + + private UInt256? _hash = null; + + /// + public UInt256 Hash + { + get + { + if (_hash == null) + { + _hash = this.CalculateHash(); + } + return _hash; + } + } + + public int Size => + sizeof(uint) + // Version + UInt256.Length + // PrevHash + UInt256.Length + // MerkleRoot + sizeof(ulong) + // Timestamp + sizeof(ulong) + // Nonce + sizeof(uint) + // Index + sizeof(byte) + // PrimaryIndex + UInt160.Length + // NextConsensus + (Witness is null ? 1 : 1 + Witness.Size); // Witness, cannot be null for valid header + + Witness[] IVerifiable.Witnesses + { + get + { + return new[] { Witness }; + } + set + { + if (value.Length != 1) + throw new ArgumentException($"Expected 1 witness, got {value.Length}.", nameof(value)); + Witness = value[0]; + } + } + + public void Deserialize(ref MemoryReader reader) + { + ((IVerifiable)this).DeserializeUnsigned(ref reader); + Witness[] witnesses = reader.ReadSerializableArray(1); + if (witnesses.Length != 1) throw new FormatException($"Expected 1 witness in Header, got {witnesses.Length}."); + Witness = witnesses[0]; + } + + void IVerifiable.DeserializeUnsigned(ref MemoryReader reader) + { + _hash = null; + Version = reader.ReadUInt32(); + if (Version > 0) throw new FormatException($"`version`({Version}) in Header must be 0"); + PrevHash = reader.ReadSerializable(); + MerkleRoot = reader.ReadSerializable(); + Timestamp = reader.ReadUInt64(); + Nonce = reader.ReadUInt64(); + Index = reader.ReadUInt32(); + PrimaryIndex = reader.ReadByte(); + NextConsensus = reader.ReadSerializable(); + } + + public bool Equals(Header? other) + { + if (other is null) return false; + if (ReferenceEquals(other, this)) return true; + return Hash.Equals(other.Hash); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Header); + } + + public override int GetHashCode() + { + return Hash.GetHashCode(); + } + + UInt160[] IVerifiable.GetScriptHashesForVerifying(IReadOnlyStore? snapshot) + { + if (PrevHash == UInt256.Zero) return [Witness.ScriptHash]; + ArgumentNullException.ThrowIfNull(snapshot); + + var prev = NativeContract.Ledger.GetTrimmedBlock(snapshot, PrevHash) + ?? throw new InvalidOperationException($"Block {PrevHash} was not found"); + return [prev.Header.NextConsensus]; + } + + public void Serialize(BinaryWriter writer) + { + ((IVerifiable)this).SerializeUnsigned(writer); + writer.Write(new Witness[] { Witness }); + } + + void IVerifiable.SerializeUnsigned(BinaryWriter writer) + { + writer.Write(Version); + writer.Write(PrevHash); + writer.Write(MerkleRoot); + writer.Write(Timestamp); + writer.Write(Nonce); + writer.Write(Index); + writer.Write(PrimaryIndex); + writer.Write(NextConsensus); + } + + /// + /// Converts the header to a JSON object. + /// + /// The used during the conversion. + /// The header represented by a JSON object. + public JObject ToJson(ProtocolSettings settings) + { + JObject json = new(); + json["hash"] = Hash.ToString(); + json["size"] = Size; + json["version"] = Version; + json["previousblockhash"] = PrevHash.ToString(); + json["merkleroot"] = MerkleRoot.ToString(); + json["time"] = Timestamp; + json["nonce"] = Nonce.ToString("X16"); + json["index"] = Index; + json["primary"] = PrimaryIndex; + json["nextconsensus"] = NextConsensus.ToAddress(settings.AddressVersion); + json["witnesses"] = new JArray(Witness.ToJson()); + return json; + } + + internal bool Verify(ProtocolSettings settings, DataCache snapshot) + { + if (PrimaryIndex >= settings.ValidatorsCount) + return false; + TrimmedBlock? prev = NativeContract.Ledger.GetTrimmedBlock(snapshot, PrevHash); + if (prev is null) return false; + if (prev.Index + 1 != Index) return false; + if (prev.Hash != PrevHash) return false; + if (prev.Header.Timestamp >= Timestamp) return false; + if (!this.VerifyWitnesses(settings, snapshot, 3_00000000L)) return false; + return true; + } + + internal bool Verify(ProtocolSettings settings, DataCache snapshot, HeaderCache headerCache) + { + Header? prev = headerCache.Last; + if (prev is null) return Verify(settings, snapshot); + if (PrimaryIndex >= settings.ValidatorsCount) + return false; + if (prev.Hash != PrevHash) return false; + if (prev.Index + 1 != Index) return false; + if (prev.Timestamp >= Timestamp) return false; + return this.VerifyWitness(settings, snapshot, prev.NextConsensus, Witness, 3_00000000L, out _); + } + + public Header Clone() + { + return new Header() + { + Version = Version, + PrevHash = PrevHash, + MerkleRoot = MerkleRoot, + Timestamp = Timestamp, + Nonce = Nonce, + Index = Index, + PrimaryIndex = PrimaryIndex, + NextConsensus = NextConsensus, + Witness = Witness.Clone(), + _hash = _hash + }; + } +} diff --git a/src/Neo/Network/P2P/Payloads/HeadersPayload.cs b/src/Neo/Network/P2P/Payloads/HeadersPayload.cs new file mode 100644 index 0000000000..a12e699c1b --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/HeadersPayload.cs @@ -0,0 +1,59 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// HeadersPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; + +namespace Neo.Network.P2P.Payloads; + +/// +/// This message is sent to respond to messages. +/// +public class HeadersPayload : ISerializable +{ + /// + /// Indicates the maximum number of headers sent each time. + /// + public const int MaxHeadersCount = 2000; + + /// + /// The list of headers. + /// + public required Header[] Headers; + + public int Size => Headers.GetVarSize(); + + /// + /// Creates a new instance of the class. + /// + /// The list of headers. + /// The created payload. + public static HeadersPayload Create(params Header[] headers) + { + return new HeadersPayload + { + Headers = headers + }; + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Headers = reader.ReadSerializableArray
(MaxHeadersCount); + if (Headers.Length == 0) throw new FormatException("`Headers` in HeadersPayload is empty"); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(Headers); + } +} diff --git a/src/Neo/Network/P2P/Payloads/HighPriorityAttribute.cs b/src/Neo/Network/P2P/Payloads/HighPriorityAttribute.cs new file mode 100644 index 0000000000..82ca745d6f --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/HighPriorityAttribute.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// HighPriorityAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract.Native; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Indicates that the transaction is of high priority. +/// +public class HighPriorityAttribute : TransactionAttribute +{ + public override bool AllowMultiple => false; + public override TransactionAttributeType Type => TransactionAttributeType.HighPriority; + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + } + + public override bool Verify(DataCache snapshot, Transaction tx) + { + UInt160 committee = NativeContract.NEO.GetCommitteeAddress(snapshot); + return tx.Signers.Any(p => p.Account.Equals(committee)); + } +} diff --git a/src/Neo/Network/P2P/Payloads/IInventory.cs b/src/Neo/Network/P2P/Payloads/IInventory.cs new file mode 100644 index 0000000000..0060214524 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/IInventory.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IInventory.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Network.P2P.Payloads; + +/// +/// Represents a message that can be relayed on the NEO network. +/// +public interface IInventory : IVerifiable +{ + /// + /// The type of the inventory. + /// + InventoryType InventoryType { get; } +} diff --git a/src/Neo/Network/P2P/Payloads/IVerifiable.cs b/src/Neo/Network/P2P/Payloads/IVerifiable.cs new file mode 100644 index 0000000000..736241710f --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/IVerifiable.cs @@ -0,0 +1,51 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IVerifiable.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents an object that can be verified in the NEO network. +/// +public interface IVerifiable : ISerializable +{ + /// + /// The hash of the object. + /// NOTE: This property may throw an exception if the object is not valid. + /// + UInt256 Hash => this.CalculateHash(); + + /// + /// The witnesses of the object. + /// + Witness[] Witnesses { get; set; } + + /// + /// Deserializes the part of the object other than . + /// + /// The for reading data. + void DeserializeUnsigned(ref MemoryReader reader); + + /// + /// Gets the script hashes that should be verified for this object. + /// + /// The snapshot to be used. + /// The script hashes that should be verified. + UInt160[] GetScriptHashesForVerifying(IReadOnlyStore? snapshot = null); + + /// + /// Serializes the part of the object other than . + /// + /// The for writing data. + void SerializeUnsigned(BinaryWriter writer); +} diff --git a/src/Neo/Network/P2P/Payloads/InvPayload.cs b/src/Neo/Network/P2P/Payloads/InvPayload.cs new file mode 100644 index 0000000000..447ab40781 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/InvPayload.cs @@ -0,0 +1,88 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// InvPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Collections; +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; + +namespace Neo.Network.P2P.Payloads; + +/// +/// This message is sent to relay inventories. +/// +public class InvPayload : ISerializable +{ + /// + /// Indicates the maximum number of inventories sent each time. + /// + public const int MaxHashesCount = 500; + + /// + /// The type of the inventories. + /// + public InventoryType Type; + + /// + /// The hashes of the inventories. + /// + public required UInt256[] Hashes; + + public int Size => sizeof(InventoryType) + Hashes.GetVarSize(); + + /// + /// Creates a new instance of the class. + /// + /// The type of the inventories. + /// The hashes of the inventories. + /// The created payload. + public static InvPayload Create(InventoryType type, params UInt256[] hashes) + { + return new InvPayload + { + Type = type, + Hashes = hashes + }; + } + + /// + /// Creates a group of the instance. + /// + /// The type of the inventories. + /// The hashes of the inventories. + /// The created payloads. + public static IEnumerable CreateGroup(InventoryType type, IReadOnlyCollection hashes) + { + foreach (var chunk in hashes.Chunk(MaxHashesCount)) + { + yield return new InvPayload + { + Type = type, + Hashes = chunk, + }; + } + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Type = (InventoryType)reader.ReadByte(); + if (!Enum.IsDefined(Type)) + throw new FormatException($"`Type`({Type}) is not defined in InventoryType"); + Hashes = reader.ReadSerializableArray(MaxHashesCount); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write((byte)Type); + writer.Write(Hashes); + } +} diff --git a/src/Neo/Network/P2P/Payloads/InventoryType.cs b/src/Neo/Network/P2P/Payloads/InventoryType.cs new file mode 100644 index 0000000000..2dd64ab7eb --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/InventoryType.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// InventoryType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Network.P2P.Payloads; + +/// +/// Represents the type of an inventory. +/// +public enum InventoryType : byte +{ + /// + /// Indicates that the inventory is a . + /// + TX = MessageCommand.Transaction, + + /// + /// Indicates that the inventory is a . + /// + Block = MessageCommand.Block, + + /// + /// Indicates that the inventory is an . + /// + Extensible = MessageCommand.Extensible +} diff --git a/src/Neo/Network/P2P/Payloads/MerkleBlockPayload.cs b/src/Neo/Network/P2P/Payloads/MerkleBlockPayload.cs new file mode 100644 index 0000000000..1aec2e6de6 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/MerkleBlockPayload.cs @@ -0,0 +1,84 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MerkleBlockPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using System.Collections; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents a block that is filtered by a . +/// +public class MerkleBlockPayload : ISerializable +{ + /// + /// The header of the block. + /// + public required Header Header; + + /// + /// The number of the transactions in the block. + /// + public int TxCount; + + /// + /// The nodes of the transactions hash tree. + /// + public required UInt256[] Hashes; + + /// + /// The data in the that filtered the block. + /// + public ReadOnlyMemory Flags; + + public int Size => Header.Size + sizeof(int) + Hashes.GetVarSize() + Flags.GetVarSize(); + + /// + /// Creates a new instance of the class. + /// + /// The original block. + /// The data in the that filtered the block. + /// The created payload. + public static MerkleBlockPayload Create(Block block, BitArray flags) + { + MerkleTree tree = new(block.Transactions.Select(p => p.Hash).ToArray()); + tree.Trim(flags); + byte[] buffer = new byte[(flags.Length + 7) / 8]; + flags.CopyTo(buffer, 0); + return new MerkleBlockPayload + { + Header = block.Header, + TxCount = block.Transactions.Length, + Hashes = tree.ToHashArray(), + Flags = buffer + }; + } + + public void Deserialize(ref MemoryReader reader) + { + Header = reader.ReadSerializable
(); + TxCount = (int)reader.ReadVarInt(ushort.MaxValue); + Hashes = reader.ReadSerializableArray(TxCount); + Flags = reader.ReadVarMemory((Math.Max(TxCount, 1) + 7) / 8); + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(Header); + writer.WriteVarInt(TxCount); + writer.Write(Hashes); + writer.WriteVarBytes(Flags.Span); + } +} diff --git a/src/Neo/Network/P2P/Payloads/NetworkAddressWithTime.cs b/src/Neo/Network/P2P/Payloads/NetworkAddressWithTime.cs new file mode 100644 index 0000000000..f2829b0b84 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/NetworkAddressWithTime.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NetworkAddressWithTime.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Capabilities; +using System.Net; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Sent with an to respond to messages. +/// +public class NetworkAddressWithTime : ISerializable +{ + /// + /// The time when connected to the node. + /// + public uint Timestamp; + + /// + /// The address of the node. + /// + public required IPAddress Address; + + /// + /// The capabilities of the node. + /// + public required NodeCapability[] Capabilities; + + /// + /// The of the Tcp server. + /// + public IPEndPoint EndPoint => new(Address, Capabilities.Where(p => p.Type == NodeCapabilityType.TcpServer).Select(p => (ServerCapability)p).FirstOrDefault()?.Port ?? 0); + + public int Size => sizeof(uint) + 16 + Capabilities.GetVarSize(); + + /// + /// Creates a new instance of the class. + /// + /// The address of the node. + /// The time when connected to the node. + /// The capabilities of the node. + /// The created payload. + public static NetworkAddressWithTime Create(IPAddress address, uint timestamp, params NodeCapability[] capabilities) + { + return new NetworkAddressWithTime + { + Timestamp = timestamp, + Address = address, + Capabilities = capabilities + }; + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Timestamp = reader.ReadUInt32(); + + // Address + ReadOnlyMemory data = reader.ReadMemory(16); + Address = new IPAddress(data.Span).UnMap(); + + // Capabilities + Capabilities = new NodeCapability[reader.ReadVarInt(VersionPayload.MaxCapabilities)]; + for (int x = 0, max = Capabilities.Length; x < max; x++) + Capabilities[x] = NodeCapability.DeserializeFrom(ref reader); + // Verify that no duplicating capabilities are included. Unknown capabilities are not + // taken into account but still preserved to be able to share through the network. + var capabilities = Capabilities.Where(c => c is not UnknownCapability); + if (capabilities.Select(p => p.Type).Distinct().Count() != capabilities.Count()) + throw new FormatException("Duplicating capabilities are included"); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(Timestamp); + writer.Write(Address.MapToIPv6().GetAddressBytes()); + writer.Write(Capabilities); + } +} diff --git a/src/Neo/Network/P2P/Payloads/NotValidBefore.cs b/src/Neo/Network/P2P/Payloads/NotValidBefore.cs new file mode 100644 index 0000000000..24904308c3 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/NotValidBefore.cs @@ -0,0 +1,55 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NotValidBefore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Persistence; +using Neo.SmartContract.Native; + +namespace Neo.Network.P2P.Payloads; + +public class NotValidBefore : TransactionAttribute +{ + /// + /// Indicates that the transaction is not valid before this height. + /// + public uint Height; + + public override TransactionAttributeType Type => TransactionAttributeType.NotValidBefore; + + public override bool AllowMultiple => false; + + public override int Size => base.Size + + sizeof(uint); // Height. + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + Height = reader.ReadUInt32(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Height); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["height"] = Height; + return json; + } + + public override bool Verify(DataCache snapshot, Transaction tx) + { + var blockHeight = NativeContract.Ledger.CurrentIndex(snapshot); + return blockHeight >= Height; + } +} diff --git a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs new file mode 100644 index 0000000000..7355ba5c05 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs @@ -0,0 +1,77 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NotaryAssisted.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Persistence; + +namespace Neo.Network.P2P.Payloads; + +public class NotaryAssisted : TransactionAttribute +{ + /// + /// Native Notary contract hash stub used until native Notary contract is properly implemented. + /// + private static readonly UInt160 s_notaryHash = SmartContract.Helper.GetContractHash(UInt160.Zero, 0, "Notary"); + + /// + /// Indicates the number of keys participating in the transaction (main or fallback) signing process. + /// + public byte NKeys { get; set; } + + public override TransactionAttributeType Type => TransactionAttributeType.NotaryAssisted; + + public override bool AllowMultiple => false; + + public override int Size => base.Size + sizeof(byte); + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + NKeys = reader.ReadByte(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(NKeys); + } + + public override JObject ToJson() + { + var json = base.ToJson(); + json["nkeys"] = NKeys; + return json; + } + + public override bool Verify(DataCache snapshot, Transaction tx) + { + if (tx.Sender == s_notaryHash) + { + // Payer is in the second position + return tx.Signers.Length == 2; + } + return tx.Signers.Any(p => p.Account.Equals(s_notaryHash)); + } + + /// + /// Calculates the network fee needed to pay for NotaryAssisted attribute. According to the + /// https://github.com/neo-project/neo/issues/1573#issuecomment-704874472, network fee consists of + /// the base Notary service fee per key multiplied by the expected number of transactions that should + /// be collected by the service to complete Notary request increased by one (for Notary node witness + /// itself). + /// + /// The snapshot used to read data. + /// The transaction to calculate. + /// The network fee of the NotaryAssisted attribute. + public override long CalculateNetworkFee(DataCache snapshot, Transaction tx) + { + return (NKeys + 1) * base.CalculateNetworkFee(snapshot, tx); + } +} diff --git a/src/Neo/Network/P2P/Payloads/OracleResponse.cs b/src/Neo/Network/P2P/Payloads/OracleResponse.cs new file mode 100644 index 0000000000..01d38f6fe6 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/OracleResponse.cs @@ -0,0 +1,107 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// OracleResponse.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Extensions.VM; +using Neo.IO; +using Neo.Json; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Indicates that the transaction is an oracle response. +/// +public class OracleResponse : TransactionAttribute +{ + /// + /// Indicates the maximum size of the field. + /// + public const int MaxResultSize = ushort.MaxValue; + + /// + /// Represents the fixed value of the field of the oracle responding transaction. + /// + public static readonly byte[] FixedScript; + + /// + /// The ID of the oracle request. + /// + public ulong Id; + + /// + /// The response code for the oracle request. + /// + public OracleResponseCode Code; + + /// + /// The result for the oracle request. + /// + public ReadOnlyMemory Result; + + public override TransactionAttributeType Type => TransactionAttributeType.OracleResponse; + public override bool AllowMultiple => false; + + public override int Size => base.Size + + sizeof(ulong) + //Id + sizeof(OracleResponseCode) + //ResponseCode + Result.GetVarSize(); //Result + + static OracleResponse() + { + using ScriptBuilder sb = new(); + sb.EmitDynamicCall(NativeContract.Oracle.Hash, "finish"); + FixedScript = sb.ToArray(); + } + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + Id = reader.ReadUInt64(); + Code = (OracleResponseCode)reader.ReadByte(); + if (!Enum.IsDefined(Code)) + throw new FormatException($"Invalid response code: {Code}."); + + Result = reader.ReadVarMemory(MaxResultSize); + if (Code != OracleResponseCode.Success && Result.Length > 0) + throw new FormatException($"Result is not empty({Result.Length}) for non-success response code({Code})"); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Id); + writer.Write((byte)Code); + writer.WriteVarBytes(Result.Span); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["id"] = Id; + json["code"] = Code; + json["result"] = Convert.ToBase64String(Result.Span); + return json; + } + + public override bool Verify(DataCache snapshot, Transaction tx) + { + if (tx.Signers.Any(p => p.Scopes != WitnessScope.None)) return false; + if (!tx.Script.Span.SequenceEqual(FixedScript)) return false; + OracleRequest? request = NativeContract.Oracle.GetRequest(snapshot, Id); + if (request is null) return false; + if (tx.NetworkFee + tx.SystemFee != request.GasForResponse) return false; + UInt160 oracleAccount = Contract.GetBFTAddress(NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, NativeContract.Ledger.CurrentIndex(snapshot) + 1)); + return tx.Signers.Any(p => p.Account.Equals(oracleAccount)); + } +} diff --git a/src/Neo/Network/P2P/Payloads/OracleResponseCode.cs b/src/Neo/Network/P2P/Payloads/OracleResponseCode.cs new file mode 100644 index 0000000000..c105d516a6 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/OracleResponseCode.cs @@ -0,0 +1,68 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// OracleResponseCode.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Network.P2P.Payloads; + +/// +/// Represents the response code for the oracle request. +/// +public enum OracleResponseCode : byte +{ + /// + /// Indicates that the request has been successfully completed. + /// + Success = 0x00, + + /// + /// Indicates that the protocol of the request is not supported. + /// + ProtocolNotSupported = 0x10, + + /// + /// Indicates that the oracle nodes cannot reach a consensus on the result of the request. + /// + ConsensusUnreachable = 0x12, + + /// + /// Indicates that the requested Uri does not exist. + /// + NotFound = 0x14, + + /// + /// Indicates that the request was not completed within the specified time. + /// + Timeout = 0x16, + + /// + /// Indicates that there is no permission to request the resource. + /// + Forbidden = 0x18, + + /// + /// Indicates that the data for the response is too large. + /// + ResponseTooLarge = 0x1a, + + /// + /// Indicates that the request failed due to insufficient balance. + /// + InsufficientFunds = 0x1c, + + /// + /// Indicates that the content-type of the request is not supported. + /// + ContentTypeNotSupported = 0x1f, + + /// + /// Indicates that the request failed due to other errors. + /// + Error = 0xff +} diff --git a/src/Neo/Network/P2P/Payloads/PingPayload.cs b/src/Neo/Network/P2P/Payloads/PingPayload.cs new file mode 100644 index 0000000000..4a8f3d7798 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/PingPayload.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// PingPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using Neo.IO; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Sent to detect whether the connection has been disconnected. +/// +public class PingPayload : ISerializable +{ + /// + /// The latest block index. + /// + public uint LastBlockIndex; + + /// + /// The timestamp when the message was sent. + /// + public uint Timestamp; + + /// + /// A random number. This number must be the same in + /// and messages. + /// + public uint Nonce; + + public int Size => + sizeof(uint) + //LastBlockIndex + sizeof(uint) + //Timestamp + sizeof(uint); //Nonce + + /// + /// Creates a new instance of the class. + /// + /// The latest block index. + /// The created payload. + public static PingPayload Create(uint height) + { + return Create(height, RandomNumberFactory.NextUInt32()); + } + + /// + /// Creates a new instance of the class. + /// + /// The latest block index. + /// The random number. + /// The created payload. + public static PingPayload Create(uint height, uint nonce) + { + return new PingPayload + { + LastBlockIndex = height, + Timestamp = TimeProvider.Current.UtcNow.ToTimestamp(), + Nonce = nonce + }; + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + LastBlockIndex = reader.ReadUInt32(); + Timestamp = reader.ReadUInt32(); + Nonce = reader.ReadUInt32(); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(LastBlockIndex); + writer.Write(Timestamp); + writer.Write(Nonce); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Signer.cs b/src/Neo/Network/P2P/Payloads/Signer.cs new file mode 100644 index 0000000000..e8f55a3020 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Signer.cs @@ -0,0 +1,256 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Signer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads.Conditions; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents a signer of a . +/// +public class Signer : IInteroperable, ISerializable, IEquatable +{ + // This limits maximum number of AllowedContracts or AllowedGroups here + private const int MaxSubitems = 16; + + /// + /// The account of the signer. + /// + public required UInt160 Account; + + /// + /// The scopes of the witness. + /// + public WitnessScope Scopes; + + /// + /// The contracts that allowed by the witness. + /// Only available when the flag is set. + /// + public UInt160[]? AllowedContracts; + + /// + /// The groups that allowed by the witness. + /// Only available when the flag is set. + /// + public ECPoint[]? AllowedGroups; + + /// + /// The rules that the witness must meet. + /// Only available when the flag is set. + /// + public WitnessRule[]? Rules; + + public int Size => + /*Account*/ UInt160.Length + + /*Scopes*/ sizeof(WitnessScope) + + /*AllowedContracts*/ (Scopes.HasFlag(WitnessScope.CustomContracts) ? AllowedContracts!.GetVarSize() : 0) + + /*AllowedGroups*/ (Scopes.HasFlag(WitnessScope.CustomGroups) ? AllowedGroups!.GetVarSize() : 0) + + /*Rules*/ (Scopes.HasFlag(WitnessScope.WitnessRules) ? Rules!.GetVarSize() : 0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Signer? other) + { + if (ReferenceEquals(this, other)) + return true; + + if (other is null) return false; + if (Account != other.Account || Scopes != other.Scopes) + return false; + + if (Scopes.HasFlag(WitnessScope.CustomContracts) && !AllowedContracts.SequenceEqual(other.AllowedContracts)) + return false; + + if (Scopes.HasFlag(WitnessScope.CustomGroups) && !AllowedGroups.SequenceEqual(other.AllowedGroups)) + return false; + + if (Scopes.HasFlag(WitnessScope.WitnessRules) && !Rules.SequenceEqual(other.Rules)) + return false; + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + return obj is Signer signerObj && Equals(signerObj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Account.GetHashCode(), Scopes); + } + + public void Deserialize(ref MemoryReader reader) + { + Account = reader.ReadSerializable(); + Scopes = (WitnessScope)reader.ReadByte(); + if ((Scopes & ~(WitnessScope.CalledByEntry | WitnessScope.CustomContracts | WitnessScope.CustomGroups | WitnessScope.WitnessRules | WitnessScope.Global)) != 0) + throw new FormatException($"`Scopes`({Scopes}) in Signer is not valid"); + if (Scopes.HasFlag(WitnessScope.Global) && Scopes != WitnessScope.Global) + throw new FormatException($"`Scopes`({Scopes}) in Signer is not valid"); + AllowedContracts = Scopes.HasFlag(WitnessScope.CustomContracts) + ? reader.ReadSerializableArray(MaxSubitems) : []; + AllowedGroups = Scopes.HasFlag(WitnessScope.CustomGroups) + ? reader.ReadSerializableArray(MaxSubitems) : []; + Rules = Scopes.HasFlag(WitnessScope.WitnessRules) + ? reader.ReadSerializableArray(MaxSubitems) : []; + } + + /// + /// Converts all rules contained in the object to . + /// + /// The array used to represent the current signer. + public IEnumerable GetAllRules() + { + if (Scopes == WitnessScope.Global) + { + yield return new WitnessRule + { + Action = WitnessRuleAction.Allow, + Condition = new BooleanCondition { Expression = true } + }; + } + else + { + if (Scopes.HasFlag(WitnessScope.CalledByEntry)) + { + yield return new WitnessRule + { + Action = WitnessRuleAction.Allow, + Condition = new CalledByEntryCondition() + }; + } + if (Scopes.HasFlag(WitnessScope.CustomContracts)) + { + foreach (var hash in AllowedContracts!) + yield return new WitnessRule + { + Action = WitnessRuleAction.Allow, + Condition = new ScriptHashCondition { Hash = hash } + }; + } + if (Scopes.HasFlag(WitnessScope.CustomGroups)) + { + foreach (var group in AllowedGroups!) + yield return new WitnessRule + { + Action = WitnessRuleAction.Allow, + Condition = new GroupCondition { Group = group } + }; + } + if (Scopes.HasFlag(WitnessScope.WitnessRules)) + { + foreach (var rule in Rules!) + yield return rule; + } + } + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(Account); + writer.Write((byte)Scopes); + if (Scopes.HasFlag(WitnessScope.CustomContracts)) + writer.Write(AllowedContracts!); + if (Scopes.HasFlag(WitnessScope.CustomGroups)) + writer.Write(AllowedGroups!); + if (Scopes.HasFlag(WitnessScope.WitnessRules)) + writer.Write(Rules!); + } + + /// + /// Converts the signer from a JSON object. + /// + /// The signer represented by a JSON object. + /// The converted signer. + public static Signer FromJson(JObject json) + { + Signer signer = new() + { + Account = UInt160.Parse(json["account"]!.GetString()), + Scopes = Enum.Parse(json["scopes"]!.GetString()) + }; + if (signer.Scopes.HasFlag(WitnessScope.CustomContracts)) + signer.AllowedContracts = ((JArray)json["allowedcontracts"]!).Select(p => UInt160.Parse(p!.GetString())).ToArray(); + if (signer.Scopes.HasFlag(WitnessScope.CustomGroups)) + signer.AllowedGroups = ((JArray)json["allowedgroups"]!).Select(p => ECPoint.Parse(p!.GetString(), ECCurve.Secp256r1)).ToArray(); + if (signer.Scopes.HasFlag(WitnessScope.WitnessRules)) + signer.Rules = ((JArray)json["rules"]!).Select(p => WitnessRule.FromJson((JObject)p!)).ToArray(); + return signer; + } + + /// + /// Converts the signer to a JSON object. + /// + /// The signer represented by a JSON object. + public JObject ToJson() + { + var json = new JObject() + { + ["account"] = Account.ToString(), + ["scopes"] = Scopes + }; + if (Scopes.HasFlag(WitnessScope.CustomContracts)) + json["allowedcontracts"] = AllowedContracts!.Select(p => (JToken)p.ToString()).ToArray(); + if (Scopes.HasFlag(WitnessScope.CustomGroups)) + json["allowedgroups"] = AllowedGroups!.Select(p => (JToken)p.ToString()).ToArray(); + if (Scopes.HasFlag(WitnessScope.WitnessRules)) + json["rules"] = Rules!.Select(p => p.ToJson()).ToArray(); + return json; + } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + throw new NotSupportedException(); + } + + StackItem IInteroperable.ToStackItem(IReferenceCounter? referenceCounter) + { + return new Array(referenceCounter, + [ + Account.ToArray(), + (byte)Scopes, + Scopes.HasFlag(WitnessScope.CustomContracts) ? new Array(referenceCounter, AllowedContracts!.Select(u => new ByteString(u.ToArray()))) : new Array(referenceCounter), + Scopes.HasFlag(WitnessScope.CustomGroups) ? new Array(referenceCounter, AllowedGroups!.Select(u => new ByteString(u.ToArray()))) : new Array(referenceCounter), + Scopes.HasFlag(WitnessScope.WitnessRules) ? new Array(referenceCounter, Rules!.Select(u => u.ToStackItem(referenceCounter))) : new Array(referenceCounter) + ]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Signer left, Signer right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Signer left, Signer right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Transaction.cs b/src/Neo/Network/P2P/Payloads/Transaction.cs new file mode 100644 index 0000000000..208ed94f5a --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Transaction.cs @@ -0,0 +1,470 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Transaction.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography; +using Neo.Cryptography.ECC; +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Ledger; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using Neo.Wallets; +using System.Diagnostics.CodeAnalysis; +using static Neo.SmartContract.Helper; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents a transaction. +/// +public class Transaction : IEquatable, IInventory, IInteroperable +{ + /// + /// The maximum size of a transaction. + /// + public const int MaxTransactionSize = 102400; + + /// + /// The maximum number of attributes that can be contained within a transaction. + /// + public const int MaxTransactionAttributes = 16; + + /// + /// The version of the transaction. + /// + public byte Version { get; set { field = value; _hash = null; } } + + /// + /// The nonce of the transaction. + /// + public uint Nonce { get; set { field = value; _hash = null; } } + + /// + /// The system fee of the transaction. + /// + public long SystemFee { get; set { field = value; _hash = null; } } + + /// + /// The network fee of the transaction. + /// + public long NetworkFee { get; set { field = value; _hash = null; } } + + /// + /// Indicates that the transaction is only valid before this block height. + /// + public uint ValidUntilBlock { get; set { field = value; _hash = null; } } + + /// + /// The signers of the transaction. + /// + public required Signer[] Signers { get; set { field = value; _hash = null; _size = 0; } } + + private Dictionary? _attributesCache; + /// + /// The attributes of the transaction. + /// + public required TransactionAttribute[] Attributes { get; set { field = value; _attributesCache = null; _hash = null; _size = 0; } } + + /// + /// The script of the transaction. + /// + public ReadOnlyMemory Script { get; set { field = value; _hash = null; _size = 0; } } + + public required Witness[] Witnesses { get; set { field = value; _size = 0; } } + + /// + /// The size of a transaction header. + /// + public const int HeaderSize = + sizeof(byte) + //Version + sizeof(uint) + //Nonce + sizeof(long) + //SystemFee + sizeof(long) + //NetworkFee + sizeof(uint); //ValidUntilBlock + + /// + /// The for the transaction divided by its . + /// + public long FeePerByte => NetworkFee / Size; + + private UInt256? _hash = null; + + /// + public UInt256 Hash + { + get + { + if (_hash == null) + { + _hash = this.CalculateHash(); + } + return _hash; + } + } + + InventoryType IInventory.InventoryType => InventoryType.TX; + + /// + /// The sender is the first signer of the transaction, regardless of its . + /// + /// Note: The sender will pay the fees of the transaction. + public UInt160 Sender => Signers[0].Account; + + private int _size; + public int Size + { + get + { + if (_size == 0) + { + _size = HeaderSize + + Signers.GetVarSize() + // Signers + Attributes.GetVarSize() + // Attributes + Script.GetVarSize() + // Script + Witnesses.GetVarSize(); // Witnesses + } + return _size; + } + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + int startPosition = reader.Position; + DeserializeUnsigned(ref reader); + Witnesses = reader.ReadSerializableArray(Signers.Length); + if (Witnesses.Length != Signers.Length) + throw new FormatException($"Witnesses.Length({Witnesses.Length}) != Signers.Length({Signers.Length})"); + _size = reader.Position - startPosition; + } + + private static TransactionAttribute[] DeserializeAttributes(ref MemoryReader reader, int maxCount) + { + int count = (int)reader.ReadVarInt((ulong)maxCount); + TransactionAttribute[] attributes = new TransactionAttribute[count]; + HashSet hashset = new(); + for (int i = 0; i < count; i++) + { + TransactionAttribute attribute = TransactionAttribute.DeserializeFrom(ref reader); + if (!attribute.AllowMultiple && !hashset.Add(attribute.Type)) + throw new FormatException($"`{attribute.Type}` in Transaction is duplicate"); + attributes[i] = attribute; + } + return attributes; + } + + private static Signer[] DeserializeSigners(ref MemoryReader reader, int maxCount) + { + int count = (int)reader.ReadVarInt((ulong)maxCount); + if (count == 0) throw new FormatException("Signers in Transaction is empty"); + Signer[] signers = new Signer[count]; + HashSet hashset = new(); + for (int i = 0; i < count; i++) + { + Signer signer = reader.ReadSerializable(); + if (!hashset.Add(signer.Account)) throw new FormatException($"`{signer.Account}` in Transaction is duplicate"); + signers[i] = signer; + } + return signers; + } + + public void DeserializeUnsigned(ref MemoryReader reader) + { + Version = reader.ReadByte(); + if (Version > 0) throw new FormatException($"Invalid version: {Version}."); + + Nonce = reader.ReadUInt32(); + SystemFee = reader.ReadInt64(); + if (SystemFee < 0) throw new FormatException($"Invalid system fee: {SystemFee}."); + + NetworkFee = reader.ReadInt64(); + if (NetworkFee < 0) throw new FormatException($"Invalid network fee: {NetworkFee}."); + + if (SystemFee + NetworkFee < SystemFee) + throw new FormatException($"Invalid fee: {SystemFee} + {NetworkFee} < {SystemFee}."); + + ValidUntilBlock = reader.ReadUInt32(); + Signers = DeserializeSigners(ref reader, MaxTransactionAttributes); + Attributes = DeserializeAttributes(ref reader, MaxTransactionAttributes - Signers.Length); + Script = reader.ReadVarMemory(ushort.MaxValue); + if (Script.Length == 0) throw new FormatException("Script in Transaction is empty"); + } + + public bool Equals(Transaction? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Hash.Equals(other.Hash); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Transaction); + } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + throw new NotSupportedException(); + } + + /// + /// Gets the attribute of the specified type. + /// + /// The type of the attribute. + /// The first attribute of this type. Or if there is no attribute of this type. + public T? GetAttribute() where T : TransactionAttribute + { + return GetAttributes().FirstOrDefault(); + } + + /// + /// Gets all attributes of the specified type. + /// + /// The type of the attributes. + /// All the attributes of this type. + public IEnumerable GetAttributes() where T : TransactionAttribute + { + _attributesCache ??= Attributes.GroupBy(p => p.GetType()).ToDictionary(p => p.Key, p => p.ToArray()); + if (_attributesCache.TryGetValue(typeof(T), out var result)) + return result.OfType(); + return Enumerable.Empty(); + } + + public override int GetHashCode() + { + return Hash.GetHashCode(); + } + + public UInt160[] GetScriptHashesForVerifying(IReadOnlyStore? snapshot = null) + { + return Signers.Select(p => p.Account).ToArray(); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + ((IVerifiable)this).SerializeUnsigned(writer); + writer.Write(Witnesses); + } + + void IVerifiable.SerializeUnsigned(BinaryWriter writer) + { + writer.Write(Version); + writer.Write(Nonce); + writer.Write(SystemFee); + writer.Write(NetworkFee); + writer.Write(ValidUntilBlock); + writer.Write(Signers); + writer.Write(Attributes); + writer.WriteVarBytes(Script.Span); + } + + /// + /// Converts the transaction to a JSON object. + /// + /// The used during the conversion. + /// The transaction represented by a JSON object. + public JObject ToJson(ProtocolSettings settings) + { + JObject json = new(); + json["hash"] = Hash.ToString(); + json["size"] = Size; + json["version"] = Version; + json["nonce"] = Nonce; + json["sender"] = Sender.ToAddress(settings.AddressVersion); + json["sysfee"] = SystemFee.ToString(); + json["netfee"] = NetworkFee.ToString(); + json["validuntilblock"] = ValidUntilBlock; + json["signers"] = Signers.Select(p => p.ToJson()).ToArray(); + json["attributes"] = Attributes.Select(p => p.ToJson()).ToArray(); + json["script"] = Convert.ToBase64String(Script.Span); + json["witnesses"] = Witnesses.Select(p => p.ToJson()).ToArray(); + return json; + } + + /// + /// Verifies the transaction. + /// + /// The used to verify the transaction. + /// The snapshot used to verify the transaction. + /// The used to verify the transaction. + /// The list of conflicting those fee should be excluded from sender's overall fee during -based verification in case of sender's match. + /// The result of the verification. + public VerifyResult Verify(ProtocolSettings settings, DataCache snapshot, TransactionVerificationContext context, IEnumerable conflictsList) + { + VerifyResult result = VerifyStateIndependent(settings); + if (result != VerifyResult.Succeed) return result; + return VerifyStateDependent(settings, snapshot, context, conflictsList); + } + + /// + /// Verifies the state-dependent part of the transaction. + /// + /// The used to verify the transaction. + /// The snapshot used to verify the transaction. + /// The used to verify the transaction. + /// The list of conflicting those fee should be excluded from sender's overall fee during -based verification in case of sender's match. + /// The result of the verification. + public virtual VerifyResult VerifyStateDependent(ProtocolSettings settings, DataCache snapshot, TransactionVerificationContext context, IEnumerable conflictsList) + { + uint height = NativeContract.Ledger.CurrentIndex(snapshot); + if (ValidUntilBlock <= height || ValidUntilBlock > height + settings.MaxValidUntilBlockIncrement) + return VerifyResult.Expired; + UInt160[] hashes = GetScriptHashesForVerifying(snapshot); + foreach (UInt160 hash in hashes) + if (NativeContract.Policy.IsBlocked(snapshot, hash)) + return VerifyResult.PolicyFail; + if (!(context?.CheckTransaction(this, conflictsList, snapshot) ?? true)) return VerifyResult.InsufficientFunds; + long attributesFee = 0; + foreach (TransactionAttribute attribute in Attributes) + { + if (!attribute.Verify(snapshot, this)) + return VerifyResult.InvalidAttribute; + attributesFee += attribute.CalculateNetworkFee(snapshot, this); + } + long netFeeDatoshi = NetworkFee - (Size * NativeContract.Policy.GetFeePerByte(snapshot)) - attributesFee; + if (netFeeDatoshi < 0) return VerifyResult.InsufficientFunds; + + if (netFeeDatoshi > MaxVerificationGas) netFeeDatoshi = MaxVerificationGas; + uint execFeeFactor = NativeContract.Policy.GetExecFeeFactor(snapshot); + for (int i = 0; i < hashes.Length; i++) + { + if (IsSignatureContract(Witnesses[i].VerificationScript.Span) && IsSingleSignatureInvocationScript(Witnesses[i].InvocationScript, out var _)) + netFeeDatoshi -= execFeeFactor * SignatureContractCost(); + else if (IsMultiSigContract(Witnesses[i].VerificationScript.Span, out int m, out int n) && IsMultiSignatureInvocationScript(m, Witnesses[i].InvocationScript, out var _)) + { + netFeeDatoshi -= execFeeFactor * MultiSignatureContractCost(m, n); + } + else + { + if (!this.VerifyWitness(settings, snapshot, hashes[i], Witnesses[i], netFeeDatoshi, out long fee)) + return VerifyResult.Invalid; + netFeeDatoshi -= fee; + } + if (netFeeDatoshi < 0) return VerifyResult.InsufficientFunds; + } + return VerifyResult.Succeed; + } + + /// + /// Verifies the state-independent part of the transaction. + /// + /// The used to verify the transaction. + /// The result of the verification. + public virtual VerifyResult VerifyStateIndependent(ProtocolSettings settings) + { + if (Size > MaxTransactionSize) return VerifyResult.OverSize; + try + { + _ = new Script(Script, true); + } + catch (BadScriptException) + { + return VerifyResult.InvalidScript; + } + UInt160[] hashes = GetScriptHashesForVerifying(null!); + for (int i = 0; i < hashes.Length; i++) + { + var witness = Witnesses[i]; + if (IsSignatureContract(witness.VerificationScript.Span) && IsSingleSignatureInvocationScript(witness.InvocationScript, out var signature)) + { + if (hashes[i] != witness.ScriptHash) return VerifyResult.Invalid; + var pubkey = witness.VerificationScript.Span[2..35]; + try + { + if (!Crypto.VerifySignature(this.GetSignData(settings.Network), signature.Span, pubkey, ECCurve.Secp256r1)) + return VerifyResult.InvalidSignature; + } + catch + { + return VerifyResult.Invalid; + } + } + else if (IsMultiSigContract(witness.VerificationScript.Span, out var m, out ECPoint[]? points) && IsMultiSignatureInvocationScript(m, witness.InvocationScript, out var signatures)) + { + if (hashes[i] != witness.ScriptHash) return VerifyResult.Invalid; + var n = points.Length; + var message = this.GetSignData(settings.Network); + try + { + for (int x = 0, y = 0; x < m && y < n;) + { + if (Crypto.VerifySignature(message, signatures[x].Span, points[y])) + x++; + y++; + if (m - x > n - y) + return VerifyResult.InvalidSignature; + } + } + catch + { + return VerifyResult.Invalid; + } + } + } + return VerifyResult.Succeed; + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + if (Signers.Length == 0) throw new ArgumentException("Sender is not specified in the transaction."); + return new Array(referenceCounter, new StackItem[] + { + // Computed properties + Hash.ToArray(), + + // Transaction properties + (int)Version, + Nonce, + Sender.ToArray(), + SystemFee, + NetworkFee, + ValidUntilBlock, + Script, + }); + } + + private static bool IsMultiSignatureInvocationScript(int m, ReadOnlyMemory invocationScript, + [NotNullWhen(true)] out ReadOnlyMemory[]? sigs) + { + sigs = null; + ReadOnlySpan span = invocationScript.Span; + int i = 0; + var signatures = new List>(); + while (i < invocationScript.Length) + { + if (span[i++] != (byte)OpCode.PUSHDATA1) return false; + if (i + 65 > invocationScript.Length) return false; + if (span[i++] != 64) return false; + signatures.Add(invocationScript[i..(i + 64)]); + i += 64; + } + if (signatures.Count != m) return false; + sigs = signatures.ToArray(); + return true; + } + + private static bool IsSingleSignatureInvocationScript(ReadOnlyMemory invocationScript, + [NotNullWhen(true)] out ReadOnlyMemory sig) + { + sig = null; + if (invocationScript.Length != 66) return false; + ReadOnlySpan span = invocationScript.Span; + if ((span[0] != (byte)OpCode.PUSHDATA1) || (span[1] != 64)) return false; + sig = invocationScript[2..66]; + return true; + } +} diff --git a/src/Neo/Network/P2P/Payloads/TransactionAttribute.cs b/src/Neo/Network/P2P/Payloads/TransactionAttribute.cs new file mode 100644 index 0000000000..0cff506e75 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/TransactionAttribute.cs @@ -0,0 +1,98 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO.Caching; +using Neo.Json; +using Neo.Persistence; +using Neo.SmartContract.Native; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents an attribute of a transaction. +/// +public abstract class TransactionAttribute : ISerializable +{ + /// + /// The type of the attribute. + /// + public abstract TransactionAttributeType Type { get; } + + /// + /// Indicates whether multiple instances of this attribute are allowed. + /// + public abstract bool AllowMultiple { get; } + + public virtual int Size => sizeof(TransactionAttributeType); + + public void Deserialize(ref MemoryReader reader) + { + var type = reader.ReadByte(); + if (type != (byte)Type) + throw new FormatException($"Expected {Type}, got {type}."); + DeserializeWithoutType(ref reader); + } + + /// + /// Deserializes an object from a . + /// + /// The for reading data. + /// The deserialized attribute. + public static TransactionAttribute DeserializeFrom(ref MemoryReader reader) + { + TransactionAttributeType type = (TransactionAttributeType)reader.ReadByte(); + if (ReflectionCache.CreateInstance(type) is not TransactionAttribute attribute) + throw new FormatException($"Invalid attribute type: {type}."); + attribute.DeserializeWithoutType(ref reader); + return attribute; + } + + /// + /// Deserializes the object from a . + /// + /// The for reading data. + protected abstract void DeserializeWithoutType(ref MemoryReader reader); + + /// + /// Converts the attribute to a JSON object. + /// + /// The attribute represented by a JSON object. + public virtual JObject ToJson() + { + return new JObject + { + ["type"] = Type + }; + } + + public void Serialize(BinaryWriter writer) + { + writer.Write((byte)Type); + SerializeWithoutType(writer); + } + + /// + /// Serializes the object to a . + /// + /// The for writing data. + protected abstract void SerializeWithoutType(BinaryWriter writer); + + /// + /// Verifies the attribute with the transaction. + /// + /// The snapshot used to verify the attribute. + /// The that contains the attribute. + /// if the verification passes; otherwise, . + public virtual bool Verify(DataCache snapshot, Transaction tx) => true; + + public virtual long CalculateNetworkFee(DataCache snapshot, Transaction tx) => NativeContract.Policy.GetAttributeFee(snapshot, (byte)Type); +} diff --git a/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs new file mode 100644 index 0000000000..12510e978d --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs @@ -0,0 +1,50 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionAttributeType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents the type of a . +/// +public enum TransactionAttributeType : byte +{ + /// + /// Indicates that the transaction is of high priority. + /// + [ReflectionCache(typeof(HighPriorityAttribute))] + HighPriority = 0x01, + + /// + /// Indicates that the transaction is an oracle response. + /// + [ReflectionCache(typeof(OracleResponse))] + OracleResponse = 0x11, + + /// + /// Indicates that the transaction is not valid before . + /// + [ReflectionCache(typeof(NotValidBefore))] + NotValidBefore = 0x20, + + /// + /// Indicates that the transaction conflicts with . + /// + [ReflectionCache(typeof(Conflicts))] + Conflicts = 0x21, + + /// + /// Indicates that the transaction uses notary request service with number of keys. + /// + [ReflectionCache(typeof(NotaryAssisted))] + NotaryAssisted = 0x22 +} diff --git a/src/Neo/Network/P2P/Payloads/VersionPayload.cs b/src/Neo/Network/P2P/Payloads/VersionPayload.cs new file mode 100644 index 0000000000..5c768eb44d --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/VersionPayload.cs @@ -0,0 +1,127 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// VersionPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Capabilities; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Sent when a connection is established. +/// +public class VersionPayload : ISerializable +{ + /// + /// Indicates the maximum number of capabilities contained in a . + /// + public const int MaxCapabilities = 32; + + /// + /// The magic number of the network. + /// + public uint Network; + + /// + /// The protocol version of the node. + /// + public uint Version; + + /// + /// The time when connected to the node (UTC). + /// + public uint Timestamp; + + /// + /// A random number used to identify the node. + /// + public uint Nonce; + + /// + /// A used to identify the client software of the node. + /// + public required string UserAgent; + + /// + /// True if allow compression + /// + public bool AllowCompression; + + /// + /// The capabilities of the node. + /// + public required NodeCapability[] Capabilities; + + public int Size => + sizeof(uint) + // Network + sizeof(uint) + // Version + sizeof(uint) + // Timestamp + sizeof(uint) + // Nonce + UserAgent.GetVarSize() + // UserAgent + Capabilities.GetVarSize(); // Capabilities + + /// + /// Creates a new instance of the class. + /// + /// The magic number of the network. + /// The random number used to identify the node. + /// The used to identify the client software of the node. + /// The capabilities of the node. + /// + public static VersionPayload Create(uint network, uint nonce, string userAgent, params NodeCapability[] capabilities) + { + var ret = new VersionPayload + { + Network = network, + Version = LocalNode.ProtocolVersion, + Timestamp = DateTime.UtcNow.ToTimestamp(), + Nonce = nonce, + UserAgent = userAgent, + Capabilities = capabilities, + // Computed + AllowCompression = !capabilities.Any(u => u is DisableCompressionCapability) + }; + + return ret; + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Network = reader.ReadUInt32(); + Version = reader.ReadUInt32(); + Timestamp = reader.ReadUInt32(); + Nonce = reader.ReadUInt32(); + UserAgent = reader.ReadVarString(1024); + + // Capabilities + Capabilities = new NodeCapability[reader.ReadVarInt(MaxCapabilities)]; + for (int x = 0, max = Capabilities.Length; x < max; x++) + Capabilities[x] = NodeCapability.DeserializeFrom(ref reader); + var capabilities = Capabilities.Where(c => c is not UnknownCapability); + if (capabilities.Select(p => p.Type).Distinct().Count() != capabilities.Count()) + throw new FormatException("Duplicating capabilities are included"); + + AllowCompression = !capabilities.Any(u => u is DisableCompressionCapability); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(Network); + writer.Write(Version); + writer.Write(Timestamp); + writer.Write(Nonce); + writer.WriteVarString(UserAgent); + writer.Write(Capabilities); + } +} diff --git a/src/Neo/Network/P2P/Payloads/Witness.cs b/src/Neo/Network/P2P/Payloads/Witness.cs new file mode 100644 index 0000000000..957a7d6444 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/Witness.cs @@ -0,0 +1,95 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Witness.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.SmartContract; + +namespace Neo.Network.P2P.Payloads; + +/// +/// Represents a witness of an object. +/// +public class Witness : ISerializable +{ + // This is designed to allow a MultiSig 21/11 (committee) + // Invocation = 11 * (64 + 2) = 726 + private const int MaxInvocationScript = 1024; + + // Verification = m + (PUSH_PubKey * 21) + length + null + syscall = 1 + ((2 + 33) * 21) + 2 + 1 + 5 = 744 + private const int MaxVerificationScript = 1024; + + /// + /// The invocation script of the witness. Used to pass arguments for . + /// + public ReadOnlyMemory InvocationScript; + + /// + /// The verification script of the witness. It can be empty if the contract is deployed. + /// + public ReadOnlyMemory VerificationScript; + + /// + /// The hash of the . + /// + public UInt160 ScriptHash + { + get => field ??= VerificationScript.Span.ToScriptHash(); + private set; + } + + public int Size => InvocationScript.GetVarSize() + VerificationScript.GetVarSize(); + + /// + /// Creates a new with empty invocation and verification scripts. + /// + public static Witness Empty => new() + { + InvocationScript = ReadOnlyMemory.Empty, + VerificationScript = ReadOnlyMemory.Empty, + }; + + void ISerializable.Deserialize(ref MemoryReader reader) + { + InvocationScript = reader.ReadVarMemory(MaxInvocationScript); + VerificationScript = reader.ReadVarMemory(MaxVerificationScript); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.WriteVarBytes(InvocationScript.Span); + writer.WriteVarBytes(VerificationScript.Span); + } + + /// + /// Converts the witness to a JSON object. + /// + /// The witness represented by a JSON object. + public JObject ToJson() + { + JObject json = new(); + json["invocation"] = Convert.ToBase64String(InvocationScript.Span); + json["verification"] = Convert.ToBase64String(VerificationScript.Span); + return json; + } + + public Witness Clone() + { + return new Witness() + { + ScriptHash = ScriptHash, + InvocationScript = InvocationScript.ToArray(), + VerificationScript = VerificationScript.ToArray() + }; + } +} diff --git a/src/Neo/Network/P2P/Payloads/WitnessRule.cs b/src/Neo/Network/P2P/Payloads/WitnessRule.cs new file mode 100644 index 0000000000..1ed7f57506 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/WitnessRule.cs @@ -0,0 +1,138 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WitnessRule.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads.Conditions; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.Network.P2P.Payloads; + +/// +/// The rule used to describe the scope of the witness. +/// +public class WitnessRule : IInteroperable, ISerializable, IEquatable +{ + /// + /// Indicates the action to be taken if the current context meets with the rule. + /// + public WitnessRuleAction Action; + + /// + /// The condition of the rule. + /// + public required WitnessCondition Condition; + + int ISerializable.Size => sizeof(WitnessRuleAction) + Condition.Size; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(WitnessRule? other) + { + if (ReferenceEquals(this, other)) return true; + if (other is null) return false; + return Action == other.Action && + Condition == other.Condition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj == null) return false; + return obj is WitnessRule wr && Equals(wr); + } + + public override int GetHashCode() + { + return HashCode.Combine(Action, Condition.GetHashCode()); + } + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Action = (WitnessRuleAction)reader.ReadByte(); + if (Action != WitnessRuleAction.Allow && Action != WitnessRuleAction.Deny) + throw new FormatException($"Invalid action: {Action}."); + Condition = WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write((byte)Action); + writer.Write(Condition); + } + + /// + /// Converts the from a JSON object. + /// + /// The represented by a JSON object. + /// The converted . + public static WitnessRule FromJson(JObject json) + { + WitnessRuleAction action = Enum.Parse(json["action"]!.GetString()); + if (action != WitnessRuleAction.Allow && action != WitnessRuleAction.Deny) + throw new FormatException($"Invalid action: {action}."); + + return new() + { + Action = action, + Condition = WitnessCondition.FromJson((JObject)json["condition"]!, WitnessCondition.MaxNestingDepth) + }; + } + + /// + /// Converts the rule to a JSON object. + /// + /// The rule represented by a JSON object. + public JObject ToJson() + { + return new JObject + { + ["action"] = Action, + ["condition"] = Condition.ToJson() + }; + } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + throw new NotSupportedException(); + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Array(referenceCounter, new StackItem[] + { + (byte)Action, + Condition.ToStackItem(referenceCounter) + }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(WitnessRule left, WitnessRule right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(WitnessRule left, WitnessRule right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/Network/P2P/Payloads/WitnessRuleAction.cs b/src/Neo/Network/P2P/Payloads/WitnessRuleAction.cs new file mode 100644 index 0000000000..aa831293ab --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/WitnessRuleAction.cs @@ -0,0 +1,28 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WitnessRuleAction.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Network.P2P.Payloads; + +/// +/// Indicates the action to be taken if the current context meets with the rule. +/// +public enum WitnessRuleAction : byte +{ + /// + /// Deny the witness according to the rule. + /// + Deny = 0, + + /// + /// Allow the witness according to the rule. + /// + Allow = 1 +} diff --git a/src/Neo/Network/P2P/Payloads/WitnessScope.cs b/src/Neo/Network/P2P/Payloads/WitnessScope.cs new file mode 100644 index 0000000000..92af86043b --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/WitnessScope.cs @@ -0,0 +1,52 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WitnessScope.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Network.P2P.Payloads; + +/// +/// Represents the scope of a . +/// +[Flags] +public enum WitnessScope : byte +{ + /// + /// Indicates that no contract was witnessed. Only sign the transaction. + /// + None = 0, + + /// + /// Indicates that the calling contract must be the entry contract. + /// The witness/permission/signature given on first invocation will automatically expire if entering deeper internal invokes. + /// This can be the default safe choice for native NEO/GAS (previously used on Neo 2 as "attach" mode). + /// + CalledByEntry = 0x01, + + /// + /// Custom hash for contract-specific. + /// + CustomContracts = 0x10, + + /// + /// Custom pubkey for group members. + /// + CustomGroups = 0x20, + + /// + /// Indicates that the current context must satisfy the specified rules. + /// + WitnessRules = 0x40, + + /// + /// This allows the witness in all contexts (default Neo2 behavior). + /// + /// Note: It cannot be combined with other flags. + Global = 0x80 +} diff --git a/src/Neo/Network/P2P/Peer.cs b/src/Neo/Network/P2P/Peer.cs new file mode 100644 index 0000000000..aaef9f0028 --- /dev/null +++ b/src/Neo/Network/P2P/Peer.cs @@ -0,0 +1,368 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Peer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO; +using Neo.Collections; +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Net; +using System.Net.NetworkInformation; + +namespace Neo.Network.P2P; + +/// +/// Actor used to manage the connections of the local node. +/// +public abstract class Peer : UntypedActor, IWithUnboundedStash +{ + public IStash Stash { get; set; } = null!; + + /// + /// Sent to to add more unconnected peers. + /// + /// The unconnected peers to be added. + public record Peers(IEnumerable EndPoints); + + /// + /// Sent to to connect to a remote node. + /// + /// The address of the remote node. + /// Indicates whether the remote node is trusted. A trusted node will always be connected. + public record Connect(IPEndPoint EndPoint, bool IsTrusted); + + private class Timer { } + + private static readonly IActorRef s_tcpManager = Context.System.Tcp(); + + private IActorRef? _tcpListener; + + private ICancelable _timer = null!; + + private static readonly HashSet s_localAddresses = new(); + + private readonly Dictionary ConnectedAddresses = new(); + + /// + /// A dictionary that stores the connected nodes. + /// + protected readonly ConcurrentDictionary ConnectedPeers = new(); + + /// + /// A set that stores the peers received from other nodes. + /// If the number of desired connections is not enough, first try to connect with the peers from this set. + /// + protected ImmutableHashSet UnconnectedPeers = ImmutableHashSet.Empty; + + /// + /// When a TCP connection request is sent to a peer, the peer will be added to the set. + /// If a Tcp.Connected or a Tcp.CommandFailed (with TCP.Command of type Tcp.Connect) is received, the related peer will be removed. + /// + protected ImmutableHashSet ConnectingPeers = ImmutableHashSet.Empty; + + /// + /// A hash set to store the trusted nodes. A trusted node will always be connected. + /// + protected HashSet TrustedIpAddresses { get; } = new(); + + /// + /// The port listened by the local Tcp server. + /// + public int ListenerTcpPort { get; private set; } + + /// + /// Channel configuration. + /// + public ChannelsConfig Config { get; private set; } = null!; + + /// + /// Indicates the maximum number of unconnected peers stored in . + /// + protected int UnconnectedMax { get; } = 1000; + + /// + /// Indicates the maximum number of pending connections. + /// + protected virtual int ConnectingMax + { + get + { + var allowedConnecting = Config.MinDesiredConnections * 4; + allowedConnecting = Config.MaxConnections != -1 && allowedConnecting > Config.MaxConnections + ? Config.MaxConnections : allowedConnecting; + return allowedConnecting - ConnectedPeers.Count; + } + } + + static Peer() + { + var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces() + .SelectMany(p => p.GetIPProperties().UnicastAddresses) + .Select(p => p.Address.UnMap()); + s_localAddresses.UnionWith(networkInterfaces); + } + + /// + /// Tries to add a set of peers to the immutable ImmutableHashSet of UnconnectedPeers. + /// + /// Peers that the method will try to add (union) to (with) UnconnectedPeers. + protected internal void AddPeers(IEnumerable peers) + { + if (UnconnectedPeers.Count < UnconnectedMax) + { + // Do not select peers to be added that are already on the ConnectedPeers + // If the address is the same, the ListenerTcpPort should be different + peers = peers.Where(p => (p.Port != ListenerTcpPort || !s_localAddresses.Contains(p.Address)) && !ConnectedPeers.Values.Contains(p)); + ImmutableInterlocked.Update(ref UnconnectedPeers, p => p.Union(peers)); + } + } + + /// + /// Tries to connect the a remote peer. + /// + /// The address of the remote peer. + /// Indicates whether the remote node is trusted. A trusted node will always be connected. + protected void ConnectToPeer(IPEndPoint endPoint, bool isTrusted = false) + { + endPoint = endPoint.UnMap(); + // If the address is the same, the ListenerTcpPort should be different, otherwise, return + if (endPoint.Port == ListenerTcpPort && s_localAddresses.Contains(endPoint.Address)) return; + + if (isTrusted) TrustedIpAddresses.Add(endPoint.Address); + // If connections with the peer greater than or equal to MaxConnectionsPerAddress, return. + if (ConnectedAddresses.TryGetValue(endPoint.Address, out int count) && count >= Config.MaxConnectionsPerAddress) + return; + if (ConnectedPeers.Values.Contains(endPoint)) return; + ImmutableInterlocked.Update(ref ConnectingPeers, p => + { + if ((p.Count >= ConnectingMax && !isTrusted) || p.Contains(endPoint)) return p; + s_tcpManager.Tell(new Tcp.Connect(endPoint)); + return p.Add(endPoint); + }); + } + + private static bool IsIntranetAddress(IPAddress address) + { + byte[] data = address.MapToIPv4().GetAddressBytes(); + uint value = BinaryPrimitives.ReadUInt32BigEndian(data); + return (value & 0xff000000) == 0x0a000000 || + (value & 0xff000000) == 0x7f000000 || + (value & 0xfff00000) == 0xac100000 || + (value & 0xffff0000) == 0xc0a80000 || + (value & 0xffff0000) == 0xa9fe0000; + } + + /// + /// Called for asking for more peers. + /// + /// Number of peers that are being requested. + protected abstract void NeedMorePeers(int count); + + protected override void OnReceive(object message) + { + switch (message) + { + case ChannelsConfig config: + OnStart(config); + Stash.UnstashAll(); + return; + + case Timer _: + if (Config is null) + { + Stash.Stash(); + return; + } + OnTimer(); + break; + + case Peers peers: + if (Config is null) + { + Stash.Stash(); + return; + } + AddPeers(peers.EndPoints); + break; + + case Connect connect: + if (Config is null) + { + Stash.Stash(); + return; + } + ConnectToPeer(connect.EndPoint, connect.IsTrusted); + break; + + case Tcp.Connected connected: + if (Config is null) + { + Stash.Stash(); + return; + } + if (connected.RemoteAddress is null) + { + Sender.Tell(Tcp.Abort.Instance); + break; + } + OnTcpConnected(((IPEndPoint)connected.RemoteAddress).UnMap(), ((IPEndPoint)connected.LocalAddress).UnMap()); + break; + + case Tcp.Bound _: + if (Config is null) + { + Stash.Stash(); + return; + } + _tcpListener = Sender; + break; + + case Tcp.CommandFailed commandFailed: + if (Config is null) + { + Stash.Stash(); + return; + } + OnTcpCommandFailed(commandFailed.Cmd); + break; + + case Terminated terminated: + if (Config is null) + { + Stash.Stash(); + return; + } + OnTerminated(terminated.ActorRef); + break; + } + } + + private void OnStart(ChannelsConfig config) + { + ListenerTcpPort = config.Tcp?.Port ?? 0; + Config = config; + + // schedule time to trigger `OnTimer` event every TimerMillisecondsInterval ms + _timer = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(0, 5000, Context.Self, new Timer(), ActorRefs.NoSender); + if (ListenerTcpPort > 0) + { + s_tcpManager.Tell(new Tcp.Bind(Self, config.Tcp, options: [new Inet.SO.ReuseAddress(true)])); + } + } + + /// + /// Will be triggered when a Tcp.Connected message is received. + /// If the conditions are met, the remote endpoint will be added to ConnectedPeers. + /// Increase the connection number with the remote endpoint by one. + /// + /// The remote endpoint of TCP connection. + /// The local endpoint of TCP connection. + private void OnTcpConnected(IPEndPoint remote, IPEndPoint local) + { + if (Config is null) // OnStart is not called yet + { + Sender.Tell(Tcp.Abort.Instance); + return; + } + + ImmutableInterlocked.Update(ref ConnectingPeers, p => p.Remove(remote)); + if (Config.MaxConnections != -1 && ConnectedPeers.Count >= Config.MaxConnections && !TrustedIpAddresses.Contains(remote.Address)) + { + Sender.Tell(Tcp.Abort.Instance); + return; + } + + ConnectedAddresses.TryGetValue(remote.Address, out int count); + if (count >= Config.MaxConnectionsPerAddress) + { + Sender.Tell(Tcp.Abort.Instance); + } + else + { + ConnectedAddresses[remote.Address] = count + 1; + var connection = Context.ActorOf(ProtocolProps(Sender, remote, local), $"connection_{Guid.NewGuid()}"); + Context.Watch(connection); + Sender.Tell(new Tcp.Register(connection)); + ConnectedPeers.TryAdd(connection, remote); + OnTcpConnected(connection); + } + } + + /// + /// Called when a Tcp connection is established. + /// + /// The connection actor. + protected virtual void OnTcpConnected(IActorRef connection) + { + } + + /// + /// Will be triggered when a Tcp.CommandFailed message is received. + /// If it's a Tcp.Connect command, remove the related endpoint from ConnectingPeers. + /// + /// Tcp.Command message/event. + private void OnTcpCommandFailed(Tcp.Command cmd) + { + switch (cmd) + { + case Tcp.Connect connect: + ImmutableInterlocked.Update(ref ConnectingPeers, p => p.Remove(((IPEndPoint)connect.RemoteAddress).UnMap())); + break; + } + } + + private void OnTerminated(IActorRef actorRef) + { + if (ConnectedPeers.TryRemove(actorRef, out IPEndPoint? endPoint)) + { + ConnectedAddresses.TryGetValue(endPoint.Address, out int count); + if (count > 0) count--; + if (count == 0) + ConnectedAddresses.Remove(endPoint.Address); + else + ConnectedAddresses[endPoint.Address] = count; + } + } + + private void OnTimer() + { + // Check if the number of desired connections is already enough + if (ConnectedPeers.Count >= Config.MinDesiredConnections) return; + + // If there aren't available UnconnectedPeers, it triggers an abstract implementation of NeedMorePeers + if (UnconnectedPeers.Count == 0) + NeedMorePeers(Config.MinDesiredConnections - ConnectedPeers.Count); + + var endpoints = UnconnectedPeers.Sample(Config.MinDesiredConnections - ConnectedPeers.Count); + ImmutableInterlocked.Update(ref UnconnectedPeers, p => p.Except(endpoints)); + foreach (var endpoint in endpoints) + { + ConnectToPeer(endpoint); + } + } + + protected override void PostStop() + { + _timer.CancelIfNotNull(); + _tcpListener?.Tell(Tcp.Unbind.Instance); + base.PostStop(); + } + + /// + /// Gets a object used for creating the protocol actor. + /// + /// The underlying connection object. + /// The address of the remote node. + /// The address of the local node. + /// The object used for creating the protocol actor. + protected abstract Props ProtocolProps(object connection, IPEndPoint remote, IPEndPoint local); +} diff --git a/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs b/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs new file mode 100644 index 0000000000..5460400c95 --- /dev/null +++ b/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs @@ -0,0 +1,437 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// RemoteNode.ProtocolHandler.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using Neo.IO.Caching; +using Neo.Ledger; +using Neo.Network.P2P.Capabilities; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract.Native; +using System.Collections; +using System.Net; + +namespace Neo.Network.P2P; + +public delegate bool MessageReceivedHandler(NeoSystem system, Message message); + +partial class RemoteNode +{ + private class Timer { } + private class PendingKnownHashesCollection : KeyedCollectionSlim> + { + protected override UInt256 GetKeyForItem(Tuple item) => item.Item1; + } + + public static event MessageReceivedHandler MessageReceived + { + add => s_handlers.Add(value); + remove => s_handlers.Remove(value); + } + + private static readonly List s_handlers = new(); + private readonly PendingKnownHashesCollection _pendingKnownHashes = new(); + private readonly HashSetCache _knownHashes; + private readonly HashSetCache _sentHashes; + private bool _verack = false; + private BloomFilter? _bloomFilter; + + private static readonly TimeSpan TimerInterval = TimeSpan.FromSeconds(30); + private readonly ICancelable timer = Context.System.Scheduler + .ScheduleTellRepeatedlyCancelable(TimerInterval, TimerInterval, Context.Self, new Timer(), ActorRefs.NoSender); + + private void OnMessage(Message msg) + { + foreach (MessageReceivedHandler handler in s_handlers) + if (!handler(_system, msg)) + return; + if (Version == null) + { + if (msg.Command != MessageCommand.Version) + throw new ProtocolViolationException(); + OnVersionMessageReceived((VersionPayload)msg.Payload!); + return; + } + if (!_verack) + { + if (msg.Command != MessageCommand.Verack) + throw new ProtocolViolationException(); + OnVerackMessageReceived(); + return; + } + switch (msg.Command) + { + case MessageCommand.Addr: + OnAddrMessageReceived((AddrPayload)msg.Payload!); + break; + case MessageCommand.Block: + case MessageCommand.Extensible: + OnInventoryReceived((IInventory)msg.Payload!); + break; + case MessageCommand.FilterAdd: + OnFilterAddMessageReceived((FilterAddPayload)msg.Payload!); + break; + case MessageCommand.FilterClear: + OnFilterClearMessageReceived(); + break; + case MessageCommand.FilterLoad: + OnFilterLoadMessageReceived((FilterLoadPayload)msg.Payload!); + break; + case MessageCommand.GetAddr: + OnGetAddrMessageReceived(); + break; + case MessageCommand.GetBlocks: + OnGetBlocksMessageReceived((GetBlocksPayload)msg.Payload!); + break; + case MessageCommand.GetBlockByIndex: + OnGetBlockByIndexMessageReceived((GetBlockByIndexPayload)msg.Payload!); + break; + case MessageCommand.GetData: + OnGetDataMessageReceived((InvPayload)msg.Payload!); + break; + case MessageCommand.GetHeaders: + OnGetHeadersMessageReceived((GetBlockByIndexPayload)msg.Payload!); + break; + case MessageCommand.Headers: + OnHeadersMessageReceived((HeadersPayload)msg.Payload!); + break; + case MessageCommand.Inv: + OnInvMessageReceived((InvPayload)msg.Payload!); + break; + case MessageCommand.Mempool: + OnMemPoolMessageReceived(); + break; + case MessageCommand.Ping: + OnPingMessageReceived((PingPayload)msg.Payload!); + break; + case MessageCommand.Pong: + OnPongMessageReceived((PingPayload)msg.Payload!); + break; + case MessageCommand.Transaction: + if (msg.Payload!.Size <= Transaction.MaxTransactionSize) + OnInventoryReceived((Transaction)msg.Payload); + break; + case MessageCommand.Verack: + case MessageCommand.Version: + throw new ProtocolViolationException(); + case MessageCommand.Alert: + case MessageCommand.MerkleBlock: + case MessageCommand.NotFound: + case MessageCommand.Reject: + default: break; + } + } + + private void OnAddrMessageReceived(AddrPayload payload) + { + ref bool sent = ref _sentCommands[(byte)MessageCommand.GetAddr]; + if (!sent) return; + sent = false; + var endPoints = payload.AddressList + .Select(p => p.EndPoint) + .Where(p => p.Port > 0) + .ToArray(); + _system.LocalNode.Tell(new Peer.Peers(endPoints)); + } + + private void OnFilterAddMessageReceived(FilterAddPayload payload) + { + _bloomFilter?.Add(payload.Data); + } + + private void OnFilterClearMessageReceived() + { + _bloomFilter = null; + } + + private void OnFilterLoadMessageReceived(FilterLoadPayload payload) + { + _bloomFilter = new BloomFilter(payload.Filter.Length * 8, payload.K, payload.Tweak, payload.Filter); + } + + /// + /// Will be triggered when a MessageCommand.GetAddr message is received. + /// Randomly select nodes from the local RemoteNodes and tells to RemoteNode actors a MessageCommand.Addr message. + /// The message contains a list of networkAddresses from those selected random peers. + /// + private void OnGetAddrMessageReceived() + { + IEnumerable peers = _localNode.RemoteNodes.Values + .Where(p => p.ListenerTcpPort > 0) + .GroupBy(p => p.Remote.Address, (k, g) => g.First()) + .OrderBy(p => RandomNumberFactory.NextInt32()) + .Take(AddrPayload.MaxCountToSend); + NetworkAddressWithTime[] networkAddresses = peers.Select(p => NetworkAddressWithTime.Create(p.Listener.Address, p.Version!.Timestamp, p.Version.Capabilities)).ToArray(); + if (networkAddresses.Length == 0) return; + EnqueueMessage(Message.Create(MessageCommand.Addr, AddrPayload.Create(networkAddresses))); + } + + /// + /// Will be triggered when a MessageCommand.GetBlocks message is received. + /// Tell the specified number of blocks' hashes starting with the requested HashStart until payload.Count or MaxHashesCount + /// Responses are sent to RemoteNode actor as MessageCommand.Inv Message. + /// + /// A GetBlocksPayload including start block Hash and number of blocks requested. + private void OnGetBlocksMessageReceived(GetBlocksPayload payload) + { + // The default value of payload.Count is -1 + int count = payload.Count < 0 || payload.Count > InvPayload.MaxHashesCount ? InvPayload.MaxHashesCount : payload.Count; + var snapshot = _system.StoreView; + UInt256? hash = payload.HashStart; + TrimmedBlock? state = NativeContract.Ledger.GetTrimmedBlock(snapshot, hash); + if (state == null) return; + uint currentHeight = NativeContract.Ledger.CurrentIndex(snapshot); + List hashes = new(); + for (uint i = 1; i <= count; i++) + { + uint index = state.Index + i; + if (index > currentHeight) + break; + hash = NativeContract.Ledger.GetBlockHash(snapshot, index); + if (hash == null) break; + hashes.Add(hash); + } + if (hashes.Count == 0) return; + EnqueueMessage(Message.Create(MessageCommand.Inv, InvPayload.Create(InventoryType.Block, hashes.ToArray()))); + } + + private void OnGetBlockByIndexMessageReceived(GetBlockByIndexPayload payload) + { + uint count = payload.Count == -1 ? InvPayload.MaxHashesCount : Math.Min((uint)payload.Count, InvPayload.MaxHashesCount); + for (uint i = payload.IndexStart, max = payload.IndexStart + count; i < max; i++) + { + Block? block = NativeContract.Ledger.GetBlock(_system.StoreView, i); + if (block == null) + break; + + if (_bloomFilter == null) + { + EnqueueMessage(Message.Create(MessageCommand.Block, block)); + } + else + { + BitArray flags = new(block.Transactions.Select(p => _bloomFilter.Test(p)).ToArray()); + EnqueueMessage(Message.Create(MessageCommand.MerkleBlock, MerkleBlockPayload.Create(block, flags))); + } + } + } + + /// + /// Will be triggered when a MessageCommand.GetData message is received. + /// The payload includes an array of hash values. + /// For different payload.Type (Tx, Block, Consensus), + /// get the corresponding (Txs, Blocks, Consensus) and tell them to RemoteNode actor. + /// + /// The payload containing the requested information. + private void OnGetDataMessageReceived(InvPayload payload) + { + var notFound = new List(); + foreach (var hash in payload.Hashes.Where(_sentHashes.TryAdd)) + { + switch (payload.Type) + { + case InventoryType.TX: + if (_system.MemPool.TryGetValue(hash, out Transaction? tx)) + EnqueueMessage(Message.Create(MessageCommand.Transaction, tx)); + else + notFound.Add(hash); + break; + case InventoryType.Block: + Block? block = NativeContract.Ledger.GetBlock(_system.StoreView, hash); + if (block != null) + { + if (_bloomFilter == null) + { + EnqueueMessage(Message.Create(MessageCommand.Block, block)); + } + else + { + BitArray flags = new(block.Transactions.Select(p => _bloomFilter.Test(p)).ToArray()); + EnqueueMessage(Message.Create(MessageCommand.MerkleBlock, MerkleBlockPayload.Create(block, flags))); + } + } + else + { + notFound.Add(hash); + } + break; + default: + if (_system.RelayCache.TryGet(hash, out IInventory? inventory)) + EnqueueMessage(Message.Create((MessageCommand)payload.Type, inventory)); + break; + } + } + + if (notFound.Count > 0) + { + foreach (InvPayload entry in InvPayload.CreateGroup(payload.Type, notFound)) + EnqueueMessage(Message.Create(MessageCommand.NotFound, entry)); + } + } + + /// + /// Will be triggered when a MessageCommand.GetHeaders message is received. + /// Tell the specified number of blocks' headers starting with the requested IndexStart to RemoteNode actor. + /// A limit set by HeadersPayload.MaxHeadersCount is also applied to the number of requested Headers, namely payload.Count. + /// + /// A GetBlockByIndexPayload including start block index and number of blocks' headers requested. + private void OnGetHeadersMessageReceived(GetBlockByIndexPayload payload) + { + var snapshot = _system.StoreView; + if (payload.IndexStart > NativeContract.Ledger.CurrentIndex(snapshot)) return; + var headers = new List
(); + uint count = payload.Count == -1 ? HeadersPayload.MaxHeadersCount : (uint)payload.Count; + for (uint i = 0; i < count; i++) + { + uint index = payload.IndexStart + i; + Header? header = _system.HeaderCache[index]; + if (header == null) + { + header = NativeContract.Ledger.GetHeader(snapshot, index); + if (header == null) break; + } + headers.Add(header); + } + if (headers.Count == 0) return; + EnqueueMessage(Message.Create(MessageCommand.Headers, HeadersPayload.Create(headers.ToArray()))); + } + + private void OnHeadersMessageReceived(HeadersPayload payload) + { + UpdateLastBlockIndex(payload.Headers[^1].Index); + _system.Blockchain.Tell(payload.Headers); + } + + private void OnInventoryReceived(IInventory inventory) + { + if (!_knownHashes.TryAdd(inventory.Hash)) return; + _pendingKnownHashes.Remove(inventory.Hash); + _system.TaskManager.Tell(inventory); + switch (inventory) + { + case Transaction transaction: + if (!(_system.ContainsTransaction(transaction.Hash) != ContainsTransactionType.NotExist || _system.ContainsConflictHash(transaction.Hash, transaction.Signers.Select(s => s.Account)))) + _system.TxRouter.Tell(new TransactionRouter.Preverify(transaction, true)); + break; + case Block block: + UpdateLastBlockIndex(block.Index); + if (block.Index > NativeContract.Ledger.CurrentIndex(_system.StoreView) + InvPayload.MaxHashesCount) return; + _system.Blockchain.Tell(inventory); + break; + default: + _system.Blockchain.Tell(inventory); + break; + } + } + + private void OnInvMessageReceived(InvPayload payload) + { + UInt256[] hashes; + var source = payload.Hashes + .Where(p => !_pendingKnownHashes.Contains(p) && !_knownHashes.Contains(p) && !_sentHashes.Contains(p)); + switch (payload.Type) + { + case InventoryType.Block: + { + var snapshot = _system.StoreView; + hashes = source.Where(p => !NativeContract.Ledger.ContainsBlock(snapshot, p)).ToArray(); + break; + } + case InventoryType.TX: + { + var snapshot = _system.StoreView; + hashes = source.Where(p => !NativeContract.Ledger.ContainsTransaction(snapshot, p)).ToArray(); + break; + } + default: + { + hashes = source.ToArray(); + break; + } + } + if (hashes.Length == 0) return; + foreach (var hash in hashes) + _pendingKnownHashes.TryAdd(Tuple.Create(hash, TimeProvider.Current.UtcNow)); + _system.TaskManager.Tell(new TaskManager.NewTasks(InvPayload.Create(payload.Type, hashes))); + } + + private void OnMemPoolMessageReceived() + { + foreach (InvPayload payload in InvPayload.CreateGroup(InventoryType.TX, _system.MemPool.GetVerifiedTransactions().Select(p => p.Hash).ToArray())) + EnqueueMessage(Message.Create(MessageCommand.Inv, payload)); + } + + private void OnPingMessageReceived(PingPayload payload) + { + UpdateLastBlockIndex(payload.LastBlockIndex); + EnqueueMessage(Message.Create(MessageCommand.Pong, PingPayload.Create(NativeContract.Ledger.CurrentIndex(_system.StoreView), payload.Nonce))); + } + + private void OnPongMessageReceived(PingPayload payload) + { + UpdateLastBlockIndex(payload.LastBlockIndex); + } + + private void OnVerackMessageReceived() + { + _verack = true; + _system.TaskManager.Tell(new TaskManager.Register(Version!)); + CheckMessageQueue(); + } + + private void OnVersionMessageReceived(VersionPayload payload) + { + Version = payload; + foreach (NodeCapability capability in payload.Capabilities) + { + switch (capability) + { + case FullNodeCapability fullNodeCapability: + IsFullNode = true; + LastBlockIndex = fullNodeCapability.StartHeight; + break; + case ServerCapability serverCapability: + if (serverCapability.Type == NodeCapabilityType.TcpServer) + ListenerTcpPort = serverCapability.Port; + break; + } + } + if (!_localNode.AllowNewConnection(Self, this)) + { + Disconnect(true); + return; + } + SendMessage(Message.Create(MessageCommand.Verack)); + } + + private void OnTimer() + { + var oneMinuteAgo = TimeProvider.Current.UtcNow.AddMinutes(-1); + while (_pendingKnownHashes.Count > 0) + { + var (_, time) = _pendingKnownHashes.FirstOrDefault!; + if (oneMinuteAgo <= time) break; + if (!_pendingKnownHashes.RemoveFirst()) break; + } + if (oneMinuteAgo > _lastSent) + EnqueueMessage(Message.Create(MessageCommand.Ping, PingPayload.Create(NativeContract.Ledger.CurrentIndex(_system.StoreView)))); + } + + private void UpdateLastBlockIndex(uint lastBlockIndex) + { + if (lastBlockIndex > LastBlockIndex) + { + LastBlockIndex = lastBlockIndex; + _system.TaskManager.Tell(new TaskManager.Update(LastBlockIndex)); + } + } +} diff --git a/src/Neo/Network/P2P/RemoteNode.cs b/src/Neo/Network/P2P/RemoteNode.cs new file mode 100644 index 0000000000..0aa16588c5 --- /dev/null +++ b/src/Neo/Network/P2P/RemoteNode.cs @@ -0,0 +1,267 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// RemoteNode.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Configuration; +using Akka.IO; +using Neo.Cryptography; +using Neo.IO; +using Neo.IO.Actors; +using Neo.IO.Caching; +using Neo.Network.P2P.Payloads; +using System.Collections; +using System.Net; + +namespace Neo.Network.P2P; + +/// +/// Represents a connection of the NEO network. +/// +public partial class RemoteNode : Connection +{ + internal record StartProtocol; + internal record Relay(IInventory Inventory); + + private readonly NeoSystem _system; + private readonly LocalNode _localNode; + private readonly Queue _messageQueueHigh = new(); + private readonly Queue _messageQueueLow = new(); + private DateTime _lastSent = TimeProvider.Current.UtcNow; + private readonly bool[] _sentCommands = new bool[1 << (sizeof(MessageCommand) * 8)]; + private ByteString _messageBuffer = ByteString.Empty; + private bool _ack = true; + private uint _lastHeightSent = 0; + + /// + /// The address of the remote Tcp server. + /// + public IPEndPoint Listener => new(Remote.Address, ListenerTcpPort); + + /// + /// The port listened by the remote Tcp server. If the remote node is not a server, this field is 0. + /// + public int ListenerTcpPort { get; private set; } = 0; + + /// + /// The sent by the remote node. + /// + public VersionPayload? Version { get; private set; } + + /// + /// The index of the last block sent by the remote node. + /// + public uint LastBlockIndex { get; private set; } = 0; + + /// + /// Indicates whether the remote node is a full node. + /// + public bool IsFullNode { get; private set; } = false; + + /// + /// Initializes a new instance of the class. + /// + /// The object that contains the . + /// The that manages the . + /// The underlying connection object. + /// The address of the remote node. + /// The address of the local node. + /// P2P settings. + public RemoteNode(NeoSystem system, LocalNode localNode, object connection, IPEndPoint remote, IPEndPoint local, ChannelsConfig config) + : base(connection, remote, local) + { + _system = system; + _localNode = localNode; + _knownHashes = new HashSetCache(Math.Max(1, config.MaxKnownHashes)); + _sentHashes = new HashSetCache(Math.Max(1, config.MaxKnownHashes)); + localNode.RemoteNodes.TryAdd(Self, this); + } + + /// + /// It defines the message queue to be used for dequeuing. + /// If the high-priority message queue is not empty, choose the high-priority message queue. + /// Otherwise, choose the low-priority message queue. + /// Finally, it sends the first message of the queue. + /// + private void CheckMessageQueue() + { + if (!_verack || !_ack) return; + Queue queue = _messageQueueHigh; + if (queue.Count == 0) + { + queue = _messageQueueLow; + if (queue.Count == 0) return; + } + SendMessage(queue.Dequeue()); + } + + private void EnqueueMessage(MessageCommand command, ISerializable? payload = null) + { + EnqueueMessage(Message.Create(command, payload)); + } + + /// + /// Add message to high priority queue or low priority queue depending on the message type. + /// + /// The message to be added. + private void EnqueueMessage(Message message) + { + bool is_single = message.Command switch + { + MessageCommand.Addr or MessageCommand.GetAddr or MessageCommand.GetBlocks or MessageCommand.GetHeaders or MessageCommand.Mempool or MessageCommand.Ping or MessageCommand.Pong => true, + _ => false, + }; + Queue message_queue = message.Command switch + { + MessageCommand.Alert or MessageCommand.Extensible or MessageCommand.FilterAdd or MessageCommand.FilterClear or MessageCommand.FilterLoad or MessageCommand.GetAddr or MessageCommand.Mempool => _messageQueueHigh, + _ => _messageQueueLow, + }; + if (!is_single || message_queue.All(p => p.Command != message.Command)) + { + message_queue.Enqueue(message); + _lastSent = TimeProvider.Current.UtcNow; + } + CheckMessageQueue(); + } + + protected override void OnAck() + { + _ack = true; + CheckMessageQueue(); + } + + protected override void OnData(ByteString data) + { + _messageBuffer = _messageBuffer.Concat(data); + + for (var message = TryParseMessage(); message != null; message = TryParseMessage()) + OnMessage(message); + } + + protected override void OnReceive(object message) + { + base.OnReceive(message); + switch (message) + { + case Timer _: + OnTimer(); + break; + case Message msg: + if (msg.Payload is PingPayload payload) + { + if (payload.LastBlockIndex > _lastHeightSent) + _lastHeightSent = payload.LastBlockIndex; + else if (msg.Command == MessageCommand.Ping) + break; + } + EnqueueMessage(msg); + break; + case IInventory inventory: + OnSend(inventory); + break; + case Relay relay: + OnRelay(relay.Inventory); + break; + case StartProtocol _: + OnStartProtocol(); + break; + } + } + + private void OnRelay(IInventory inventory) + { + if (!IsFullNode) return; + if (inventory.InventoryType == InventoryType.TX) + { + if (_bloomFilter != null && !_bloomFilter.Test((Transaction)inventory)) + return; + } + EnqueueMessage(MessageCommand.Inv, InvPayload.Create(inventory.InventoryType, inventory.Hash)); + } + + private void OnSend(IInventory inventory) + { + if (!IsFullNode) return; + if (inventory.InventoryType == InventoryType.TX) + { + if (_bloomFilter != null && !_bloomFilter.Test((Transaction)inventory)) + return; + } + EnqueueMessage((MessageCommand)inventory.InventoryType, inventory); + } + + private void OnStartProtocol() + { + SendMessage(Message.Create(MessageCommand.Version, VersionPayload.Create(_system.Settings.Network, LocalNode.Nonce, LocalNode.UserAgent, _localNode.GetNodeCapabilities()))); + } + + protected override void PostStop() + { + timer.CancelIfNotNull(); + if (_localNode.RemoteNodes.TryRemove(Self, out _)) + { + _knownHashes.Clear(); + _sentHashes.Clear(); + } + base.PostStop(); + } + + internal static Props Props(NeoSystem system, LocalNode localNode, object connection, IPEndPoint remote, IPEndPoint local, ChannelsConfig config) + { + return Akka.Actor.Props.Create(() => new RemoteNode(system, localNode, connection, remote, local, config)).WithMailbox("remote-node-mailbox"); + } + + private void SendMessage(Message message) + { + _ack = false; + // Here it is possible that we dont have the Version message yet, + // so we need to send the message uncompressed + SendData(ByteString.FromBytes(message.ToArray(Version?.AllowCompression ?? false))); + _sentCommands[(byte)message.Command] = true; + } + + private Message? TryParseMessage() + { + var length = Message.TryDeserialize(_messageBuffer, out var msg); + if (length <= 0) return null; + + _messageBuffer = _messageBuffer.Slice(length).Compact(); + return msg; + } +} + +internal class RemoteNodeMailbox : PriorityMailbox +{ + public RemoteNodeMailbox(Settings settings, Config config) : base(settings, config) { } + + internal protected override bool IsHighPriority(object message) + { + return message switch + { + Message msg => msg.Command switch + { + MessageCommand.Extensible or MessageCommand.FilterAdd or MessageCommand.FilterClear or MessageCommand.FilterLoad or MessageCommand.Verack or MessageCommand.Version or MessageCommand.Alert => true, + _ => false, + }, + Tcp.ConnectionClosed _ or Connection.Close _ or Connection.Ack _ => true, + _ => false, + }; + } + + internal protected override bool ShallDrop(object message, IEnumerable queue) + { + if (message is not Message msg) return false; + return msg.Command switch + { + MessageCommand.GetAddr or MessageCommand.GetBlocks or MessageCommand.GetHeaders or MessageCommand.Mempool => queue.OfType().Any(p => p.Command == msg.Command), + _ => false, + }; + } +} diff --git a/src/Neo/Network/P2P/TaskManager.cs b/src/Neo/Network/P2P/TaskManager.cs new file mode 100644 index 0000000000..f25a047221 --- /dev/null +++ b/src/Neo/Network/P2P/TaskManager.cs @@ -0,0 +1,447 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TaskManager.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Configuration; +using Akka.IO; +using Neo.Collections; +using Neo.IO.Actors; +using Neo.IO.Caching; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract.Native; +using System.Collections; +using System.Runtime.CompilerServices; + +namespace Neo.Network.P2P; + +/// +/// Actor used to manage the tasks of inventories. +/// +public class TaskManager : UntypedActor +{ + internal record Register(VersionPayload Version); + internal record Update(uint LastBlockIndex); + internal record NewTasks(InvPayload Payload); + + /// + /// Sent to to restart tasks for inventories. + /// + /// The inventories that need to restart. + public record RestartTasks(InvPayload Payload); + + private record Timer; + + private static readonly TimeSpan TimerInterval = TimeSpan.FromSeconds(30); + private static readonly TimeSpan TaskTimeout = TimeSpan.FromMinutes(1); + private static readonly UInt256 HeaderTaskHash = UInt256.Zero; + + private const int MaxConcurrentTasks = 3; + + private readonly NeoSystem system; + /// + /// A set of known hashes, of inventories or payloads, already received. + /// + private readonly HashSetCache _knownHashes; + private readonly Dictionary globalInvTasks = new(); + private readonly Dictionary globalIndexTasks = new(); + private readonly Dictionary sessions = new(); + private readonly ICancelable timer = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable( + TimerInterval, TimerInterval, Context.Self, new Timer(), ActorRefs.NoSender); + private uint lastSeenPersistedIndex = 0; + + private bool HasHeaderTask => globalInvTasks.ContainsKey(HeaderTaskHash); + + /// + /// Initializes a new instance of the class. + /// + /// The object that contains the . + public TaskManager(NeoSystem system) + { + this.system = system; + // Exactly the same as mempool + _knownHashes = new HashSetCache(Math.Max(100, system.MemPool.Capacity)); + Context.System.EventStream.Subscribe(Self, typeof(Blockchain.PersistCompleted)); + Context.System.EventStream.Subscribe(Self, typeof(Blockchain.RelayResult)); + } + + private void OnHeaders(Header[] _) + { + if (!sessions.TryGetValue(Sender, out var session)) return; + if (session.InvTasks.Remove(HeaderTaskHash)) + DecrementGlobalTask(HeaderTaskHash); + RequestTasks(Sender, session); + } + + private void OnInvalidBlock(Block invalidBlock) + { + foreach (var (actor, session) in sessions) + { + if (session.ReceivedBlock.TryGetValue(invalidBlock.Index, out var block)) + { + if (block.Hash == invalidBlock.Hash) + actor.Tell(Tcp.Abort.Instance); + } + } + } + + private void OnNewTasks(InvPayload payload) + { + if (!sessions.TryGetValue(Sender, out var session)) return; + + // Do not accept payload of type InventoryType.TX if not synced on HeaderHeight + uint currentHeight = Math.Max(NativeContract.Ledger.CurrentIndex(system.StoreView), lastSeenPersistedIndex); + uint headerHeight = system.HeaderCache.Last?.Index ?? currentHeight; + if (currentHeight < headerHeight && (payload.Type == InventoryType.TX || (payload.Type == InventoryType.Block && currentHeight < session.LastBlockIndex - InvPayload.MaxHashesCount))) + { + RequestTasks(Sender, session); + return; + } + + HashSet hashes = [.. payload.Hashes]; + // Remove all previously processed knownHashes from the list that is being requested + hashes.Remove(_knownHashes); + // Add to AvailableTasks the ones, of type InventoryType.Block, that are global (already under process by other sessions) + if (payload.Type == InventoryType.Block) + session.AvailableTasks.UnionWith(hashes.Where(p => globalInvTasks.ContainsKey(p))); + + // Remove those that are already in process by other sessions + hashes.Remove(globalInvTasks); + if (hashes.Count == 0) + { + RequestTasks(Sender, session); + return; + } + + // Update globalTasks with the ones that will be requested within this current session + foreach (UInt256 hash in hashes) + { + IncrementGlobalTask(hash); + session.InvTasks[hash] = TimeProvider.Current.UtcNow; + } + + foreach (InvPayload group in InvPayload.CreateGroup(payload.Type, hashes)) + Sender.Tell(Message.Create(MessageCommand.GetData, group)); + } + + private void OnPersistCompleted(Block block) + { + lastSeenPersistedIndex = block.Index; + + foreach (var (actor, session) in sessions) + if (session.ReceivedBlock.Remove(block.Index, out Block? receivedBlock)) + { + if (block.Hash == receivedBlock.Hash) + RequestTasks(actor, session); + else + actor.Tell(Tcp.Abort.Instance); + } + } + + protected override void OnReceive(object message) + { + switch (message) + { + case Register register: + OnRegister(register.Version); + break; + case Update update: + OnUpdate(update); + break; + case NewTasks tasks: + OnNewTasks(tasks.Payload); + break; + case RestartTasks restart: + OnRestartTasks(restart.Payload); + break; + case Header[] headers: + OnHeaders(headers); + break; + case IInventory inventory: + OnTaskCompleted(inventory); + break; + case Blockchain.PersistCompleted pc: + OnPersistCompleted(pc.Block); + break; + case Blockchain.RelayResult rr: + if (rr.Inventory is Block invalidBlock && rr.Result == VerifyResult.Invalid) + OnInvalidBlock(invalidBlock); + break; + case Timer _: + OnTimer(); + break; + case Terminated terminated: + OnTerminated(terminated.ActorRef); + break; + } + } + + private void OnRegister(VersionPayload version) + { + Context.Watch(Sender); + TaskSession session = new(version); + sessions.Add(Sender, session); + RequestTasks(Sender, session); + } + + private void OnUpdate(Update update) + { + if (!sessions.TryGetValue(Sender, out var session)) return; + session.LastBlockIndex = update.LastBlockIndex; + } + + private void OnRestartTasks(InvPayload payload) + { + _knownHashes.ExceptWith(payload.Hashes); + foreach (var hash in payload.Hashes) + { + globalInvTasks.Remove(hash); + } + + foreach (var group in InvPayload.CreateGroup(payload.Type, payload.Hashes)) + { + system.LocalNode.Tell(Message.Create(MessageCommand.GetData, group)); + } + } + + private void OnTaskCompleted(IInventory inventory) + { + var block = inventory as Block; + _knownHashes.TryAdd(inventory.Hash); + globalInvTasks.Remove(inventory.Hash); + if (block is not null) + { + globalIndexTasks.Remove(block.Index); + } + + foreach (var ms in sessions.Values) + { + ms.AvailableTasks.Remove(inventory.Hash); + } + + if (sessions.TryGetValue(Sender, out var session)) + { + session.InvTasks.Remove(inventory.Hash); + if (block is not null) + { + session.IndexTasks.Remove(block.Index); + if (session.ReceivedBlock.TryGetValue(block.Index, out var blockOld)) + { + if (block.Hash != blockOld.Hash) + { + Sender.Tell(Tcp.Abort.Instance); + return; + } + } + else + { + session.ReceivedBlock.Add(block.Index, block); + } + } + else + { + RequestTasks(Sender, session); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecrementGlobalTask(UInt256 hash) + { + if (globalInvTasks.TryGetValue(hash, out var value)) + { + if (value == 1) + globalInvTasks.Remove(hash); + else + globalInvTasks[hash] = value - 1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecrementGlobalTask(uint index) + { + if (globalIndexTasks.TryGetValue(index, out var value)) + { + if (value == 1) + globalIndexTasks.Remove(index); + else + globalIndexTasks[index] = value - 1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IncrementGlobalTask(UInt256 hash) + { + if (!globalInvTasks.TryGetValue(hash, out var value)) + { + globalInvTasks[hash] = 1; + return true; + } + + if (value >= MaxConcurrentTasks) return false; + + globalInvTasks[hash] = value + 1; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IncrementGlobalTask(uint index) + { + if (!globalIndexTasks.TryGetValue(index, out var value)) + { + globalIndexTasks[index] = 1; + return true; + } + + if (value >= MaxConcurrentTasks) return false; + + globalIndexTasks[index] = value + 1; + return true; + } + + private void OnTerminated(IActorRef actor) + { + if (!sessions.TryGetValue(actor, out var session)) return; + foreach (var hash in session.InvTasks.Keys) + { + DecrementGlobalTask(hash); + } + foreach (var index in session.IndexTasks.Keys) + { + DecrementGlobalTask(index); + } + sessions.Remove(actor); + } + + private void OnTimer() + { + foreach (var session in sessions.Values) + { + var now = TimeProvider.Current.UtcNow; + session.InvTasks.RemoveWhere(p => now - p.Value > TaskTimeout, p => DecrementGlobalTask(p.Key)); + session.IndexTasks.RemoveWhere(p => now - p.Value > TaskTimeout, p => DecrementGlobalTask(p.Key)); + } + + foreach (var (actor, session) in sessions) + { + RequestTasks(actor, session); + } + } + + protected override void PostStop() + { + timer.CancelIfNotNull(); + base.PostStop(); + } + + /// + /// Gets a object used for creating the actor. + /// + /// The object that contains the . + /// The object used for creating the actor. + public static Props Props(NeoSystem system) + { + return Akka.Actor.Props.Create(() => new TaskManager(system)).WithMailbox("task-manager-mailbox"); + } + + private void RequestTasks(IActorRef remoteNode, TaskSession session) + { + if (session.HasTooManyTasks) return; + + var snapshot = system.StoreView; + + // If there are pending tasks of InventoryType.Block we should process them + if (session.AvailableTasks.Count > 0) + { + session.AvailableTasks.Remove(_knownHashes); + // Search any similar hash that is on Singleton's knowledge, which means, on the way or already processed + session.AvailableTasks.RemoveWhere(p => NativeContract.Ledger.ContainsBlock(snapshot, p)); + HashSet hashes = [.. session.AvailableTasks]; + if (hashes.Count > 0) + { + hashes.RemoveWhere(p => !IncrementGlobalTask(p)); + session.AvailableTasks.Remove(hashes); + + foreach (UInt256 hash in hashes) + session.InvTasks[hash] = DateTime.UtcNow; + + foreach (InvPayload group in InvPayload.CreateGroup(InventoryType.Block, hashes)) + remoteNode.Tell(Message.Create(MessageCommand.GetData, group)); + return; + } + } + + uint currentHeight = Math.Max(NativeContract.Ledger.CurrentIndex(snapshot), lastSeenPersistedIndex); + uint headerHeight = system.HeaderCache.Last?.Index ?? currentHeight; + // When the number of AvailableTasks is no more than 0, + // no pending tasks of InventoryType.Block, it should process pending the tasks of headers + // If not HeaderTask pending to be processed it should ask for more Blocks + if ((!HasHeaderTask || globalInvTasks[HeaderTaskHash] < MaxConcurrentTasks) && headerHeight < session.LastBlockIndex && !system.HeaderCache.Full) + { + session.InvTasks[HeaderTaskHash] = DateTime.UtcNow; + IncrementGlobalTask(HeaderTaskHash); + remoteNode.Tell(Message.Create(MessageCommand.GetHeaders, GetBlockByIndexPayload.Create(headerHeight + 1))); + } + else if (currentHeight < session.LastBlockIndex) + { + uint startHeight = currentHeight + 1; + while (globalIndexTasks.ContainsKey(startHeight) || session.ReceivedBlock.ContainsKey(startHeight)) { startHeight++; } + if (startHeight > session.LastBlockIndex || startHeight >= currentHeight + InvPayload.MaxHashesCount) return; + uint endHeight = startHeight; + while (!globalIndexTasks.ContainsKey(++endHeight) && endHeight <= session.LastBlockIndex && endHeight <= currentHeight + InvPayload.MaxHashesCount) { } + uint count = Math.Min(endHeight - startHeight, InvPayload.MaxHashesCount); + for (uint i = 0; i < count; i++) + { + session.IndexTasks[startHeight + i] = TimeProvider.Current.UtcNow; + IncrementGlobalTask(startHeight + i); + } + remoteNode.Tell(Message.Create(MessageCommand.GetBlockByIndex, GetBlockByIndexPayload.Create(startHeight, (short)count))); + } + else if (!session.MempoolSent) + { + session.MempoolSent = true; + remoteNode.Tell(Message.Create(MessageCommand.Mempool)); + } + } +} + +internal class TaskManagerMailbox : PriorityMailbox +{ + public TaskManagerMailbox(Settings settings, Config config) + : base(settings, config) + { + } + + internal protected override bool IsHighPriority(object message) + { + switch (message) + { + case TaskManager.Register _: + case TaskManager.Update _: + case TaskManager.RestartTasks _: + return true; + case TaskManager.NewTasks tasks: + if (tasks.Payload.Type == InventoryType.Block || tasks.Payload.Type == InventoryType.Extensible) + return true; + return false; + default: + return false; + } + } + + internal protected override bool ShallDrop(object message, IEnumerable queue) + { + if (message is not TaskManager.NewTasks tasks) return false; + // Remove duplicate tasks + return queue.OfType() + .Any(x => x.Payload.Type == tasks.Payload.Type && x.Payload.Hashes.SequenceEqual(tasks.Payload.Hashes)); + } +} diff --git a/src/Neo/Network/P2P/TaskSession.cs b/src/Neo/Network/P2P/TaskSession.cs new file mode 100644 index 0000000000..60c8a07094 --- /dev/null +++ b/src/Neo/Network/P2P/TaskSession.cs @@ -0,0 +1,34 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TaskSession.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Capabilities; +using Neo.Network.P2P.Payloads; + +namespace Neo.Network.P2P; + +internal class TaskSession +{ + public Dictionary InvTasks { get; } = new Dictionary(); + public Dictionary IndexTasks { get; } = new Dictionary(); + public HashSet AvailableTasks { get; } = new HashSet(); + public Dictionary ReceivedBlock { get; } = new Dictionary(); + public bool HasTooManyTasks => InvTasks.Count + IndexTasks.Count >= 100; + public bool IsFullNode { get; } + public uint LastBlockIndex { get; set; } + public bool MempoolSent { get; set; } + + public TaskSession(VersionPayload version) + { + var fullNode = version.Capabilities.OfType().FirstOrDefault(); + IsFullNode = fullNode != null; + LastBlockIndex = fullNode?.StartHeight ?? 0; + } +} diff --git a/src/Neo/Persistence/ClonedCache.cs b/src/Neo/Persistence/ClonedCache.cs new file mode 100644 index 0000000000..87e5421aa0 --- /dev/null +++ b/src/Neo/Persistence/ClonedCache.cs @@ -0,0 +1,59 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ClonedCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.Persistence; + +class ClonedCache(DataCache innerCache) : DataCache(false) +{ + private readonly DataCache _innerCache = innerCache; + + protected override void AddInternal(StorageKey key, StorageItem value) + { + _innerCache.Add(key, value.Clone()); + } + + protected override void DeleteInternal(StorageKey key) + { + _innerCache.Delete(key); + } + + protected override bool ContainsInternal(StorageKey key) + { + return _innerCache.Contains(key); + } + + /// + protected override StorageItem GetInternal(StorageKey key) + { + return _innerCache[key].Clone(); + } + + protected override IEnumerable<(StorageKey, StorageItem)> SeekInternal(byte[] keyOrPreifx, SeekDirection direction) + { + foreach (var (key, value) in _innerCache.Seek(keyOrPreifx, direction)) + yield return (key, value.Clone()); + } + + protected override StorageItem? TryGetInternal(StorageKey key) + { + return _innerCache.TryGet(key)?.Clone(); + } + + protected override void UpdateInternal(StorageKey key, StorageItem value) + { + var entry = _innerCache.GetAndChange(key) + ?? throw new KeyNotFoundException(); + + entry.FromReplica(value); + } +} diff --git a/src/Neo/Persistence/DataCache.cs b/src/Neo/Persistence/DataCache.cs new file mode 100644 index 0000000000..4a3d8c7a4e --- /dev/null +++ b/src/Neo/Persistence/DataCache.cs @@ -0,0 +1,579 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// DataCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Neo.Persistence; + +/// +/// Represents a cache for the underlying storage of the NEO blockchain. +/// +public abstract class DataCache : IReadOnlyStore +{ + /// + /// Represents an entry in the cache. + /// + [DebuggerDisplay("{Item.ToString()}, State = {State.ToString()}")] + public class Trackable(StorageItem item, TrackState state) + { + /// + /// The data of the entry. + /// + public StorageItem Item { get; set; } = item; + + /// + /// The state of the entry. + /// + public TrackState State { get; set; } = state; + } + + /// + /// Delegate for storage entries + /// + /// DataCache + /// Key + /// Item + public delegate void OnEntryDelegate(DataCache sender, StorageKey key, StorageItem item); + + private readonly Dictionary _dictionary = []; + private readonly HashSet? _changeSet; + + public event OnEntryDelegate? OnRead; + public event OnEntryDelegate? OnUpdate; + + /// + /// True if DataCache is readOnly + /// + public bool IsReadOnly => _changeSet == null; + + /// + /// Reads a specified entry from the cache. If the entry is not in the cache, it will be automatically loaded from the underlying storage. + /// + /// The key of the entry. + /// The cached data. + /// If the entry doesn't exist. + public StorageItem this[StorageKey key] + { + get + { + lock (_dictionary) + { + if (_dictionary.TryGetValue(key, out var trackable)) + { + if (trackable.State == TrackState.Deleted || trackable.State == TrackState.NotFound) + throw new KeyNotFoundException(); + } + else + { + trackable = new Trackable(GetInternal(key), TrackState.None); + _dictionary.Add(key, trackable); + } + return trackable.Item; + } + } + } + + /// + /// Data cache constructor + /// + /// True if you don't want to allow writes + protected DataCache(bool readOnly) + { + if (!readOnly) + _changeSet = []; + } + + /// + /// Adds a new entry to the cache. + /// + /// The key of the entry. + /// The data of the entry. + /// The entry has already been cached. + /// Note: This method does not read the internal storage to check whether the record already exists. + public void Add(StorageKey key, StorageItem value) + { + lock (_dictionary) + { + if (_dictionary.TryGetValue(key, out var trackable)) + { + trackable.Item = value; + trackable.State = trackable.State switch + { + TrackState.Deleted => TrackState.Changed, + TrackState.NotFound => TrackState.Added, + _ => throw new ArgumentException($"The element currently has state {trackable.State}") + }; + } + else + { + _dictionary[key] = new Trackable(value, TrackState.Added); + } + _changeSet?.Add(key); + } + } + + /// + /// Adds a new entry to the underlying storage. + /// + /// The key of the entry. + /// The data of the entry. + protected abstract void AddInternal(StorageKey key, StorageItem value); + + /// + /// Commits all changes in the cache to the underlying storage. + /// + public virtual void Commit() + { + if (_changeSet is null) + { + throw new InvalidOperationException("DataCache is read only"); + } + + lock (_dictionary) + { + foreach (var key in _changeSet) + { + var trackable = _dictionary[key]; + switch (trackable.State) + { + case TrackState.Added: + AddInternal(key, trackable.Item); + trackable.State = TrackState.None; + break; + case TrackState.Changed: + UpdateInternalWrapper(key, trackable.Item); + trackable.State = TrackState.None; + break; + case TrackState.Deleted: + DeleteInternal(key); + _dictionary.Remove(key); + break; + } + } + _changeSet.Clear(); + } + } + + /// + /// Gets the change set in the cache. + /// + /// The change set. + public IEnumerable> GetChangeSet() + { + if (_changeSet is null) + { + throw new InvalidOperationException("DataCache is read only"); + } + + lock (_dictionary) + { + foreach (var key in _changeSet) + yield return new(key, _dictionary[key]); + } + } + + /// + /// Creates a clone of the snapshot cache, which uses this instance as the underlying storage. + /// + /// The of this instance. + public DataCache CloneCache() + { + return new ClonedCache(this); + } + + /// + /// Deletes an entry from the cache. + /// + /// The key of the entry. + public void Delete(StorageKey key) + { + lock (_dictionary) + { + if (_dictionary.TryGetValue(key, out var trackable)) + { + if (trackable.State == TrackState.Added) + { + trackable.State = TrackState.NotFound; + _changeSet?.Remove(key); + } + else if (trackable.State != TrackState.NotFound) + { + trackable.State = TrackState.Deleted; + _changeSet?.Add(key); + } + } + else + { + var item = TryGetInternalWrapper(key); + if (item == null) return; + _dictionary.Add(key, new Trackable(item, TrackState.Deleted)); + _changeSet?.Add(key); + } + } + } + + /// + /// Deletes an entry from the underlying storage. + /// + /// The key of the entry. + protected abstract void DeleteInternal(StorageKey key); + + /// + /// Finds the entries starting with the specified prefix. + /// + /// The search direction. + /// The entries found with the desired prefix. + public IEnumerable<(StorageKey Key, StorageItem Value)> Find(SeekDirection direction = SeekDirection.Forward) + { + return Find((byte[]?)null, direction); + } + + /// + public IEnumerable<(StorageKey Key, StorageItem Value)> Find(StorageKey? keyPrefix = null, SeekDirection direction = SeekDirection.Forward) + { + var key = keyPrefix?.ToArray(); + return Find(key, direction); + } + + /// + /// Finds the entries starting with the specified prefix. + /// + /// The prefix of the key. + /// The search direction. + /// The entries found with the desired prefix. + public IEnumerable<(StorageKey Key, StorageItem Value)> Find(byte[]? keyPrefix = null, SeekDirection direction = SeekDirection.Forward) + { + var seekPrefix = keyPrefix; + if (direction == SeekDirection.Backward) + { + ArgumentNullException.ThrowIfNull(keyPrefix); + if (keyPrefix.Length == 0) + { + // Backwards seek for zero prefix is not supported for now. + throw new ArgumentOutOfRangeException(nameof(keyPrefix)); + } + seekPrefix = null; + for (var i = keyPrefix.Length - 1; i >= 0; i--) + { + if (keyPrefix[i] < 0xff) + { + seekPrefix = keyPrefix.Take(i + 1).ToArray(); + // The next key after the keyPrefix. + seekPrefix[i]++; + break; + } + } + if (seekPrefix == null) + { + throw new ArgumentException($"{nameof(keyPrefix)} with all bytes being 0xff is not supported now"); + } + } + return FindInternal(keyPrefix, seekPrefix, direction); + } + + private IEnumerable<(StorageKey Key, StorageItem Value)> FindInternal(byte[]? keyPrefix, byte[]? seekPrefix, SeekDirection direction) + { + foreach (var (key, value) in Seek(seekPrefix, direction)) + { + if (keyPrefix == null || key.ToArray().AsSpan().StartsWith(keyPrefix)) + yield return (key, value); + else if (direction == SeekDirection.Forward || (seekPrefix == null || !key.ToArray().SequenceEqual(seekPrefix))) + yield break; + } + } + + /// + /// Finds the entries that between [start, end). + /// + /// The start key (inclusive). + /// The end key (exclusive). + /// The search direction. + /// The entries found with the desired range. + public IEnumerable<(StorageKey Key, StorageItem Value)> FindRange(byte[] start, byte[] end, SeekDirection direction = SeekDirection.Forward) + { + var comparer = direction == SeekDirection.Forward + ? ByteArrayComparer.Default + : ByteArrayComparer.Reverse; + foreach (var (key, value) in Seek(start, direction)) + { + if (comparer.Compare(key.ToArray(), end) < 0) + yield return (key, value); + else + yield break; + } + } + + /// + /// Determines whether the cache contains the specified entry. + /// + /// The key of the entry. + /// if the cache contains an entry with the specified key; otherwise, . + public bool Contains(StorageKey key) + { + lock (_dictionary) + { + if (_dictionary.TryGetValue(key, out var trackable)) + return trackable.State != TrackState.Deleted && trackable.State != TrackState.NotFound; + return ContainsInternal(key); + } + } + + /// + /// Determines whether the underlying storage contains the specified entry. + /// + /// The key of the entry. + /// if the underlying storage contains an entry with the specified key; otherwise, . + protected abstract bool ContainsInternal(StorageKey key); + + /// + /// Reads a specified entry from the underlying storage. + /// + /// The key of the entry. + /// The data of the entry. Or throw if the entry doesn't exist. + /// If the entry doesn't exist. + protected abstract StorageItem GetInternal(StorageKey key); + + /// + /// Reads a specified entry from the cache, and mark it as . + /// If the entry is not in the cache, it will be automatically loaded from the underlying storage. + /// + /// The key of the entry. + /// + /// A delegate used to create the entry if it doesn't exist. + /// If the entry already exists, the factory will not be used. + /// + /// + /// The cached data, or if it doesn't exist and the is not provided. + /// + [return: NotNullIfNotNull(nameof(factory))] + public StorageItem? GetAndChange(StorageKey key, Func? factory = null) + { + lock (_dictionary) + { + if (_dictionary.TryGetValue(key, out var trackable)) + { + if (trackable.State == TrackState.Deleted || trackable.State == TrackState.NotFound) + { + if (factory == null) return null; + trackable.Item = factory(); + if (trackable.State == TrackState.Deleted) + { + trackable.State = TrackState.Changed; + } + else + { + trackable.State = TrackState.Added; + _changeSet?.Add(key); + } + } + else if (trackable.State == TrackState.None) + { + trackable.State = TrackState.Changed; + _changeSet?.Add(key); + } + } + else + { + var item = TryGetInternalWrapper(key); + if (item == null) + { + if (factory == null) return null; + trackable = new Trackable(factory(), TrackState.Added); + } + else + { + trackable = new Trackable(item, TrackState.Changed); + } + _dictionary.Add(key, trackable); + _changeSet?.Add(key); + } + return trackable.Item; + } + } + + private StorageItem? TryGetInternalWrapper(StorageKey key) + { + var item = TryGetInternal(key); + if (item == null) return null; + + OnRead?.Invoke(this, key, item); + return item; + } + + private void UpdateInternalWrapper(StorageKey key, StorageItem value) + { + UpdateInternal(key, value); + OnUpdate?.Invoke(this, key, value); + } + + /// + /// Reads a specified entry from the cache. + /// If the entry is not in the cache, it will be automatically loaded from the underlying storage. + /// If the entry doesn't exist, the factory will be used to create a new one. + /// + /// The key of the entry. + /// + /// A delegate used to create the entry if it doesn't exist. + /// If the entry already exists, the factory will not be used. + /// + /// The cached data. + public StorageItem GetOrAdd(StorageKey key, Func factory) + { + lock (_dictionary) + { + if (_dictionary.TryGetValue(key, out var trackable)) + { + if (trackable.State == TrackState.Deleted || trackable.State == TrackState.NotFound) + { + trackable.Item = factory(); + if (trackable.State == TrackState.Deleted) + { + trackable.State = TrackState.Changed; + } + else + { + trackable.State = TrackState.Added; + _changeSet?.Add(key); + } + } + } + else + { + var item = TryGetInternalWrapper(key); + if (item == null) + { + trackable = new Trackable(factory(), TrackState.Added); + _changeSet?.Add(key); + } + else + { + trackable = new Trackable(item, TrackState.None); + } + _dictionary.Add(key, trackable); + } + return trackable.Item; + } + } + + /// + /// Seeks to the entry with the specified key. + /// + /// The key to be sought. + /// The direction of seek. + /// An enumerator containing all the entries after seeking. + public IEnumerable<(StorageKey Key, StorageItem Value)> Seek(byte[]? keyOrPrefix = null, SeekDirection direction = SeekDirection.Forward) + { + List<(byte[], StorageKey, StorageItem)> cached; + HashSet cachedKeySet; + var comparer = direction == SeekDirection.Forward ? ByteArrayComparer.Default : ByteArrayComparer.Reverse; + lock (_dictionary) + { + cached = _dictionary + .Where(p => p.Value.State != TrackState.Deleted && p.Value.State != TrackState.NotFound && (keyOrPrefix == null || comparer.Compare(p.Key.ToArray(), keyOrPrefix) >= 0)) + .Select(p => + ( + KeyBytes: p.Key.ToArray(), + p.Key, + p.Value.Item + )) + .OrderBy(p => p.KeyBytes, comparer) + .ToList(); + cachedKeySet = new HashSet(_dictionary.Keys); + } + var uncached = SeekInternal(keyOrPrefix ?? Array.Empty(), direction) + .Where(p => !cachedKeySet.Contains(p.Key)) + .Select(p => + ( + KeyBytes: p.Key.ToArray(), + p.Key, + p.Value + )); + using var e1 = cached.GetEnumerator(); + using var e2 = uncached.GetEnumerator(); + (byte[] KeyBytes, StorageKey Key, StorageItem Item) i1, i2; + var c1 = e1.MoveNext(); + var c2 = e2.MoveNext(); + i1 = c1 ? e1.Current : default; + i2 = c2 ? e2.Current : default; + while (c1 || c2) + { + if (!c2 || (c1 && comparer.Compare(i1.KeyBytes, i2.KeyBytes) < 0)) + { + if (i1.Key == null || i1.Item == null) throw new NullReferenceException("SeekInternal returned a null key or item"); + yield return (i1.Key, i1.Item); + c1 = e1.MoveNext(); + i1 = c1 ? e1.Current : default; + } + else + { + if (i2.Key == null || i2.Item == null) throw new NullReferenceException("SeekInternal returned a null key or item"); + yield return (i2.Key, i2.Item); + c2 = e2.MoveNext(); + i2 = c2 ? e2.Current : default; + } + } + } + + /// + /// Seeks to the entry with the specified key in the underlying storage. + /// + /// The key to be sought. + /// The direction of seek. + /// An enumerator containing all the entries after seeking. + protected abstract IEnumerable<(StorageKey Key, StorageItem Value)> SeekInternal(byte[] keyOrPrefix, SeekDirection direction); + + /// + /// Reads a specified entry from the cache. If the entry is not in the cache, it will be automatically loaded from the underlying storage. + /// + /// The key of the entry. + /// The cached data. Or if it is neither in the cache nor in the underlying storage. + public StorageItem? TryGet(StorageKey key) + { + lock (_dictionary) + { + if (_dictionary.TryGetValue(key, out var trackable)) + { + if (trackable.State == TrackState.Deleted || trackable.State == TrackState.NotFound) + return null; + return trackable.Item; + } + var value = TryGetInternalWrapper(key); + if (value == null) return null; + _dictionary.Add(key, new Trackable(value, TrackState.None)); + return value; + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGet(StorageKey key, [NotNullWhen(true)] out StorageItem? item) + { + item = TryGet(key); + return item != null; + } + + /// + /// Reads a specified entry from the underlying storage. + /// + /// The key of the entry. + /// The data of the entry. Or if it doesn't exist. + protected abstract StorageItem? TryGetInternal(StorageKey key); + + /// + /// Updates an entry in the underlying storage. + /// + /// The key of the entry. + /// The data of the entry. + protected abstract void UpdateInternal(StorageKey key, StorageItem value); +} diff --git a/src/Neo/Persistence/IReadOnlyStore.cs b/src/Neo/Persistence/IReadOnlyStore.cs new file mode 100644 index 0000000000..f62b3d5cc8 --- /dev/null +++ b/src/Neo/Persistence/IReadOnlyStore.cs @@ -0,0 +1,73 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IReadOnlyStore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 System.Diagnostics.CodeAnalysis; + +namespace Neo.Persistence; + +/// +/// This interface provides methods to read from the database. +/// +public interface IReadOnlyStore : IReadOnlyStore { } + +/// +/// This interface provides methods to read from the database. +/// +public interface IReadOnlyStore where TKey : class? +{ + /// + /// Gets the entry with the specified key. + /// + /// The key to get. + /// The entry if found, throws a otherwise. + public TValue this[TKey key] + { + get + { + return TryGet(key) ?? throw new KeyNotFoundException(); + } + } + + /// + /// Reads a specified entry from the database. + /// + /// The key of the entry. + /// The data of the entry; if the value is not found in the database. + TValue? TryGet(TKey key); + + /// + /// Reads a specified entry from the database. + /// + /// The key of the entry. + /// The data of the entry. + /// if the entry exists; otherwise, . + public bool TryGet(TKey key, [NotNullWhen(true)] out TValue? value) + { + value = TryGet(key); + return value is not null; + } + + /// + /// Determines whether the database contains the specified entry. + /// + /// The key of the entry. + /// if the database contains an entry with the specified key; otherwise, . + bool Contains(TKey key); + + /// + /// Finds the entries starting with the specified prefix. + /// + /// The prefix of the key. + /// The search direction. + /// The entries found with the desired prefix. + public IEnumerable<(TKey Key, TValue Value)> Find(TKey? keyPrefix = null, SeekDirection direction = SeekDirection.Forward); +} diff --git a/src/Neo/Persistence/IStore.cs b/src/Neo/Persistence/IStore.cs new file mode 100644 index 0000000000..bf6e1a5d40 --- /dev/null +++ b/src/Neo/Persistence/IStore.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IStore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Persistence; + +/// +/// This interface provides methods for reading, writing from/to database. Developers should implement this interface to provide new storage engines for NEO. +/// +public interface IStore : + IReadOnlyStore, + IWriteStore, + IDisposable +{ + /// + /// Delegate for OnNewSnapshot + /// + /// Store + /// Snapshot + public delegate void OnNewSnapshotDelegate(IStore sender, IStoreSnapshot snapshot); + + /// + /// Event raised when a new snapshot is created + /// + public event OnNewSnapshotDelegate? OnNewSnapshot; + + /// + /// Creates a snapshot of the database. + /// + /// A snapshot of the database. + IStoreSnapshot GetSnapshot(); +} diff --git a/src/Neo/Persistence/IStoreProvider.cs b/src/Neo/Persistence/IStoreProvider.cs new file mode 100644 index 0000000000..128e99e33d --- /dev/null +++ b/src/Neo/Persistence/IStoreProvider.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IStoreProvider.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Persistence; + +/// +/// A provider used to create instances. +/// +public interface IStoreProvider +{ + /// + /// Gets the name of the . + /// + string Name { get; } + + /// + /// Creates a new instance of the interface. + /// + /// The path of the database. + /// The created instance. + IStore GetStore(string? path); +} diff --git a/src/Neo/Persistence/IStoreSnapshot.cs b/src/Neo/Persistence/IStoreSnapshot.cs new file mode 100644 index 0000000000..1c2521c0e5 --- /dev/null +++ b/src/Neo/Persistence/IStoreSnapshot.cs @@ -0,0 +1,31 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IStoreSnapshot.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Persistence; + +/// +/// This interface provides methods for reading, writing, and committing from/to snapshot. +/// +public interface IStoreSnapshot : + IReadOnlyStore, + IWriteStore, + IDisposable +{ + /// + /// Store + /// + IStore Store { get; } + + /// + /// Commits all changes in the snapshot to the database. + /// + void Commit(); +} diff --git a/src/Neo/Persistence/IWriteStore.cs b/src/Neo/Persistence/IWriteStore.cs new file mode 100644 index 0000000000..28a5b63964 --- /dev/null +++ b/src/Neo/Persistence/IWriteStore.cs @@ -0,0 +1,38 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IWriteStore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Persistence; + +/// +/// This interface provides methods to read from the database. +/// +public interface IWriteStore +{ + /// + /// Deletes an entry from the snapshot. + /// + /// The key of the entry. + void Delete(TKey key); + + /// + /// Puts an entry to the snapshot. + /// + /// The key of the entry. + /// The data of the entry. + void Put(TKey key, TValue value); + + /// + /// Puts an entry to the database synchronously. + /// + /// The key of the entry. + /// The data of the entry. + void PutSync(TKey key, TValue value) => Put(key, value); +} diff --git a/src/Neo/Persistence/Providers/MemorySnapshot.cs b/src/Neo/Persistence/Providers/MemorySnapshot.cs new file mode 100644 index 0000000000..19a3b48352 --- /dev/null +++ b/src/Neo/Persistence/Providers/MemorySnapshot.cs @@ -0,0 +1,93 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MemorySnapshot.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Concurrent; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Persistence.Providers; + +/// +/// On-chain write operations on a snapshot cannot be concurrent. +/// +internal class MemorySnapshot : IStoreSnapshot +{ + private readonly ConcurrentDictionary _innerData; + private readonly ImmutableDictionary _immutableData; + private readonly ConcurrentDictionary _writeBatch; + + public IStore Store { get; } + + internal int WriteBatchLength => _writeBatch.Count; + + internal MemorySnapshot(MemoryStore store, ConcurrentDictionary innerData) + { + Store = store; + _innerData = innerData; + _immutableData = innerData.ToImmutableDictionary(ByteArrayEqualityComparer.Default); + _writeBatch = new ConcurrentDictionary(ByteArrayEqualityComparer.Default); + } + + public void Commit() + { + foreach (var pair in _writeBatch) + if (pair.Value is null) + _innerData.TryRemove(pair.Key, out _); + else + _innerData[pair.Key] = pair.Value; + + _writeBatch.Clear(); + } + + public void Delete(byte[] key) + { + _writeBatch[key] = null; + } + + public void Dispose() { } + + public void Put(byte[] key, byte[] value) + { + _writeBatch[key[..]] = value[..]; + } + + /// + public IEnumerable<(byte[] Key, byte[] Value)> Find(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward) + { + keyOrPrefix ??= []; + if (direction == SeekDirection.Backward && keyOrPrefix.Length == 0) yield break; + + var comparer = direction == SeekDirection.Forward ? ByteArrayComparer.Default : ByteArrayComparer.Reverse; + IEnumerable> records = _immutableData; + if (keyOrPrefix.Length > 0) + records = records + .Where(p => comparer.Compare(p.Key, keyOrPrefix) >= 0); + records = records.OrderBy(p => p.Key, comparer); + foreach (var pair in records) + yield return (pair.Key[..], pair.Value[..]); + } + + public byte[]? TryGet(byte[] key) + { + _immutableData.TryGetValue(key, out var value); + return value?[..]; + } + + public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value) + { + return _immutableData.TryGetValue(key, out value); + } + + public bool Contains(byte[] key) + { + return _immutableData.ContainsKey(key); + } +} diff --git a/src/Neo/Persistence/Providers/MemoryStore.cs b/src/Neo/Persistence/Providers/MemoryStore.cs new file mode 100644 index 0000000000..0b14991c69 --- /dev/null +++ b/src/Neo/Persistence/Providers/MemoryStore.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MemoryStore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Neo.Persistence.Providers; + +/// +/// An in-memory implementation that uses ConcurrentDictionary as the underlying storage. +/// +public sealed class MemoryStore : IStore +{ + private readonly ConcurrentDictionary _innerData = new(ByteArrayEqualityComparer.Default); + + /// + public event IStore.OnNewSnapshotDelegate? OnNewSnapshot; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Delete(byte[] key) + { + _innerData.TryRemove(key, out _); + } + + public void Dispose() { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IStoreSnapshot GetSnapshot() + { + var snapshot = new MemorySnapshot(this, _innerData); + OnNewSnapshot?.Invoke(this, snapshot); + return snapshot; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Put(byte[] key, byte[] value) + { + _innerData[key[..]] = value[..]; + } + + /// + public IEnumerable<(byte[] Key, byte[] Value)> Find(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward) + { + keyOrPrefix ??= []; + if (direction == SeekDirection.Backward && keyOrPrefix.Length == 0) yield break; + + var comparer = direction == SeekDirection.Forward ? ByteArrayComparer.Default : ByteArrayComparer.Reverse; + IEnumerable> records = _innerData; + if (keyOrPrefix.Length > 0) + records = records + .Where(p => comparer.Compare(p.Key, keyOrPrefix) >= 0); + records = records.OrderBy(p => p.Key, comparer); + foreach (var pair in records) + yield return (pair.Key[..], pair.Value[..]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte[]? TryGet(byte[] key) + { + if (!_innerData.TryGetValue(key, out var value)) return null; + return value[..]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value) + { + return _innerData.TryGetValue(key, out value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(byte[] key) + { + return _innerData.ContainsKey(key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Reset() + { + _innerData.Clear(); + } +} diff --git a/src/Neo/Persistence/Providers/MemoryStoreProvider.cs b/src/Neo/Persistence/Providers/MemoryStoreProvider.cs new file mode 100644 index 0000000000..6229b79069 --- /dev/null +++ b/src/Neo/Persistence/Providers/MemoryStoreProvider.cs @@ -0,0 +1,18 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MemoryStoreProvider.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Persistence.Providers; + +public class MemoryStoreProvider : IStoreProvider +{ + public string Name => nameof(MemoryStore); + public IStore GetStore(string? path) => new MemoryStore(); +} diff --git a/src/Neo/Persistence/SeekDirection.cs b/src/Neo/Persistence/SeekDirection.cs new file mode 100644 index 0000000000..71151d35a4 --- /dev/null +++ b/src/Neo/Persistence/SeekDirection.cs @@ -0,0 +1,28 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// SeekDirection.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Persistence; + +/// +/// Represents the direction when searching from the database. +/// +public enum SeekDirection : sbyte +{ + /// + /// Indicates that the search should be performed in ascending order. + /// + Forward = 1, + + /// + /// Indicates that the search should be performed in descending order. + /// + Backward = -1 +} diff --git a/src/Neo/Persistence/StoreCache.cs b/src/Neo/Persistence/StoreCache.cs new file mode 100644 index 0000000000..cd624cd053 --- /dev/null +++ b/src/Neo/Persistence/StoreCache.cs @@ -0,0 +1,102 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StoreCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.SmartContract; + +namespace Neo.Persistence; + +/// +/// Represents a cache for the snapshot or database of the NEO blockchain. +/// +public sealed class StoreCache : DataCache, IDisposable +{ + private readonly IReadOnlyStore _store; + private readonly IStoreSnapshot? _snapshot; + + /// + /// Initializes a new instance of the class. + /// + /// An to create a readonly cache. + /// True if you don't want to track write changes + public StoreCache(IStore store, bool readOnly = true) : base(readOnly) + { + _store = store; + } + + /// + /// Initializes a new instance of the class. + /// + /// An to create a snapshot cache. + public StoreCache(IStoreSnapshot snapshot) : base(false) + { + _store = snapshot; + _snapshot = snapshot; + } + + #region IStoreSnapshot + + protected override void UpdateInternal(StorageKey key, StorageItem value) + { + _snapshot?.Put(key.ToArray(), value.ToArray()); + } + + protected override void AddInternal(StorageKey key, StorageItem value) + { + _snapshot?.Put(key.ToArray(), value.ToArray()); + } + + protected override void DeleteInternal(StorageKey key) + { + _snapshot?.Delete(key.ToArray()); + } + + public override void Commit() + { + base.Commit(); + _snapshot?.Commit(); + } + + public void Dispose() + { + _snapshot?.Dispose(); + } + + #endregion + + #region IReadOnlyStore + + protected override bool ContainsInternal(StorageKey key) + { + return _store.Contains(key.ToArray()); + } + + /// + protected override StorageItem GetInternal(StorageKey key) + { + if (_store.TryGet(key.ToArray(), out var value)) + return new(value); + throw new KeyNotFoundException(); + } + + protected override IEnumerable<(StorageKey, StorageItem)> SeekInternal(byte[] keyOrPrefix, SeekDirection direction) + { + return _store.Find(keyOrPrefix, direction).Select(p => (new StorageKey(p.Key, false), new StorageItem(p.Value))); + } + + /// + protected override StorageItem? TryGetInternal(StorageKey key) + { + return _store.TryGet(key.ToArray(), out var value) ? new(value) : null; + } + + #endregion +} diff --git a/src/Neo/Persistence/StoreFactory.cs b/src/Neo/Persistence/StoreFactory.cs new file mode 100644 index 0000000000..2d8a2fb3b1 --- /dev/null +++ b/src/Neo/Persistence/StoreFactory.cs @@ -0,0 +1,65 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StoreFactory.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence.Providers; + +namespace Neo.Persistence; + +public static class StoreFactory +{ + private static readonly Dictionary s_providers = []; + + static StoreFactory() + { + var memProvider = new MemoryStoreProvider(); + RegisterProvider(memProvider); + + // Default cases + s_providers.Add("", memProvider); + } + + public static void RegisterProvider(IStoreProvider provider) + { + s_providers.Add(provider.Name, provider); + } + + /// + /// Get store provider by name + /// + /// Name + /// Store provider + public static IStoreProvider? GetStoreProvider(string name) + { + if (s_providers.TryGetValue(name, out var provider)) + { + return provider; + } + + return null; + } + + /// + /// Get store from name + /// + /// + /// The storage engine used to create the objects. + /// If this parameter is , a default in-memory storage engine will be used. + /// + /// + /// The path of the storage. + /// If is the default in-memory storage engine, this parameter is ignored. + /// + /// The storage engine. + public static IStore GetStore(string storageProvider, string path) + { + return s_providers[storageProvider].GetStore(path); + } +} diff --git a/src/Neo/Persistence/TrackState.cs b/src/Neo/Persistence/TrackState.cs new file mode 100644 index 0000000000..8a0796e2fd --- /dev/null +++ b/src/Neo/Persistence/TrackState.cs @@ -0,0 +1,43 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TrackState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Persistence; + +/// +/// Represents the state of a cached entry. +/// +public enum TrackState : byte +{ + /// + /// Indicates that the entry has been loaded from the underlying storage, but has not been modified. + /// + None, + + /// + /// Indicates that this is a newly added record. + /// + Added, + + /// + /// Indicates that the entry has been loaded from the underlying storage, and has been modified. + /// + Changed, + + /// + /// Indicates that the entry should be deleted from the underlying storage when committing. + /// + Deleted, + + /// + /// Indicates that the entry was not found in the underlying storage. + /// + NotFound +} diff --git a/src/Neo/Plugins/IPluginSettings.cs b/src/Neo/Plugins/IPluginSettings.cs new file mode 100644 index 0000000000..cc4a477cd2 --- /dev/null +++ b/src/Neo/Plugins/IPluginSettings.cs @@ -0,0 +1,17 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IPluginSettings.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +public interface IPluginSettings +{ + public UnhandledExceptionPolicy ExceptionPolicy { get; } +} diff --git a/src/Neo/Plugins/Plugin.cs b/src/Neo/Plugins/Plugin.cs new file mode 100644 index 0000000000..079167e5a3 --- /dev/null +++ b/src/Neo/Plugins/Plugin.cs @@ -0,0 +1,310 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Plugin.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Reflection; +using static System.IO.Path; + +namespace Neo.Plugins; + +/// +/// Represents the base class of all plugins. Any plugin should inherit this class. +/// The plugins are automatically loaded when the process starts. +/// +public abstract class Plugin : IDisposable +{ + /// + /// A list of all loaded plugins. + /// + public static readonly List Plugins = []; + + /// + /// The directory containing the plugin folders. Files can be contained in any subdirectory. + /// + public static readonly string PluginsDirectory = + Combine(GetDirectoryName(AppContext.BaseDirectory)!, "Plugins"); + + private static readonly FileSystemWatcher? s_configWatcher; + + /// + /// Indicates the root path of the plugin. + /// + public string RootPath => Combine(PluginsDirectory, GetType().Assembly.GetName().Name!); + + /// + /// Indicates the location of the plugin configuration file. + /// + public virtual string ConfigFile => Combine(RootPath, "config.json"); + + /// + /// Indicates the name of the plugin. + /// + public virtual string Name => GetType().Name; + + /// + /// Indicates the description of the plugin. + /// + public virtual string Description => ""; + + /// + /// Indicates the location of the plugin dll file. + /// + public virtual string Path => Combine(RootPath, GetType().Assembly.ManifestModule.ScopeName); + + /// + /// Indicates the version of the plugin. + /// + public virtual Version Version => GetType().Assembly.GetName().Version ?? new Version(); + + /// + /// If the plugin should be stopped when an exception is thrown. + /// Default is StopNode. + /// + protected internal virtual UnhandledExceptionPolicy ExceptionPolicy { get; init; } = UnhandledExceptionPolicy.StopNode; + + /// + /// The plugin will be stopped if an exception is thrown. + /// But it also depends on . + /// + internal bool IsStopped { get; set; } + + static Plugin() + { + if (!Directory.Exists(PluginsDirectory)) return; + s_configWatcher = new FileSystemWatcher(PluginsDirectory) + { + EnableRaisingEvents = true, + IncludeSubdirectories = true, + NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.CreationTime | + NotifyFilters.LastWrite | NotifyFilters.Size, + }; + s_configWatcher.Changed += ConfigWatcher_Changed; + s_configWatcher.Created += ConfigWatcher_Changed; + s_configWatcher.Renamed += ConfigWatcher_Changed; + s_configWatcher.Deleted += ConfigWatcher_Changed; + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + } + + /// + /// Initializes a new instance of the class. + /// + protected Plugin() + { + Plugins.Add(this); + Configure(); + } + + ~Plugin() + { + Dispose(false); + } + + /// + /// Called when the plugin is loaded and need to load the configure file, + /// or the configuration file has been modified and needs to be reconfigured. + /// + protected virtual void Configure() { } + + private static void ConfigWatcher_Changed(object? sender, FileSystemEventArgs e) + { + switch (GetExtension(e.Name)) + { + case ".json": + case ".dll": + Utility.Log(nameof(Plugin), LogLevel.Warning, + $"File {e.Name} is {e.ChangeType}, please restart node."); + break; + } + } + + private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args) + { + if (args.Name.Contains(".resources")) + return null; + + AssemblyName an = new(args.Name); + + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.Name) ?? + AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == an.Name); + if (assembly != null) return assembly; + + var filename = an.Name + ".dll"; + var path = filename; + if (!File.Exists(path)) path = Combine(GetDirectoryName(AppContext.BaseDirectory)!, filename); + if (!File.Exists(path)) path = Combine(PluginsDirectory, filename); + if (!File.Exists(path) && !string.IsNullOrEmpty(args.RequestingAssembly?.GetName().Name)) + path = Combine(PluginsDirectory, args.RequestingAssembly!.GetName().Name!, filename); + if (!File.Exists(path)) return null; + + try + { + return Assembly.Load(File.ReadAllBytes(path)); + } + catch (Exception ex) + { + Utility.Log(nameof(Plugin), LogLevel.Error, ex); + return null; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) { } + + /// + /// Loads the configuration file from the path of . + /// + /// The content of the configuration file read. + protected IConfigurationSection GetConfiguration() + { + return new ConfigurationBuilder().AddJsonFile(ConfigFile, optional: true).Build() + .GetSection("PluginConfiguration"); + } + + private static void LoadPlugin(Assembly assembly) + { + Type[] exportedTypes; + + var assemblyName = assembly.GetName().Name; + + try + { + exportedTypes = (Type[])assembly.ExportedTypes; + } + catch (Exception ex) + { + Utility.Log(nameof(Plugin), LogLevel.Error, $"Failed to load plugin assembly {assemblyName}: {ex}"); + throw; + } + + foreach (var type in exportedTypes) + { + if (!type.IsSubclassOf(typeof(Plugin))) continue; + if (type.IsAbstract) continue; + + var constructor = type.GetConstructor(Type.EmptyTypes); + if (constructor == null) continue; + + try + { + constructor.Invoke(null); + } + catch (Exception ex) + { + Utility.Log(nameof(Plugin), LogLevel.Error, $"Failed to initialize plugin type {type.FullName} of {assemblyName}: {ex}"); + } + } + } + + internal static void LoadPlugins() + { + if (!Directory.Exists(PluginsDirectory)) return; + List assemblies = []; + foreach (var rootPath in Directory.GetDirectories(PluginsDirectory)) + { + foreach (var filename in Directory.EnumerateFiles(rootPath, "*.dll", SearchOption.TopDirectoryOnly)) + { + try + { + assemblies.Add(Assembly.Load(File.ReadAllBytes(filename))); + } + catch (Exception ex) + { + Utility.Log(nameof(Plugin), LogLevel.Error, $"Failed to load plugin assembly file {filename}: {ex}"); + } + } + } + + foreach (var assembly in assemblies) + { + LoadPlugin(assembly); + } + } + + /// + /// Write a log for the plugin. + /// + /// The message of the log. + /// The level of the log. + protected void Log(object message, LogLevel level = LogLevel.Info) + { + Utility.Log($"{nameof(Plugin)}:{Name}", level, message); + } + + /// + /// Called when a message to the plugins is received. The message is sent by calling . + /// + /// The received message. + /// if the has been handled; otherwise, . + /// If a message has been handled by a plugin, the other plugins won't receive it anymore. + protected virtual bool OnMessage(object message) + { + return false; + } + + /// + /// Called when a is loaded. + /// + /// The loaded . + protected internal virtual void OnSystemLoaded(NeoSystem system) { } + + /// + /// Sends a message to all plugins. It can be handled by . + /// + /// The message to send. + /// if the is handled by a plugin; otherwise, . + public static bool SendMessage(object message) + { + foreach (var plugin in Plugins) + { + if (plugin.IsStopped) + { + continue; + } + + bool result; + try + { + result = plugin.OnMessage(message); + } + catch (Exception ex) + { + Utility.Log(nameof(Plugin), LogLevel.Error, ex); + + switch (plugin.ExceptionPolicy) + { + case UnhandledExceptionPolicy.StopNode: + throw; + case UnhandledExceptionPolicy.StopPlugin: + plugin.IsStopped = true; + break; + case UnhandledExceptionPolicy.Ignore: + break; + default: + throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); + } + + continue; // Skip to the next plugin if an exception is handled + } + + if (result) + { + return true; + } + } + + return false; + } +} diff --git a/src/Neo/Plugins/UnhandledExceptionPolicy.cs b/src/Neo/Plugins/UnhandledExceptionPolicy.cs new file mode 100644 index 0000000000..e3a5e7b807 --- /dev/null +++ b/src/Neo/Plugins/UnhandledExceptionPolicy.cs @@ -0,0 +1,19 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UnhandledExceptionPolicy.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +public enum UnhandledExceptionPolicy : byte +{ + Ignore = 0, + StopPlugin = 1, + StopNode = 2, +} diff --git a/src/Neo/ProtocolSettings.cs b/src/Neo/ProtocolSettings.cs new file mode 100644 index 0000000000..3f4b64df4a --- /dev/null +++ b/src/Neo/ProtocolSettings.cs @@ -0,0 +1,294 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ProtocolSettings.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Cryptography.ECC; +using Neo.Network.P2P.Payloads; +using System.Collections.Immutable; + +namespace Neo; + +/// +/// Represents the protocol settings of the NEO system. +/// +public record ProtocolSettings +{ + /// + /// The magic number of the NEO network. + /// + public uint Network { get; init; } + + /// + /// The address version of the NEO system. + /// + public byte AddressVersion { get; init; } + + /// + /// The public keys of the standby committee members. + /// + public required IReadOnlyList StandbyCommittee { get; init; } + + /// + /// The number of members of the committee in NEO system. + /// + public int CommitteeMembersCount => StandbyCommittee.Count; + + /// + /// The number of the validators in NEO system. + /// + public int ValidatorsCount { get; init; } + + /// + /// The default seed nodes list. + /// + public required string[] SeedList { get; init; } + + /// + /// Indicates the time in milliseconds between two blocks. Note that starting from + /// HF_Echidna block generation time is managed by native Policy contract, hence + /// use NeoSystemExtensions.GetTimePerBlock extension method instead of direct access + /// to this property. + /// + public uint MillisecondsPerBlock { get; init; } + + /// + /// Indicates the time between two blocks. Note that starting from HF_Echidna block + /// generation time is managed by native Policy contract, hence use + /// NeoSystemExtensions.GetTimePerBlock extension method instead of direct access + /// to this property. + /// + public TimeSpan TimePerBlock => TimeSpan.FromMilliseconds(MillisecondsPerBlock); + + /// + /// The maximum increment of the field. + /// + public uint MaxValidUntilBlockIncrement { get; init; } + + /// + /// Indicates the maximum number of transactions that can be contained in a block. + /// + public uint MaxTransactionsPerBlock { get; init; } + + /// + /// Indicates the maximum number of transactions that can be contained in the memory pool. + /// + public int MemoryPoolMaxTransactions { get; init; } + + /// + /// Indicates the maximum number of blocks that can be traced in the smart contract. Note + /// that starting from HF_Echidna the maximum number of traceable blocks is managed by + /// native Policy contract, hence use NeoSystemExtensions.GetMaxTraceableBlocks extension + /// method instead of direct access to this property. + /// + public uint MaxTraceableBlocks { get; init; } + + /// + /// Sets the block height from which a hardfork is activated. + /// + public required ImmutableDictionary Hardforks { get; init; } + + /// + /// Indicates the amount of gas to distribute during initialization. + /// In the unit of datoshi, 1 GAS = 1e8 datoshi + /// + public ulong InitialGasDistribution { get; init; } + + /// + /// The public keys of the standby validators. + /// + public IReadOnlyList StandbyValidators => field ??= StandbyCommittee.Take(ValidatorsCount).ToArray(); + + /// + /// The default protocol settings for NEO MainNet. + /// + public static ProtocolSettings Default { get; } = Custom ?? new ProtocolSettings + { + Network = 0u, + AddressVersion = 0x35, + StandbyCommittee = Array.Empty(), + ValidatorsCount = 0, + SeedList = Array.Empty(), + MillisecondsPerBlock = 15000, + MaxTransactionsPerBlock = 512, + MaxValidUntilBlockIncrement = 86400000 / 15000, + MemoryPoolMaxTransactions = 50_000, + MaxTraceableBlocks = 2_102_400, + InitialGasDistribution = 52_000_000_00000000, + Hardforks = EnsureOmmitedHardforks(new Dictionary()).ToImmutableDictionary() + }; + + public static ProtocolSettings? Custom { get; set; } + + /// + /// Searches for a file in the given path. If not found, checks in the executable directory. + /// + /// The name of the file to search for. + /// The primary path to search in. + /// Full path of the file if found, null otherwise. + public static string? FindFile(string fileName, string path) + { + // Check if the given path is relative + if (!Path.IsPathRooted(path)) + { + // Combine with the executable directory if relative + var executablePath = AppContext.BaseDirectory; + path = Path.Combine(executablePath, path); + } + + // Check if file exists in the specified (resolved) path + var fullPath = Path.Combine(path, fileName); + if (File.Exists(fullPath)) + { + return fullPath; + } + + // Check if file exists in the executable directory + var executableDir = AppContext.BaseDirectory; + fullPath = Path.Combine(executableDir, fileName); + if (File.Exists(fullPath)) + { + return fullPath; + } + + // File not found in either location + return null; + } + + /// + /// Loads the from the specified stream. + /// + /// The stream of the settings. + /// The loaded . + public static ProtocolSettings Load(Stream stream) + { + var config = new ConfigurationBuilder().AddJsonStream(stream).Build(); + var section = config.GetSection("ProtocolConfiguration"); + return Load(section); + } + + /// + /// Loads the at the specified path. + /// + /// The path of the settings file. + /// The loaded . + public static ProtocolSettings Load(string path) + { + string? fullpath = FindFile(path, Environment.CurrentDirectory); + + if (fullpath is null) + { + return Default; + } + + using var stream = File.OpenRead(fullpath); + return Load(stream); + } + + /// + /// Loads the with the specified . + /// + /// The to be loaded. + /// The loaded . + public static ProtocolSettings Load(IConfigurationSection section) + { + Custom = new ProtocolSettings + { + Network = section.GetValue("Network", Default.Network), + AddressVersion = section.GetValue("AddressVersion", Default.AddressVersion), + StandbyCommittee = section.GetSection("StandbyCommittee").Exists() + ? section.GetSection("StandbyCommittee").GetChildren().Select(p => ECPoint.Parse(p.Get()!, ECCurve.Secp256r1)).ToArray() + : Default.StandbyCommittee, + ValidatorsCount = section.GetValue("ValidatorsCount", Default.ValidatorsCount), + SeedList = section.GetSection("SeedList").Exists() + ? section.GetSection("SeedList").GetChildren().Select(p => p.Get()!).ToArray() + : Default.SeedList, + MillisecondsPerBlock = section.GetValue("MillisecondsPerBlock", Default.MillisecondsPerBlock), + MaxTransactionsPerBlock = section.GetValue("MaxTransactionsPerBlock", Default.MaxTransactionsPerBlock), + MemoryPoolMaxTransactions = section.GetValue("MemoryPoolMaxTransactions", Default.MemoryPoolMaxTransactions), + MaxTraceableBlocks = section.GetValue("MaxTraceableBlocks", Default.MaxTraceableBlocks), + MaxValidUntilBlockIncrement = section.GetValue("MaxValidUntilBlockIncrement", Default.MaxValidUntilBlockIncrement), + InitialGasDistribution = section.GetValue("InitialGasDistribution", Default.InitialGasDistribution), + Hardforks = section.GetSection("Hardforks").Exists() + ? EnsureOmmitedHardforks(section.GetSection("Hardforks").GetChildren().ToDictionary(p => Enum.Parse(p.Key, true), p => uint.Parse(p.Value!))).ToImmutableDictionary() + : Default.Hardforks + }; + CheckingHardfork(Custom); + return Custom; + } + + /// + /// Explicitly set the height of all old omitted hardforks to 0 for proper IsHardforkEnabled behaviour. + /// + /// HardForks + /// Processed hardfork configuration + private static Dictionary EnsureOmmitedHardforks(Dictionary hardForks) + { + foreach (Hardfork hf in Enum.GetValues()) + { + if (!hardForks.ContainsKey(hf)) + { + hardForks[hf] = 0; + } + else + { + break; + } + } + + return hardForks; + } + + private static void CheckingHardfork(ProtocolSettings settings) + { + var allHardforks = Enum.GetValues().Cast().ToList(); + // Check for continuity in configured hardforks + var sortedHardforks = settings.Hardforks.Keys + .OrderBy(allHardforks.IndexOf) + .ToList(); + + for (int i = 0; i < sortedHardforks.Count - 1; i++) + { + int currentIndex = allHardforks.IndexOf(sortedHardforks[i]); + int nextIndex = allHardforks.IndexOf(sortedHardforks[i + 1]); + + // If they aren't consecutive, return false. + if (nextIndex - currentIndex > 1) + throw new ArgumentException($"Hardfork configuration is not continuous. There is a gap between {sortedHardforks[i]} and {sortedHardforks[i + 1]}. All hardforks must be configured in sequential order without gaps."); + } + // Check that block numbers are not higher in earlier hardforks than in later ones + for (int i = 0; i < sortedHardforks.Count - 1; i++) + { + if (settings.Hardforks[sortedHardforks[i]] > settings.Hardforks[sortedHardforks[i + 1]]) + { + // This means the block number for the current hardfork is greater than the next one, which should not be allowed. + throw new ArgumentException($"Invalid hardfork configuration: {sortedHardforks[i]} is configured to activate at block {settings.Hardforks[sortedHardforks[i]]}, which is greater than {sortedHardforks[i + 1]} at block {settings.Hardforks[sortedHardforks[i + 1]]}. Earlier hardforks must activate at lower block numbers than later hardforks."); + } + } + } + + /// + /// Check if the Hardfork is Enabled + /// + /// Hardfork + /// Block index + /// True if enabled + public bool IsHardforkEnabled(Hardfork hardfork, uint index) + { + if (Hardforks.TryGetValue(hardfork, out uint height)) + { + // If the hardfork has a specific height in the configuration, check the block height. + return index >= height; + } + + // If the hardfork isn't specified in the configuration, return false. + return false; + } +} diff --git a/src/Neo/Sign/ISigner.cs b/src/Neo/Sign/ISigner.cs new file mode 100644 index 0000000000..7dcd5ca64e --- /dev/null +++ b/src/Neo/Sign/ISigner.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ISigner.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Persistence; + +namespace Neo.Sign; + +/// +/// Represents a signer that can sign messages. +/// +public interface ISigner +{ + /// + /// Signs the with the wallet. + /// + /// The to be used. + /// The snapshot. + /// The network. + /// The witness. + /// Thrown when the payload is null. + Witness SignExtensiblePayload(ExtensiblePayload payload, DataCache snapshot, uint network); + + /// + /// Signs the specified data with the corresponding private key of the specified public key. + /// + /// The block to sign. + /// The public key. + /// The network. + /// The signature. + /// Thrown when the block or public key is null. + /// + /// Thrown when the account is not found or not signable, or the network is not matching. + /// + ReadOnlyMemory SignBlock(Block block, ECPoint publicKey, uint network); + + /// + /// Checks if the wallet contains an account(has private key and is not locked) with the specified public key. + /// If the wallet has the public key but not the private key or the account is locked, it will return false. + /// + /// The public key. + /// + /// if the wallet contains the specified public key and the corresponding unlocked private key; + /// otherwise, . + /// + bool ContainsSignable(ECPoint publicKey); +} diff --git a/src/Neo/Sign/SignException.cs b/src/Neo/Sign/SignException.cs new file mode 100644 index 0000000000..fa718ac8a3 --- /dev/null +++ b/src/Neo/Sign/SignException.cs @@ -0,0 +1,25 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// SignException.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Sign; + +/// +/// The exception that is thrown when `Sign` fails. +/// +public class SignException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The cause of the exception. + public SignException(string message, Exception? cause = null) : base(message, cause) { } +} diff --git a/src/Neo/Sign/SignerManager.cs b/src/Neo/Sign/SignerManager.cs new file mode 100644 index 0000000000..d531fb2e07 --- /dev/null +++ b/src/Neo/Sign/SignerManager.cs @@ -0,0 +1,62 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// SignerManager.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Concurrent; + +namespace Neo.Sign; + +public static class SignerManager +{ + private static readonly ConcurrentDictionary s_signers = new(); + + /// + /// Get a signer by name. If only one signer is registered, it will return the only one signer. + /// + /// The name of the signer + /// + /// The signer; if not found or no signer or multiple signers are registered. + /// + public static ISigner? GetSignerOrDefault(string name) + { + if (!string.IsNullOrEmpty(name) && s_signers.TryGetValue(name, out var signer)) return signer; + if (s_signers.Count == 1) return s_signers.Values.First(); + return null; + } + + /// + /// Register a signer, and it only can be called before the node starts. + /// + /// The name of the signer + /// The signer to register + /// Thrown when is null or empty + /// Thrown when is null + /// Thrown when is already registered + public static void RegisterSigner(string name, ISigner signer) + { + if (string.IsNullOrEmpty(name)) throw new ArgumentException("Name cannot be null or empty", nameof(name)); + ArgumentNullException.ThrowIfNull(signer); + + if (!s_signers.TryAdd(name, signer)) throw new InvalidOperationException($"Signer {name} already exists"); + } + + /// + /// Unregister a signer, and it only can be called before the node starts. + /// + /// The name of the signer + /// + /// if the signer is unregistered; otherwise, . + /// + public static bool UnregisterSigner(string name) + { + if (string.IsNullOrEmpty(name)) return false; + return s_signers.TryRemove(name, out _); + } +} diff --git a/src/Neo/SmartContract/ApplicationEngine.Contract.cs b/src/Neo/SmartContract/ApplicationEngine.Contract.cs new file mode 100644 index 0000000000..c0e039fec8 --- /dev/null +++ b/src/Neo/SmartContract/ApplicationEngine.Contract.cs @@ -0,0 +1,180 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ApplicationEngine.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. + +using Neo.Cryptography.ECC; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract; + +partial class ApplicationEngine +{ + /// + /// The of System.Contract.Call. + /// Use it to call another contract dynamically. + /// + public static readonly InteropDescriptor System_Contract_Call = Register("System.Contract.Call", nameof(CallContract), 1 << 15, CallFlags.ReadStates | CallFlags.AllowCall); + + /// + /// The of System.Contract.CallNative. + /// + /// Note: It is for internal use only. Do not use it directly in smart contracts. + public static readonly InteropDescriptor System_Contract_CallNative = Register("System.Contract.CallNative", nameof(CallNativeContract), 0, CallFlags.None); + + /// + /// The of System.Contract.GetCallFlags. + /// Gets the of the current context. + /// + public static readonly InteropDescriptor System_Contract_GetCallFlags = Register("System.Contract.GetCallFlags", nameof(GetCallFlags), 1 << 10, CallFlags.None); + + /// + /// The of System.Contract.CreateStandardAccount. + /// Calculates corresponding account scripthash for the given public key. + /// + public static readonly InteropDescriptor System_Contract_CreateStandardAccount = Register("System.Contract.CreateStandardAccount", nameof(CreateStandardAccount), 0, CallFlags.None); + + /// + /// The of System.Contract.CreateMultisigAccount. + /// Calculates corresponding multisig account scripthash for the given public keys. + /// + public static readonly InteropDescriptor System_Contract_CreateMultisigAccount = Register("System.Contract.CreateMultisigAccount", nameof(CreateMultisigAccount), 0, CallFlags.None); + + /// + /// The of System.Contract.NativeOnPersist. + /// + /// Note: It is for internal use only. Do not use it directly in smart contracts. + public static readonly InteropDescriptor System_Contract_NativeOnPersist = Register("System.Contract.NativeOnPersist", nameof(NativeOnPersistAsync), 0, CallFlags.States); + + /// + /// The of System.Contract.NativePostPersist. + /// + /// Note: It is for internal use only. Do not use it directly in smart contracts. + public static readonly InteropDescriptor System_Contract_NativePostPersist = Register("System.Contract.NativePostPersist", nameof(NativePostPersistAsync), 0, CallFlags.States); + + /// + /// The implementation of System.Contract.Call. + /// Use it to call another contract dynamically. + /// + /// The hash of the contract to be called. + /// The method of the contract to be called. + /// The to be used to call the contract. + /// The arguments to be used. + protected internal void CallContract(UInt160 contractHash, string method, CallFlags callFlags, Array args) + { + if (method.StartsWith('_')) throw new ArgumentException($"Method name '{method}' cannot start with underscore.", nameof(method)); + if ((callFlags & ~CallFlags.All) != 0) + throw new ArgumentOutOfRangeException(nameof(callFlags)); + + ContractState contract = NativeContract.ContractManagement.GetContract(SnapshotCache, contractHash) + ?? throw new InvalidOperationException($"Called Contract Does Not Exist: {contractHash}.{method}"); + ContractMethodDescriptor md = contract.Manifest.Abi.GetMethod(method, args.Count) + ?? throw new InvalidOperationException($"Method \"{method}\" with {args.Count} parameter(s) doesn't exist in the contract {contractHash}."); + bool hasReturnValue = md.ReturnType != ContractParameterType.Void; + + VM.ExecutionContext context = CallContractInternal(contract, md, callFlags, hasReturnValue, args); + context.GetState().IsDynamicCall = true; + } + + /// + /// The implementation of System.Contract.CallNative. + /// Calls to a native contract. + /// + /// The version of the native contract to be called. + protected internal void CallNativeContract(byte version) + { + NativeContract contract = NativeContract.GetContract(CurrentScriptHash!) + ?? throw new InvalidOperationException("It is not allowed to use \"System.Contract.CallNative\" directly."); + if (!contract.IsActive(ProtocolSettings, NativeContract.Ledger.CurrentIndex(SnapshotCache))) + throw new InvalidOperationException($"The native contract {contract.Name} is not active."); + contract.Invoke(this, version); + } + + /// + /// The implementation of System.Contract.GetCallFlags. + /// Gets the of the current context. + /// + /// The of the current context. + protected internal CallFlags GetCallFlags() + { + var state = CurrentContext!.GetState(); + return state.CallFlags; + } + + /// + /// The implementation of System.Contract.CreateStandardAccount. + /// Calculates corresponding account scripthash for the given public key. + /// + /// The public key of the account. + /// The hash of the account. + internal protected UInt160 CreateStandardAccount(ECPoint pubKey) + { + AddFee(CheckSigPrice * ExecFeeFactor); + return Contract.CreateSignatureRedeemScript(pubKey).ToScriptHash(); + } + + /// + /// The implementation of System.Contract.CreateMultisigAccount. + /// Calculates corresponding multisig account scripthash for the given public keys. + /// + /// The number of correct signatures that need to be provided in order for the verification to pass. + /// The public keys of the account. + /// The hash of the account. + internal protected UInt160 CreateMultisigAccount(int m, ECPoint[] pubKeys) + { + AddFee(CheckSigPrice * pubKeys.Length * ExecFeeFactor); + return Contract.CreateMultiSigRedeemScript(m, pubKeys).ToScriptHash(); + } + + /// + /// The implementation of System.Contract.NativeOnPersist. + /// Calls to the of all native contracts. + /// + protected internal async void NativeOnPersistAsync() + { + try + { + if (Trigger != TriggerType.OnPersist) + throw new InvalidOperationException(); + foreach (NativeContract contract in NativeContract.Contracts) + { + if (contract.IsActive(ProtocolSettings, PersistingBlock!.Index)) + await contract.OnPersistAsync(this); + } + } + catch (Exception ex) + { + Throw(ex); + } + } + + /// + /// The implementation of System.Contract.NativePostPersist. + /// Calls to the of all native contracts. + /// + protected internal async void NativePostPersistAsync() + { + try + { + if (Trigger != TriggerType.PostPersist) + throw new InvalidOperationException(); + foreach (NativeContract contract in NativeContract.Contracts) + { + if (contract.IsActive(ProtocolSettings, PersistingBlock!.Index)) + await contract.PostPersistAsync(this); + } + } + catch (Exception ex) + { + Throw(ex); + } + } +} diff --git a/src/Neo/SmartContract/ApplicationEngine.Crypto.cs b/src/Neo/SmartContract/ApplicationEngine.Crypto.cs new file mode 100644 index 0000000000..c1d925c108 --- /dev/null +++ b/src/Neo/SmartContract/ApplicationEngine.Crypto.cs @@ -0,0 +1,75 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ApplicationEngine.Crypto.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Network.P2P; + +namespace Neo.SmartContract; + +partial class ApplicationEngine +{ + /// + /// The price of System.Crypto.CheckSig. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS + /// + public const long CheckSigPrice = 1 << 15; + + /// + /// The of System.Crypto.CheckSig. + /// Checks the signature for the current script container. + /// + public static readonly InteropDescriptor System_Crypto_CheckSig = Register("System.Crypto.CheckSig", nameof(CheckSig), CheckSigPrice, CallFlags.None); + + /// + /// The of System.Crypto.CheckMultisig. + /// Checks the signatures for the current script container. + /// + public static readonly InteropDescriptor System_Crypto_CheckMultisig = Register("System.Crypto.CheckMultisig", nameof(CheckMultisig), 0, CallFlags.None); + + /// + /// The implementation of System.Crypto.CheckSig. + /// Checks the signature for the current script container. + /// + /// The public key of the account. + /// The signature of the current script container. + /// if the signature is valid; otherwise, . + protected internal bool CheckSig(byte[] pubkey, byte[] signature) + { + return Crypto.VerifySignature(ScriptContainer!.GetSignData(ProtocolSettings.Network), signature, pubkey, ECCurve.Secp256r1); + } + + /// + /// The implementation of System.Crypto.CheckMultisig. + /// Checks the signatures for the current script container. + /// + /// The public keys of the account. + /// The signatures of the current script container. + /// if the signatures are valid; otherwise, . + protected internal bool CheckMultisig(byte[][] pubkeys, byte[][] signatures) + { + var message = ScriptContainer!.GetSignData(ProtocolSettings.Network); + int m = signatures.Length, n = pubkeys.Length; + if (n == 0) throw new ArgumentException("pubkeys array cannot be empty."); + if (m == 0) throw new ArgumentException("signatures array cannot be empty."); + if (m > n) throw new ArgumentException($"signatures count ({m}) cannot be greater than pubkeys count ({n})."); + AddFee(CheckSigPrice * n * ExecFeeFactor); + for (int i = 0, j = 0; i < m && j < n;) + { + if (Crypto.VerifySignature(message, signatures[i], pubkeys[j], ECCurve.Secp256r1)) + i++; + j++; + if (m - i > n - j) + return false; + } + return true; + } +} diff --git a/src/Neo/SmartContract/ApplicationEngine.Helper.cs b/src/Neo/SmartContract/ApplicationEngine.Helper.cs new file mode 100644 index 0000000000..6e891314df --- /dev/null +++ b/src/Neo/SmartContract/ApplicationEngine.Helper.cs @@ -0,0 +1,53 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ApplicationEngine.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.SmartContract.Native; +using Neo.VM; +using System.Text; + +namespace Neo.SmartContract; + +public partial class ApplicationEngine : ExecutionEngine +{ + public string GetEngineStackInfoOnFault(bool exceptionStackTrace = true, bool exceptionMessage = true) + { + if (State != VMState.FAULT || FaultException == null) + return ""; + StringBuilder traceback = new(); + if (CallingScriptHash != null) + traceback.AppendLine($"CallingScriptHash={CallingScriptHash}[{NativeContract.ContractManagement.GetContract(SnapshotCache, CallingScriptHash)?.Manifest.Name}]"); + traceback.AppendLine($"CurrentScriptHash={CurrentScriptHash}[{NativeContract.ContractManagement.GetContract(SnapshotCache, CurrentScriptHash!)?.Manifest.Name}]"); + traceback.AppendLine($"EntryScriptHash={EntryScriptHash}"); + + foreach (VM.ExecutionContext context in InvocationStack.Reverse()) + { + UInt160 contextScriptHash = context.GetScriptHash(); + string? contextContractName = NativeContract.ContractManagement.GetContract(SnapshotCache, contextScriptHash)?.Manifest.Name; + traceback.AppendLine($"\tInstructionPointer={context.InstructionPointer}, OpCode {context.CurrentInstruction?.OpCode}, Script Length={context.Script.Length} {contextScriptHash}[{contextContractName}]"); + } + traceback.Append(GetEngineExceptionInfo(exceptionStackTrace: exceptionStackTrace, exceptionMessage: exceptionMessage)); + + return traceback.ToString(); + } + + public string GetEngineExceptionInfo(bool exceptionStackTrace = true, bool exceptionMessage = true) + { + if (State != VMState.FAULT || FaultException == null) + return ""; + StringBuilder traceback = new(); + Exception baseException = FaultException.GetBaseException(); + if (exceptionStackTrace) + traceback.AppendLine(baseException.StackTrace); + if (exceptionMessage) + traceback.AppendLine(baseException.Message); + return traceback.ToString(); + } +} diff --git a/src/Neo/SmartContract/ApplicationEngine.Iterator.cs b/src/Neo/SmartContract/ApplicationEngine.Iterator.cs new file mode 100644 index 0000000000..9457dfd0a6 --- /dev/null +++ b/src/Neo/SmartContract/ApplicationEngine.Iterator.cs @@ -0,0 +1,52 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ApplicationEngine.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 Neo.SmartContract.Iterators; +using Neo.VM.Types; + +namespace Neo.SmartContract; + +partial class ApplicationEngine +{ + /// + /// The of System.Iterator.Next. + /// Advances the iterator to the next element of the collection. + /// + public static readonly InteropDescriptor System_Iterator_Next = Register("System.Iterator.Next", nameof(IteratorNext), 1 << 15, CallFlags.None); + + /// + /// The of System.Iterator.Value. + /// Gets the element in the collection at the current position of the iterator. + /// + public static readonly InteropDescriptor System_Iterator_Value = Register("System.Iterator.Value", nameof(IteratorValue), 1 << 4, CallFlags.None); + + /// + /// The implementation of System.Iterator.Next. + /// Advances the iterator to the next element of the collection. + /// + /// The iterator to be advanced. + /// if the iterator was successfully advanced to the next element; if the iterator has passed the end of the collection. + internal protected static bool IteratorNext(IIterator iterator) + { + return iterator.Next(); + } + + /// + /// The implementation of System.Iterator.Value. + /// Gets the element in the collection at the current position of the iterator. + /// + /// The iterator to be used. + /// The element in the collection at the current position of the iterator. + internal protected StackItem IteratorValue(IIterator iterator) + { + return iterator.Value(ReferenceCounter); + } +} diff --git a/src/Neo/SmartContract/ApplicationEngine.OpCodePrices.cs b/src/Neo/SmartContract/ApplicationEngine.OpCodePrices.cs new file mode 100644 index 0000000000..2a99696325 --- /dev/null +++ b/src/Neo/SmartContract/ApplicationEngine.OpCodePrices.cs @@ -0,0 +1,237 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ApplicationEngine.OpCodePrices.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; + +namespace Neo.SmartContract; + +partial class ApplicationEngine +{ + /// + /// The prices of all the opcodes. + /// + private static readonly IReadOnlyDictionary s_prices = new Dictionary + { + [OpCode.PUSHINT8] = 1 << 0, + [OpCode.PUSHINT16] = 1 << 0, + [OpCode.PUSHINT32] = 1 << 0, + [OpCode.PUSHINT64] = 1 << 0, + [OpCode.PUSHINT128] = 1 << 2, + [OpCode.PUSHINT256] = 1 << 2, + [OpCode.PUSHT] = 1 << 0, + [OpCode.PUSHF] = 1 << 0, + [OpCode.PUSHA] = 1 << 2, + [OpCode.PUSHNULL] = 1 << 0, + [OpCode.PUSHDATA1] = 1 << 3, + [OpCode.PUSHDATA2] = 1 << 9, + [OpCode.PUSHDATA4] = 1 << 12, + [OpCode.PUSHM1] = 1 << 0, + [OpCode.PUSH0] = 1 << 0, + [OpCode.PUSH1] = 1 << 0, + [OpCode.PUSH2] = 1 << 0, + [OpCode.PUSH3] = 1 << 0, + [OpCode.PUSH4] = 1 << 0, + [OpCode.PUSH5] = 1 << 0, + [OpCode.PUSH6] = 1 << 0, + [OpCode.PUSH7] = 1 << 0, + [OpCode.PUSH8] = 1 << 0, + [OpCode.PUSH9] = 1 << 0, + [OpCode.PUSH10] = 1 << 0, + [OpCode.PUSH11] = 1 << 0, + [OpCode.PUSH12] = 1 << 0, + [OpCode.PUSH13] = 1 << 0, + [OpCode.PUSH14] = 1 << 0, + [OpCode.PUSH15] = 1 << 0, + [OpCode.PUSH16] = 1 << 0, + [OpCode.NOP] = 1 << 0, + [OpCode.JMP] = 1 << 1, + [OpCode.JMP_L] = 1 << 1, + [OpCode.JMPIF] = 1 << 1, + [OpCode.JMPIF_L] = 1 << 1, + [OpCode.JMPIFNOT] = 1 << 1, + [OpCode.JMPIFNOT_L] = 1 << 1, + [OpCode.JMPEQ] = 1 << 1, + [OpCode.JMPEQ_L] = 1 << 1, + [OpCode.JMPNE] = 1 << 1, + [OpCode.JMPNE_L] = 1 << 1, + [OpCode.JMPGT] = 1 << 1, + [OpCode.JMPGT_L] = 1 << 1, + [OpCode.JMPGE] = 1 << 1, + [OpCode.JMPGE_L] = 1 << 1, + [OpCode.JMPLT] = 1 << 1, + [OpCode.JMPLT_L] = 1 << 1, + [OpCode.JMPLE] = 1 << 1, + [OpCode.JMPLE_L] = 1 << 1, + [OpCode.CALL] = 1 << 9, + [OpCode.CALL_L] = 1 << 9, + [OpCode.CALLA] = 1 << 9, + [OpCode.CALLT] = 1 << 15, + [OpCode.ABORT] = 0, + [OpCode.ABORTMSG] = 0, + [OpCode.ASSERT] = 1 << 0, + [OpCode.ASSERTMSG] = 1 << 0, + [OpCode.THROW] = 1 << 9, + [OpCode.TRY] = 1 << 2, + [OpCode.TRY_L] = 1 << 2, + [OpCode.ENDTRY] = 1 << 2, + [OpCode.ENDTRY_L] = 1 << 2, + [OpCode.ENDFINALLY] = 1 << 2, + [OpCode.RET] = 0, + [OpCode.SYSCALL] = 0, + [OpCode.DEPTH] = 1 << 1, + [OpCode.DROP] = 1 << 1, + [OpCode.NIP] = 1 << 1, + [OpCode.XDROP] = 1 << 4, + [OpCode.CLEAR] = 1 << 4, + [OpCode.DUP] = 1 << 1, + [OpCode.OVER] = 1 << 1, + [OpCode.PICK] = 1 << 1, + [OpCode.TUCK] = 1 << 1, + [OpCode.SWAP] = 1 << 1, + [OpCode.ROT] = 1 << 1, + [OpCode.ROLL] = 1 << 4, + [OpCode.REVERSE3] = 1 << 1, + [OpCode.REVERSE4] = 1 << 1, + [OpCode.REVERSEN] = 1 << 4, + [OpCode.INITSSLOT] = 1 << 4, + [OpCode.INITSLOT] = 1 << 6, + [OpCode.LDSFLD0] = 1 << 1, + [OpCode.LDSFLD1] = 1 << 1, + [OpCode.LDSFLD2] = 1 << 1, + [OpCode.LDSFLD3] = 1 << 1, + [OpCode.LDSFLD4] = 1 << 1, + [OpCode.LDSFLD5] = 1 << 1, + [OpCode.LDSFLD6] = 1 << 1, + [OpCode.LDSFLD] = 1 << 1, + [OpCode.STSFLD0] = 1 << 1, + [OpCode.STSFLD1] = 1 << 1, + [OpCode.STSFLD2] = 1 << 1, + [OpCode.STSFLD3] = 1 << 1, + [OpCode.STSFLD4] = 1 << 1, + [OpCode.STSFLD5] = 1 << 1, + [OpCode.STSFLD6] = 1 << 1, + [OpCode.STSFLD] = 1 << 1, + [OpCode.LDLOC0] = 1 << 1, + [OpCode.LDLOC1] = 1 << 1, + [OpCode.LDLOC2] = 1 << 1, + [OpCode.LDLOC3] = 1 << 1, + [OpCode.LDLOC4] = 1 << 1, + [OpCode.LDLOC5] = 1 << 1, + [OpCode.LDLOC6] = 1 << 1, + [OpCode.LDLOC] = 1 << 1, + [OpCode.STLOC0] = 1 << 1, + [OpCode.STLOC1] = 1 << 1, + [OpCode.STLOC2] = 1 << 1, + [OpCode.STLOC3] = 1 << 1, + [OpCode.STLOC4] = 1 << 1, + [OpCode.STLOC5] = 1 << 1, + [OpCode.STLOC6] = 1 << 1, + [OpCode.STLOC] = 1 << 1, + [OpCode.LDARG0] = 1 << 1, + [OpCode.LDARG1] = 1 << 1, + [OpCode.LDARG2] = 1 << 1, + [OpCode.LDARG3] = 1 << 1, + [OpCode.LDARG4] = 1 << 1, + [OpCode.LDARG5] = 1 << 1, + [OpCode.LDARG6] = 1 << 1, + [OpCode.LDARG] = 1 << 1, + [OpCode.STARG0] = 1 << 1, + [OpCode.STARG1] = 1 << 1, + [OpCode.STARG2] = 1 << 1, + [OpCode.STARG3] = 1 << 1, + [OpCode.STARG4] = 1 << 1, + [OpCode.STARG5] = 1 << 1, + [OpCode.STARG6] = 1 << 1, + [OpCode.STARG] = 1 << 1, + [OpCode.NEWBUFFER] = 1 << 8, + [OpCode.MEMCPY] = 1 << 11, + [OpCode.CAT] = 1 << 11, + [OpCode.SUBSTR] = 1 << 11, + [OpCode.LEFT] = 1 << 11, + [OpCode.RIGHT] = 1 << 11, + [OpCode.INVERT] = 1 << 2, + [OpCode.AND] = 1 << 3, + [OpCode.OR] = 1 << 3, + [OpCode.XOR] = 1 << 3, + [OpCode.EQUAL] = 1 << 5, + [OpCode.NOTEQUAL] = 1 << 5, + [OpCode.SIGN] = 1 << 2, + [OpCode.ABS] = 1 << 2, + [OpCode.NEGATE] = 1 << 2, + [OpCode.INC] = 1 << 2, + [OpCode.DEC] = 1 << 2, + [OpCode.ADD] = 1 << 3, + [OpCode.SUB] = 1 << 3, + [OpCode.MUL] = 1 << 3, + [OpCode.DIV] = 1 << 3, + [OpCode.MOD] = 1 << 3, + [OpCode.POW] = 1 << 6, + [OpCode.SQRT] = 1 << 6, + [OpCode.MODMUL] = 1 << 5, + [OpCode.MODPOW] = 1 << 11, + [OpCode.SHL] = 1 << 3, + [OpCode.SHR] = 1 << 3, + [OpCode.NOT] = 1 << 2, + [OpCode.BOOLAND] = 1 << 3, + [OpCode.BOOLOR] = 1 << 3, + [OpCode.NZ] = 1 << 2, + [OpCode.NUMEQUAL] = 1 << 3, + [OpCode.NUMNOTEQUAL] = 1 << 3, + [OpCode.LT] = 1 << 3, + [OpCode.LE] = 1 << 3, + [OpCode.GT] = 1 << 3, + [OpCode.GE] = 1 << 3, + [OpCode.MIN] = 1 << 3, + [OpCode.MAX] = 1 << 3, + [OpCode.WITHIN] = 1 << 3, + [OpCode.PACKMAP] = 1 << 11, + [OpCode.PACKSTRUCT] = 1 << 11, + [OpCode.PACK] = 1 << 11, + [OpCode.UNPACK] = 1 << 11, + [OpCode.NEWARRAY0] = 1 << 4, + [OpCode.NEWARRAY] = 1 << 9, + [OpCode.NEWARRAY_T] = 1 << 9, + [OpCode.NEWSTRUCT0] = 1 << 4, + [OpCode.NEWSTRUCT] = 1 << 9, + [OpCode.NEWMAP] = 1 << 3, + [OpCode.SIZE] = 1 << 2, + [OpCode.HASKEY] = 1 << 6, + [OpCode.KEYS] = 1 << 4, + [OpCode.VALUES] = 1 << 13, + [OpCode.PICKITEM] = 1 << 6, + [OpCode.APPEND] = 1 << 13, + [OpCode.SETITEM] = 1 << 13, + [OpCode.REVERSEITEMS] = 1 << 13, + [OpCode.REMOVE] = 1 << 4, + [OpCode.CLEARITEMS] = 1 << 4, + [OpCode.POPITEM] = 1 << 4, + [OpCode.ISNULL] = 1 << 1, + [OpCode.ISTYPE] = 1 << 1, + [OpCode.CONVERT] = 1 << 13, + }; + + /// + /// The prices of all the opcodes. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS + /// + public static readonly long[] OpCodePriceTable = new long[byte.MaxValue]; + + /// + /// Init OpCodePrices + /// + static ApplicationEngine() + { + foreach (var (opcode, price) in s_prices) + { + OpCodePriceTable[(byte)opcode] = price; + } + } +} diff --git a/src/Neo/SmartContract/ApplicationEngine.Runtime.cs b/src/Neo/SmartContract/ApplicationEngine.Runtime.cs new file mode 100644 index 0000000000..89d11cdd67 --- /dev/null +++ b/src/Neo/SmartContract/ApplicationEngine.Runtime.cs @@ -0,0 +1,484 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ApplicationEngine.Runtime.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract; + +partial class ApplicationEngine +{ + /// + /// The maximum length of event name. + /// + public const int MaxEventName = 32; + + /// + /// The maximum size of notification objects. + /// + public const int MaxNotificationSize = 1024; + + /// + /// The maximum number of notifications per application execution. + /// + public const int MaxNotificationCount = 512; + + private uint randomTimes = 0; + + /// + /// The of System.Runtime.Platform. + /// Gets the name of the current platform. + /// + public static readonly InteropDescriptor System_Runtime_Platform = Register("System.Runtime.Platform", nameof(GetPlatform), 1 << 3, CallFlags.None); + + /// + /// The of System.Runtime.GetNetwork. + /// Gets the magic number of the current network. + /// + public static readonly InteropDescriptor System_Runtime_GetNetwork = Register("System.Runtime.GetNetwork", nameof(GetNetwork), 1 << 3, CallFlags.None); + + /// + /// The of System.Runtime.GetAddressVersion. + /// Gets the address version of the current network. + /// + public static readonly InteropDescriptor System_Runtime_GetAddressVersion = Register("System.Runtime.GetAddressVersion", nameof(GetAddressVersion), 1 << 3, CallFlags.None); + + /// + /// The of System.Runtime.GetTrigger. + /// Gets the trigger of the execution. + /// + public static readonly InteropDescriptor System_Runtime_GetTrigger = Register("System.Runtime.GetTrigger", nameof(Trigger), 1 << 3, CallFlags.None); + + /// + /// The of System.Runtime.GetTime. + /// Gets the timestamp of the current block. + /// + public static readonly InteropDescriptor System_Runtime_GetTime = Register("System.Runtime.GetTime", nameof(GetTime), 1 << 3, CallFlags.None); + + /// + /// The of System.Runtime.GetScriptContainer. + /// Gets the current script container. + /// + public static readonly InteropDescriptor System_Runtime_GetScriptContainer = Register("System.Runtime.GetScriptContainer", nameof(GetScriptContainer), 1 << 3, CallFlags.None); + + /// + /// The of System.Runtime.GetExecutingScriptHash. + /// Gets the script hash of the current context. + /// + public static readonly InteropDescriptor System_Runtime_GetExecutingScriptHash = Register("System.Runtime.GetExecutingScriptHash", nameof(CurrentScriptHash), 1 << 4, CallFlags.None); + + /// + /// The of System.Runtime.GetCallingScriptHash. + /// Gets the script hash of the calling contract. + /// + public static readonly InteropDescriptor System_Runtime_GetCallingScriptHash = Register("System.Runtime.GetCallingScriptHash", nameof(CallingScriptHash), 1 << 4, CallFlags.None); + + /// + /// The of System.Runtime.GetEntryScriptHash. + /// Gets the script hash of the entry context. + /// + public static readonly InteropDescriptor System_Runtime_GetEntryScriptHash = Register("System.Runtime.GetEntryScriptHash", nameof(EntryScriptHash), 1 << 4, CallFlags.None); + + /// + /// The of System.Runtime.LoadScript. + /// Loads a script at rumtime. + /// + public static readonly InteropDescriptor System_Runtime_LoadScript = Register("System.Runtime.LoadScript", nameof(RuntimeLoadScript), 1 << 15, CallFlags.AllowCall); + + /// + /// The of System.Runtime.CheckWitness. + /// Determines whether the specified account has witnessed the current transaction. + /// + public static readonly InteropDescriptor System_Runtime_CheckWitness = Register("System.Runtime.CheckWitness", nameof(CheckWitness), 1 << 10, CallFlags.None); + + /// + /// The of System.Runtime.GetInvocationCounter. + /// Gets the number of times the current contract has been called during the execution. + /// + public static readonly InteropDescriptor System_Runtime_GetInvocationCounter = Register("System.Runtime.GetInvocationCounter", nameof(GetInvocationCounter), 1 << 4, CallFlags.None); + + /// + /// The of System.Runtime.GetRandom. + /// Gets the random number generated from the VRF. + /// + public static readonly InteropDescriptor System_Runtime_GetRandom = Register("System.Runtime.GetRandom", nameof(GetRandom), 0, CallFlags.None); + + /// + /// The of System.Runtime.Log. + /// Writes a log. + /// + public static readonly InteropDescriptor System_Runtime_Log = Register("System.Runtime.Log", nameof(RuntimeLog), 1 << 15, CallFlags.AllowNotify); + + /// + /// The of System.Runtime.Notify. + /// Sends a notification. + /// + public static readonly InteropDescriptor System_Runtime_Notify = Register("System.Runtime.Notify", nameof(RuntimeNotify), 1 << 15, CallFlags.AllowNotify); + + /// + /// The of System.Runtime.GetNotifications. + /// Gets the notifications sent by the specified contract during the execution. + /// + public static readonly InteropDescriptor System_Runtime_GetNotifications = Register("System.Runtime.GetNotifications", nameof(GetNotifications), 1 << 12, CallFlags.None); + + /// + /// The of System.Runtime.GasLeft. + /// Gets the remaining GAS that can be spent in order to complete the execution. + /// + public static readonly InteropDescriptor System_Runtime_GasLeft = Register("System.Runtime.GasLeft", nameof(GasLeft), 1 << 4, CallFlags.None); + + /// + /// The of System.Runtime.BurnGas. + /// Burning GAS to benefit the NEO ecosystem. + /// + public static readonly InteropDescriptor System_Runtime_BurnGas = Register("System.Runtime.BurnGas", nameof(BurnGas), 1 << 4, CallFlags.None); + + /// + /// The of System.Runtime.CurrentSigners. + /// Get the Signers of the current transaction. + /// + public static readonly InteropDescriptor System_Runtime_CurrentSigners = Register("System.Runtime.CurrentSigners", nameof(GetCurrentSigners), 1 << 4, CallFlags.None); + + /// + /// The implementation of System.Runtime.Platform. + /// Gets the name of the current platform. + /// + /// It always returns "NEO". + internal protected static string GetPlatform() + { + return "NEO"; + } + + /// + /// The implementation of System.Runtime.GetNetwork. + /// Gets the magic number of the current network. + /// + /// The magic number of the current network. + internal protected uint GetNetwork() + { + return ProtocolSettings.Network; + } + + /// + /// The implementation of System.Runtime.GetAddressVersion. + /// Gets the address version of the current network. + /// + /// The address version of the current network. + internal protected byte GetAddressVersion() + { + return ProtocolSettings.AddressVersion; + } + + /// + /// The implementation of System.Runtime.GetTime. + /// Gets the timestamp of the current block. + /// + /// The timestamp of the current block. + protected internal ulong GetTime() + { + if (PersistingBlock is null) + throw new InvalidOperationException("GetTime can only be called with Application trigger."); + return PersistingBlock.Timestamp; + } + + /// + /// The implementation of System.Runtime.GetScriptContainer. + /// Gets the current script container. + /// + /// The current script container. + protected internal StackItem GetScriptContainer() + { + if (ScriptContainer is not IInteroperable interop) throw new InvalidOperationException(); + return interop.ToStackItem(ReferenceCounter); + } + + /// + /// The implementation of System.Runtime.LoadScript. + /// Loads a script at rumtime. + /// + protected internal void RuntimeLoadScript(byte[] script, CallFlags callFlags, Array args) + { + if ((callFlags & ~CallFlags.All) != 0) + throw new ArgumentOutOfRangeException(nameof(callFlags), $"Invalid call flags: {callFlags}"); + + ExecutionContextState state = CurrentContext!.GetState(); + VM.ExecutionContext context = LoadScript(new Script(script, true), configureState: p => + { + p.CallingContext = CurrentContext; + p.CallFlags = callFlags & state.CallFlags & CallFlags.ReadOnly; + p.IsDynamicCall = true; + }); + + for (int i = args.Count - 1; i >= 0; i--) + context.EvaluationStack.Push(args[i]); + } + + /// + /// The implementation of System.Runtime.CheckWitness. + /// Determines whether the specified account has witnessed the current transaction. + /// + /// The hash or public key of the account. + /// if the account has witnessed the current transaction; otherwise, . + protected internal bool CheckWitness(byte[] hashOrPubkey) + { + UInt160 hash = hashOrPubkey.Length switch + { + 20 => new UInt160(hashOrPubkey), + 33 => Contract.CreateSignatureRedeemScript(ECPoint.DecodePoint(hashOrPubkey, ECCurve.Secp256r1)).ToScriptHash(), + _ => throw new ArgumentException("Invalid hashOrPubkey length", nameof(hashOrPubkey)) + }; + return CheckWitnessInternal(hash); + } + + /// + /// Determines whether the specified account has witnessed the current transaction. + /// + /// The hash of the account. + /// if the account has witnessed the current transaction; otherwise, . + protected internal bool CheckWitnessInternal(UInt160 hash) + { + if (hash.Equals(CallingScriptHash)) return true; + + if (ScriptContainer is Transaction tx) + { + Signer[] signers; + OracleResponse? response = tx.GetAttribute(); + if (response is null) + { + signers = tx.Signers; + } + else + { + OracleRequest request = NativeContract.Oracle.GetRequest(SnapshotCache, response.Id)!; + signers = NativeContract.Ledger.GetTransaction(SnapshotCache, request.OriginalTxid)!.Signers; + } + Signer? signer = signers.FirstOrDefault(p => p.Account.Equals(hash)); + if (signer is null) return false; + foreach (WitnessRule rule in signer.GetAllRules()) + { + if (rule.Condition.Match(this)) + return rule.Action == WitnessRuleAction.Allow; + } + return false; + } + + // If we don't have the ScriptContainer, we consider that there are no script hashes for verifying + if (ScriptContainer is null) return false; + + // Check allow state callflag + ValidateCallFlags(CallFlags.ReadStates); + + // only for non-Transaction types (Block, etc) + return ScriptContainer.GetScriptHashesForVerifying(SnapshotCache).Contains(hash); + } + + /// + /// The implementation of System.Runtime.GetInvocationCounter. + /// Gets the number of times the current contract has been called during the execution. + /// + /// The number of times the current contract has been called during the execution. + protected internal int GetInvocationCounter() + { + if (!invocationCounter.TryGetValue(CurrentScriptHash!, out var counter)) + { + invocationCounter[CurrentScriptHash!] = counter = 1; + } + return counter; + } + + /// + /// The implementation of System.Runtime.GetRandom. + /// Gets the next random number. + /// + /// The next random number. + protected internal BigInteger GetRandom() + { + AddFee((1 << 13) * ExecFeeFactor); + byte[] buffer = Cryptography.Helper.Murmur128(nonceData, ProtocolSettings.Network + randomTimes++); + return new BigInteger(buffer, isUnsigned: true); + } + + /// + /// The implementation of System.Runtime.Log. + /// Writes a log. + /// + /// The message of the log. + protected internal void RuntimeLog(byte[] state) + { + if (state.Length > MaxNotificationSize) + throw new ArgumentException($"Notification size {state.Length} exceeds maximum allowed size of {MaxNotificationSize} bytes", nameof(state)); + try + { + string message = state.ToStrictUtf8String(); + Log?.Invoke(this, new LogEventArgs(ScriptContainer, CurrentScriptHash!, message)); + } + catch + { + throw new ArgumentException("Failed to convert byte array to string: Invalid UTF-8 sequence", nameof(state)); + } + } + + /// + /// The implementation of System.Runtime.Notify. + /// Sends a notification. + /// + /// The name of the event. + /// The arguments of the event. + protected internal void RuntimeNotify(byte[] eventName, Array state) + { + if (eventName.Length > MaxEventName) + throw new ArgumentException($"Event name size {eventName.Length} exceeds maximum allowed size of {MaxEventName} bytes", nameof(eventName)); + + string name = eventName.ToStrictUtf8String(); + ContractState contract = CurrentContext!.GetState().Contract + ?? throw new InvalidOperationException("Notifications are not allowed in dynamic scripts."); + var @event = contract.Manifest.Abi.Events.FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.Ordinal)) + ?? throw new InvalidOperationException($"Event `{name}` does not exist."); + if (@event.Parameters.Length != state.Count) + throw new InvalidOperationException("The number of the arguments does not match the formal parameters of the event."); + for (int i = 0; i < @event.Parameters.Length; i++) + { + var p = @event.Parameters[i]; + if (!CheckItemType(state[i], p.Type)) + throw new InvalidOperationException($"The type of the argument `{p.Name}` does not match the formal parameter."); + } + using MemoryStream ms = new(MaxNotificationSize); + using BinaryWriter writer = new(ms, Utility.StrictUTF8, true); + BinarySerializer.Serialize(writer, state, MaxNotificationSize, Limits.MaxStackSize); + SendNotification(CurrentScriptHash!, name, state); + } + + /// + /// Sends a notification for the specified contract. + /// + /// The hash of the specified contract. + /// The name of the event. + /// The arguments of the event. + protected internal void SendNotification(UInt160 hash, string eventName, Array state) + { + notifications ??= new List(); + // Restrict the number of notifications for Application executions. Do not check + // persisting triggers to avoid native persist failure. Do not check verification + // trigger since verification context is loaded with ReadOnly flag. + if (Trigger == TriggerType.Application && notifications.Count >= MaxNotificationCount) + { + throw new InvalidOperationException($"Maximum number of notifications `{MaxNotificationCount}` is reached."); + } + NotifyEventArgs notification = new(ScriptContainer, hash, eventName, (Array)state.DeepCopy(asImmutable: true)); + Notify?.Invoke(this, notification); + notifications.Add(notification); + CurrentContext!.GetState().NotificationCount++; + } + + /// + /// The implementation of System.Runtime.GetNotifications. + /// Gets the notifications sent by the specified contract during the execution. + /// + /// The hash of the specified contract. It can be set to to get all notifications. + /// The notifications sent during the execution. + protected internal Array GetNotifications(UInt160? hash) + { + IEnumerable notifications = Notifications; + if (hash != null) // must filter by scriptHash + notifications = notifications.Where(p => p.ScriptHash == hash); + var array = notifications.ToArray(); + if (array.Length > Limits.MaxStackSize) throw new InvalidOperationException(); + Array notifyArray = new(ReferenceCounter); + foreach (var notify in array) + { + notifyArray.Add(notify.ToStackItem(ReferenceCounter)); + } + return notifyArray; + } + + /// + /// The implementation of System.Runtime.BurnGas. + /// Burning GAS to benefit the NEO ecosystem. + /// + /// The amount of GAS to burn, in the unit of datoshi, 1 datoshi = 1e-8 GAS + protected internal void BurnGas(long datoshi) + { + if (datoshi <= 0) + throw new InvalidOperationException("GAS must be positive."); + AddFee(datoshi); + } + + /// + /// Get the Signers of the current transaction. + /// + /// The signers of the current transaction, or null if is not related to a transaction execution. + protected internal Signer[]? GetCurrentSigners() + { + if (ScriptContainer is Transaction tx) + return tx.Signers; + + return null; + } + + private static bool CheckItemType(StackItem item, ContractParameterType type) + { + StackItemType aType = item.Type; + if (aType == StackItemType.Pointer) return false; + switch (type) + { + case ContractParameterType.Any: + return true; + case ContractParameterType.Boolean: + return aType == StackItemType.Boolean; + case ContractParameterType.Integer: + return aType == StackItemType.Integer; + case ContractParameterType.ByteArray: + return aType is StackItemType.Any or StackItemType.ByteString or StackItemType.Buffer; + case ContractParameterType.String: + { + if (aType is StackItemType.ByteString or StackItemType.Buffer) + { + try + { + _ = item.GetSpan().ToStrictUtf8String(); // Prevent any non-UTF8 string + return true; + } + catch { } + } + return false; + } + case ContractParameterType.Hash160: + if (aType == StackItemType.Any) return true; + if (aType != StackItemType.ByteString && aType != StackItemType.Buffer) return false; + return item.GetSpan().Length == UInt160.Length; + case ContractParameterType.Hash256: + if (aType == StackItemType.Any) return true; + if (aType != StackItemType.ByteString && aType != StackItemType.Buffer) return false; + return item.GetSpan().Length == UInt256.Length; + case ContractParameterType.PublicKey: + if (aType == StackItemType.Any) return true; + if (aType != StackItemType.ByteString && aType != StackItemType.Buffer) return false; + return item.GetSpan().Length == 33; + case ContractParameterType.Signature: + if (aType == StackItemType.Any) return true; + if (aType != StackItemType.ByteString && aType != StackItemType.Buffer) return false; + return item.GetSpan().Length == 64; + case ContractParameterType.Array: + return aType is StackItemType.Any or StackItemType.Array or StackItemType.Struct; + case ContractParameterType.Map: + return aType is StackItemType.Any or StackItemType.Map; + case ContractParameterType.InteropInterface: + return aType is StackItemType.Any or StackItemType.InteropInterface; + default: + return false; + } + } +} diff --git a/src/Neo/SmartContract/ApplicationEngine.Storage.cs b/src/Neo/SmartContract/ApplicationEngine.Storage.cs new file mode 100644 index 0000000000..5b27caee0f --- /dev/null +++ b/src/Neo/SmartContract/ApplicationEngine.Storage.cs @@ -0,0 +1,299 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ApplicationEngine.Storage.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.SmartContract.Iterators; +using Neo.SmartContract.Native; + +namespace Neo.SmartContract; + +partial class ApplicationEngine +{ + /// + /// The maximum size of storage keys. + /// + public const int MaxStorageKeySize = 64; + + /// + /// The maximum size of storage values. + /// + public const int MaxStorageValueSize = ushort.MaxValue; + + /// + /// The of System.Storage.GetContext. + /// Gets the storage context for the current contract. + /// + public static readonly InteropDescriptor System_Storage_GetContext = Register("System.Storage.GetContext", nameof(GetStorageContext), 1 << 4, CallFlags.ReadStates); + + /// + /// The of System.Storage.GetReadOnlyContext. + /// Gets the readonly storage context for the current contract. + /// + public static readonly InteropDescriptor System_Storage_GetReadOnlyContext = Register("System.Storage.GetReadOnlyContext", nameof(GetReadOnlyContext), 1 << 4, CallFlags.ReadStates); + + /// + /// The of System.Storage.AsReadOnly. + /// Converts the specified storage context to a new readonly storage context. + /// + public static readonly InteropDescriptor System_Storage_AsReadOnly = Register("System.Storage.AsReadOnly", nameof(AsReadOnly), 1 << 4, CallFlags.ReadStates); + + /// + /// The of System.Storage.Get. + /// Gets the entry with the specified key from the storage. + /// + public static readonly InteropDescriptor System_Storage_Get = Register("System.Storage.Get", nameof(Get), 1 << 15, CallFlags.ReadStates); + + /// + /// The of System.Storage.Find. + /// Finds the entries from the storage. + /// + public static readonly InteropDescriptor System_Storage_Find = Register("System.Storage.Find", nameof(Find), 1 << 15, CallFlags.ReadStates); + + /// + /// The of System.Storage.Put. + /// Puts a new entry into the storage. + /// + public static readonly InteropDescriptor System_Storage_Put = Register("System.Storage.Put", nameof(Put), 1 << 15, CallFlags.WriteStates); + + /// + /// The of System.Storage.Delete. + /// Deletes an entry from the storage. + /// + public static readonly InteropDescriptor System_Storage_Delete = Register("System.Storage.Delete", nameof(Delete), 1 << 15, CallFlags.WriteStates); + + /// + /// The of System.Storage.Local.Get. + /// Gets the entry with the specified key from the storage. + /// + public static readonly InteropDescriptor System_Storage_Local_Get = Register("System.Storage.Local.Get", nameof(GetLocal), 1 << 15, CallFlags.ReadStates); + + /// + /// The of System.Storage.Local.Find. + /// Finds the entries from the storage. + /// + public static readonly InteropDescriptor System_Storage_Local_Find = Register("System.Storage.Local.Find", nameof(FindLocal), 1 << 15, CallFlags.ReadStates); + + /// + /// The of System.Storage.Local.Put. + /// Puts a new entry into the storage. + /// + public static readonly InteropDescriptor System_Storage_Local_Put = Register("System.Storage.Local.Put", nameof(PutLocal), 1 << 15, CallFlags.WriteStates); + + /// + /// The of System.Storage.Local.Delete. + /// Deletes an entry from the storage. + /// + public static readonly InteropDescriptor System_Storage_Local_Delete = Register("System.Storage.Local.Delete", nameof(DeleteLocal), 1 << 15, CallFlags.WriteStates); + + /// + /// The implementation of System.Storage.GetContext. + /// Gets the storage context for the current contract. + /// + /// The storage context for the current contract. + protected internal StorageContext GetStorageContext() + { + ContractState contract = NativeContract.ContractManagement.GetContract(SnapshotCache, CurrentScriptHash!) + ?? throw new InvalidOperationException("This method can only be called by a deployed contract."); + return new StorageContext + { + Id = contract.Id, + IsReadOnly = false + }; + } + + /// + /// The implementation of System.Storage.GetReadOnlyContext. + /// Gets the readonly storage context for the current contract. + /// + /// The storage context for the current contract. + protected internal StorageContext GetReadOnlyContext() + { + ContractState contract = NativeContract.ContractManagement.GetContract(SnapshotCache, CurrentScriptHash!) + ?? throw new InvalidOperationException("This method can only be called by a deployed contract."); + return new StorageContext + { + Id = contract.Id, + IsReadOnly = true + }; + } + + /// + /// The implementation of System.Storage.AsReadOnly. + /// Converts the specified storage context to a new readonly storage context. + /// + /// The storage context to convert. + /// The readonly storage context. + protected internal static StorageContext AsReadOnly(StorageContext context) + { + if (!context.IsReadOnly) + context = new StorageContext + { + Id = context.Id, + IsReadOnly = true + }; + return context; + } + + /// + /// The implementation of System.Storage.Get. + /// Gets the entry with the specified key from the storage. + /// + /// The context of the storage. + /// The key of the entry. + /// The value of the entry. Or if the entry doesn't exist. + protected internal ReadOnlyMemory? Get(StorageContext context, byte[] key) + { + return SnapshotCache.TryGet(new StorageKey + { + Id = context.Id, + Key = key + })?.Value; + } + + /// + /// The implementation of System.Storage.Local.Get. + /// Gets the entry with the specified key from the storage. + /// + /// The key of the entry. + /// The value of the entry. Or if the entry doesn't exist. + protected internal ReadOnlyMemory? GetLocal(byte[] key) + { + return Get(GetReadOnlyContext(), key); + } + + /// + /// The implementation of System.Storage.Find. + /// Finds the entries from the storage. + /// + /// The context of the storage. + /// The prefix of keys to find. + /// The options of the search. + /// An iterator for the results. + protected internal IIterator Find(StorageContext context, byte[] prefix, FindOptions options) + { + if ((options & ~FindOptions.All) != 0) + throw new ArgumentOutOfRangeException(nameof(options), $"Invalid find options: {options}"); + + if (options.HasFlag(FindOptions.KeysOnly) && + (options.HasFlag(FindOptions.ValuesOnly) || + options.HasFlag(FindOptions.DeserializeValues) || + options.HasFlag(FindOptions.PickField0) || + options.HasFlag(FindOptions.PickField1))) + { + throw new ArgumentException("KeysOnly cannot be used with ValuesOnly, DeserializeValues, PickField0, or PickField1", nameof(options)); + } + + if (options.HasFlag(FindOptions.ValuesOnly) && (options.HasFlag(FindOptions.KeysOnly) || options.HasFlag(FindOptions.RemovePrefix))) + throw new ArgumentException("ValuesOnly cannot be used with KeysOnly or RemovePrefix", nameof(options)); + + if (options.HasFlag(FindOptions.PickField0) && options.HasFlag(FindOptions.PickField1)) + throw new ArgumentException("PickField0 and PickField1 cannot be used together", nameof(options)); + + if ((options.HasFlag(FindOptions.PickField0) || options.HasFlag(FindOptions.PickField1)) && !options.HasFlag(FindOptions.DeserializeValues)) + throw new ArgumentException("PickField0 or PickField1 requires DeserializeValues", nameof(options)); + + var prefixKey = StorageKey.CreateSearchPrefix(context.Id, prefix); + var direction = options.HasFlag(FindOptions.Backwards) ? SeekDirection.Backward : SeekDirection.Forward; + return new StorageIterator(SnapshotCache.Find(prefixKey, direction).GetEnumerator(), prefix.Length, options); + } + + /// + /// The implementation of System.Storage.Local.Find. + /// Finds the entries from the storage. + /// + /// The prefix of keys to find. + /// The options of the search. + /// An iterator for the results. + protected internal IIterator FindLocal(byte[] prefix, FindOptions options) + { + return Find(GetReadOnlyContext(), prefix, options); + } + + /// + /// The implementation of System.Storage.Put. + /// Puts a new entry into the storage. + /// + /// The context of the storage. + /// The key of the entry. + /// The value of the entry. + protected internal void Put(StorageContext context, byte[] key, byte[] value) + { + if (key.Length > MaxStorageKeySize) + throw new ArgumentException($"Key length {key.Length} exceeds maximum allowed size of {MaxStorageKeySize} bytes.", nameof(key)); + if (value.Length > MaxStorageValueSize) + throw new ArgumentException($"Value length {value.Length} exceeds maximum allowed size of {MaxStorageValueSize} bytes.", nameof(value)); + if (context.IsReadOnly) throw new ArgumentException("StorageContext is read-only", nameof(context)); + + int newDataSize; + StorageKey skey = new() + { + Id = context.Id, + Key = key + }; + var item = SnapshotCache.GetAndChange(skey); + if (item is null) + { + newDataSize = key.Length + value.Length; + SnapshotCache.Add(skey, item = new StorageItem()); + } + else + { + if (value.Length == 0) + newDataSize = 0; + else if (value.Length <= item.Value.Length) + newDataSize = (value.Length - 1) / 4 + 1; + else if (item.Value.Length == 0) + newDataSize = value.Length; + else + newDataSize = (item.Value.Length - 1) / 4 + 1 + value.Length - item.Value.Length; + } + AddFee(newDataSize * StoragePrice); + + item.Value = value; + } + + /// + /// The implementation of System.Storage.Local.Put. + /// Puts a new entry into the storage. + /// + /// The key of the entry. + /// The value of the entry. + protected internal void PutLocal(byte[] key, byte[] value) + { + Put(GetStorageContext(), key, value); + } + + /// + /// The implementation of System.Storage.Delete. + /// Deletes an entry from the storage. + /// + /// The context of the storage. + /// The key of the entry. + protected internal void Delete(StorageContext context, byte[] key) + { + if (context.IsReadOnly) throw new ArgumentException("StorageContext is read-only", nameof(context)); + SnapshotCache.Delete(new StorageKey + { + Id = context.Id, + Key = key + }); + } + + /// + /// The implementation of System.Storage.Local.Delete. + /// Deletes an entry from the storage. + /// + /// The key of the entry. + protected internal void DeleteLocal(byte[] key) + { + Delete(GetStorageContext(), key); + } +} diff --git a/src/Neo/SmartContract/ApplicationEngine.cs b/src/Neo/SmartContract/ApplicationEngine.cs new file mode 100644 index 0000000000..3fc05542ec --- /dev/null +++ b/src/Neo/SmartContract/ApplicationEngine.cs @@ -0,0 +1,768 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ApplicationEngine.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System.Collections.Immutable; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using Array = System.Array; +using ExecutionContext = Neo.VM.ExecutionContext; +using VMArray = Neo.VM.Types.Array; + +namespace Neo.SmartContract; + +/// +/// A virtual machine used to execute smart contracts in the NEO system. +/// +public partial class ApplicationEngine : ExecutionEngine +{ + protected static readonly JumpTable DefaultJumpTable = ComposeDefaultJumpTable(); + + /// + /// The maximum cost that can be spent when a contract is executed in test mode. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS + /// + public const long TestModeGas = 20_00000000; + + public delegate void OnInstanceHandlerEvent(ApplicationEngine engine); + public delegate void OnLogEvent(ApplicationEngine engine, LogEventArgs args); + public delegate void OnNotifyEvent(ApplicationEngine engine, NotifyEventArgs args); + + /// + /// Triggered when a contract calls System.Runtime.Notify. + /// + public event OnNotifyEvent? Notify; + + /// + /// Triggered when a contract calls System.Runtime.Log. + /// + public event OnLogEvent? Log; + + /// + /// On Application Engine + /// + public static event OnInstanceHandlerEvent? InstanceCreated; + + private static Dictionary? services; + // Total amount of GAS spent to execute. + // In the unit of datoshi, 1 datoshi = 1e-8 GAS, 1 GAS = 1e8 datoshi + private readonly long _feeAmount; + private Dictionary? states; + private readonly DataCache originalSnapshotCache; + private List? notifications; + private List? disposables; + private readonly Dictionary invocationCounter = new(); + private readonly Dictionary contractTasks = new(); + internal readonly uint ExecFeeFactor; + // In the unit of datoshi, 1 datoshi = 1e-8 GAS + internal readonly uint StoragePrice; + private byte[] nonceData; + + /// + /// Gets or sets the provider used to create the . + /// + public static IApplicationEngineProvider? Provider { get; set; } + + /// + /// Gets the descriptors of all interoperable services available in NEO. + /// + public static IReadOnlyDictionary Services => services ?? (IReadOnlyDictionary)ImmutableDictionary.Empty; + + /// + /// The diagnostic used by the engine. This property can be . + /// + public IDiagnostic? Diagnostic { get; } + + private List Disposables => disposables ??= new List(); + + /// + /// The trigger of the execution. + /// + public TriggerType Trigger { get; } + + /// + /// The container that containing the executed script. This field could be if the contract is invoked by system. + /// + public IVerifiable? ScriptContainer { get; } + + /// + /// The snapshotcache used to read or write data. + /// + public DataCache SnapshotCache => CurrentContext?.GetState().SnapshotCache ?? originalSnapshotCache; + + /// + /// The block being persisted. This field could be if the is . + /// + public Block? PersistingBlock { get; } + + /// + /// The used by the engine. + /// + public ProtocolSettings ProtocolSettings { get; } + + /// + /// GAS spent to execute. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS, 1 GAS = 1e8 datoshi + /// + public long FeeConsumed { get; protected set; } = 0; + + /// + /// The remaining GAS that can be spent in order to complete the execution. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS, 1 GAS = 1e8 datoshi + /// + public long GasLeft => _feeAmount - FeeConsumed; + + /// + /// The exception that caused the execution to terminate abnormally. This field could be if no exception is thrown. + /// + public Exception? FaultException { get; protected set; } + + /// + /// The script hash of the current context. This field could be if no context is loaded to the engine. + /// + public UInt160? CurrentScriptHash => CurrentContext?.GetScriptHash(); + + /// + /// The script hash of the calling contract. This field could be if the current context is the entry context. + /// + public virtual UInt160? CallingScriptHash + { + get + { + if (CurrentContext is null) return null; + var state = CurrentContext.GetState(); + return state.NativeCallingScriptHash ?? state.CallingContext?.GetState().ScriptHash; + } + } + + /// + /// The script hash of the entry context. This field could be if no context is loaded to the engine. + /// + public virtual UInt160? EntryScriptHash => EntryContext?.GetScriptHash(); + + /// + /// The notifications sent during the execution. + /// + public IReadOnlyList Notifications => notifications ?? (IReadOnlyList)Array.Empty(); + + /// + /// Initializes a new instance of the class. + /// + /// The trigger of the execution. + /// The container of the script. + /// The snapshot used by the engine during execution. + /// + /// The block being persisted. + /// It should be if the is . + /// + /// The used by the engine. + /// + /// The maximum gas, in the unit of datoshi, used in this execution. + /// The execution will fail when the gas is exhausted. + /// + /// The diagnostic to be used by the . + /// The jump table to be used by the . + protected ApplicationEngine( + TriggerType trigger, IVerifiable? container, DataCache snapshotCache, Block? persistingBlock, + ProtocolSettings settings, long gas, IDiagnostic? diagnostic = null, JumpTable? jumpTable = null) + : base(jumpTable ?? DefaultJumpTable) + { + Trigger = trigger; + ScriptContainer = container; + originalSnapshotCache = snapshotCache; + PersistingBlock = persistingBlock; + ProtocolSettings = settings; + _feeAmount = gas; + Diagnostic = diagnostic; + nonceData = container is Transaction tx ? tx.Hash.ToArray()[..16] : new byte[16]; + if (snapshotCache is null || persistingBlock?.Index == 0) + { + ExecFeeFactor = PolicyContract.DefaultExecFeeFactor; + StoragePrice = PolicyContract.DefaultStoragePrice; + } + else + { + ExecFeeFactor = NativeContract.Policy.GetExecFeeFactor(snapshotCache); + StoragePrice = NativeContract.Policy.GetStoragePrice(snapshotCache); + } + + if (persistingBlock is not null) + { + ref ulong nonce = ref Unsafe.As(ref nonceData[0]); + nonce ^= persistingBlock.Nonce; + } + diagnostic?.Initialized(this); + } + + #region JumpTable + + private static JumpTable ComposeDefaultJumpTable() + { + var table = new JumpTable(); + + table[OpCode.SYSCALL] = OnSysCall; + table[OpCode.CALLT] = OnCallT; + + return table; + } + + protected static void OnCallT(ExecutionEngine engine, Instruction instruction) + { + if (engine is ApplicationEngine app) + { + uint tokenId = instruction.TokenU16; + + app.ValidateCallFlags(CallFlags.ReadStates | CallFlags.AllowCall); + ContractState? contract = app.CurrentContext!.GetState().Contract; + if (contract is null || tokenId >= contract.Nef.Tokens.Length) + throw new InvalidOperationException(); + MethodToken token = contract.Nef.Tokens[tokenId]; + if (token.ParametersCount > app.CurrentContext.EvaluationStack.Count) + throw new InvalidOperationException(); + StackItem[] args = new StackItem[token.ParametersCount]; + for (int i = 0; i < token.ParametersCount; i++) + args[i] = app.Pop(); + app.CallContractInternal(token.Hash, token.Method, token.CallFlags, token.HasReturnValue, args); + } + else + { + throw new InvalidOperationException(); + } + } + + protected static void OnSysCall(ExecutionEngine engine, Instruction instruction) + { + if (engine is ApplicationEngine app) + { + var interop = GetInteropDescriptor(instruction.TokenU32); + + if (interop.Hardfork != null && !app.IsHardforkEnabled(interop.Hardfork.Value)) + { + // The syscall is not active + + throw new KeyNotFoundException(); + } + + app.OnSysCall(interop); + } + else + { + throw new InvalidOperationException(); + } + } + + #endregion + + /// + /// Adds GAS to and checks if it has exceeded the maximum limit. + /// + /// The amount of GAS, in the unit of datoshi, 1 datoshi = 1e-8 GAS, to be added. + protected internal void AddFee(long datoshi) + { + // Check whitelist + + if (CurrentContext?.GetState()?.WhiteListed == true) + { + // The execution is whitelisted + return; + } + + FeeConsumed = checked(FeeConsumed + datoshi); + if (FeeConsumed > _feeAmount) + throw new InvalidOperationException("Insufficient GAS."); + } + + protected override void OnFault(Exception ex) + { + FaultException = ex; + notifications = null; + base.OnFault(ex); + } + + internal void Throw(Exception ex) + { + OnFault(ex); + } + + private ExecutionContext CallContractInternal(UInt160 contractHash, string method, CallFlags flags, bool hasReturnValue, StackItem[] args) + { + ContractState contract = NativeContract.ContractManagement.GetContract(SnapshotCache, contractHash) + ?? throw new InvalidOperationException($"Called Contract Does Not Exist: {contractHash}"); + ContractMethodDescriptor md = contract.Manifest.Abi.GetMethod(method, args.Length) + ?? throw new InvalidOperationException($"Method \"{method}\" with {args.Length} parameter(s) doesn't exist in the contract {contractHash}."); + return CallContractInternal(contract, md, flags, hasReturnValue, args); + } + + private ExecutionContext CallContractInternal(ContractState contract, ContractMethodDescriptor method, CallFlags flags, bool hasReturnValue, IReadOnlyList args) + { + if (NativeContract.Policy.IsBlocked(SnapshotCache, contract.Hash)) + throw new InvalidOperationException($"The contract {contract.Hash} has been blocked."); + + ExecutionContext currentContext = CurrentContext!; + ExecutionContextState state = currentContext.GetState(); + if (method.Safe) + { + flags &= ~(CallFlags.WriteStates | CallFlags.AllowNotify); + } + else + { + if (state.Contract?.CanCall(contract, method.Name) == false) + throw new InvalidOperationException($"Cannot Call Method {method.Name} Of Contract {contract.Hash} From Contract {CurrentScriptHash}"); + } + + // Check whitelist + + if (NativeContract.Policy.IsWhitelistFeeContract(SnapshotCache, contract.Hash, method, out var fixedFee)) + { + AddFee(fixedFee.Value); + state.WhiteListed = true; + } + + if (invocationCounter.TryGetValue(contract.Hash, out var counter)) + { + invocationCounter[contract.Hash] = counter + 1; + } + else + { + invocationCounter[contract.Hash] = 1; + } + + CallFlags callingFlags = state.CallFlags; + + if (args.Count != method.Parameters.Length) throw new InvalidOperationException($"Method {method} Expects {method.Parameters.Length} Arguments But Receives {args.Count} Arguments"); + if (hasReturnValue ^ (method.ReturnType != ContractParameterType.Void)) throw new InvalidOperationException("The return value type does not match."); + + var contextNew = LoadContract(contract, method, flags & callingFlags); + state = contextNew.GetState(); + state.CallingContext = currentContext; + + for (int i = args.Count - 1; i >= 0; i--) + contextNew.EvaluationStack.Push(args[i]); + + return contextNew; + } + + internal ContractTask CallFromNativeContractAsync(UInt160 callingScriptHash, UInt160 hash, string method, params object?[] args) + { + var contextNew = CallContractInternal(hash, method, CallFlags.All, false, args.Select(Convert).ToArray()); + var state = contextNew.GetState(); + state.NativeCallingScriptHash = callingScriptHash; + ContractTask task = new(); + contractTasks.Add(contextNew, task.GetAwaiter()); + return task; + } + + internal ContractTask CallFromNativeContractAsync(UInt160 callingScriptHash, UInt160 hash, string method, params object?[] args) + { + var contextNew = CallContractInternal(hash, method, CallFlags.All, true, args.Select(Convert).ToArray()); + var state = contextNew.GetState(); + state.NativeCallingScriptHash = callingScriptHash; + ContractTask task = new(); + contractTasks.Add(contextNew, task.GetAwaiter()); + return task; + } + + protected override void ContextUnloaded(ExecutionContext context) + { + base.ContextUnloaded(context); + if (context.Script != CurrentContext?.Script) + { + ExecutionContextState state = context.GetState(); + if (UncaughtException is null) + { + state.SnapshotCache?.Commit(); + if (CurrentContext != null) + { + ExecutionContextState contextState = CurrentContext.GetState(); + contextState.NotificationCount += state.NotificationCount; + if (state.IsDynamicCall) + { + if (context.EvaluationStack.Count == 0) + Push(StackItem.Null); + else if (context.EvaluationStack.Count > 1) + throw new NotSupportedException("Multiple return values are not allowed in cross-contract calls."); + } + } + } + else + { + if (state.NotificationCount > 0) + notifications!.RemoveRange(notifications.Count - state.NotificationCount, state.NotificationCount); + } + } + Diagnostic?.ContextUnloaded(context); + if (contractTasks.Remove(context, out var awaiter)) + { + if (UncaughtException is not null) + throw new VMUnhandledException(UncaughtException); + awaiter.SetResult(this); + } + } + + /// + /// Use the loaded to create a new instance of the class. + /// If no is loaded, the constructor of will be called. + /// + /// The trigger of the execution. + /// The container of the script. + /// The snapshot used by the engine during execution. + /// + /// The block being persisted. + /// It should be if the is . + /// + /// The used by the engine. + /// + /// The maximum gas used in this execution, in the unit of datoshi. + /// The execution will fail when the gas is exhausted. + /// + /// The diagnostic to be used by the . + /// The engine instance created. + public static ApplicationEngine Create(TriggerType trigger, IVerifiable? container, DataCache snapshot, Block? persistingBlock = null, ProtocolSettings? settings = null, long gas = TestModeGas, IDiagnostic? diagnostic = null) + { + var index = persistingBlock?.Index ?? NativeContract.Ledger.CurrentIndex(snapshot); + settings ??= ProtocolSettings.Default; + // Adjust jump table according persistingBlock + var engine = Provider?.Create(trigger, container, snapshot, persistingBlock, settings, gas, diagnostic, DefaultJumpTable) + ?? new ApplicationEngine(trigger, container, snapshot, persistingBlock, settings, gas, diagnostic, DefaultJumpTable); + + InstanceCreated?.Invoke(engine); + return engine; + } + + public override void LoadContext(ExecutionContext context) + { + // Set default execution context state + var state = context.GetState(); + state.ScriptHash ??= ((ReadOnlyMemory)context.Script).Span.ToScriptHash(); + invocationCounter.TryAdd(state.ScriptHash, 1); + base.LoadContext(context); + Diagnostic?.ContextLoaded(context); + } + + /// + /// Loads a deployed contract to the invocation stack. If the _initialize method is found on the contract, loads it as well. + /// + /// The contract to be loaded. + /// The method of the contract to be called. + /// The used to call the method. + /// The loaded context. + public ExecutionContext LoadContract(ContractState contract, ContractMethodDescriptor method, CallFlags callFlags) + { + ExecutionContext context = LoadScript(contract.Script, + rvcount: method.ReturnType == ContractParameterType.Void ? 0 : 1, + initialPosition: method.Offset, + configureState: p => + { + p.CallFlags = callFlags; + p.ScriptHash = contract.Hash; + p.Contract = new ContractState + { + Id = contract.Id, + UpdateCounter = contract.UpdateCounter, + Hash = contract.Hash, + Nef = contract.Nef, + Manifest = contract.Manifest + }; + }); + + // Call initialization + var init = contract.Manifest.Abi.GetMethod(ContractBasicMethod.Initialize, ContractBasicMethod.InitializePCount); + if (init is not null) + { + LoadContext(context.Clone(init.Offset)); + } + + return context; + } + + /// + /// Loads a script to the invocation stack. + /// + /// The script to be loaded. + /// The number of return values of the script. + /// The initial position of the instruction pointer. + /// The action used to configure the state of the loaded context. + /// The loaded context. + public ExecutionContext LoadScript(Script script, int rvcount = -1, int initialPosition = 0, Action? configureState = null) + { + // Create and configure context + ExecutionContext context = CreateContext(script, rvcount, initialPosition); + ExecutionContextState state = context.GetState(); + state.SnapshotCache = SnapshotCache?.CloneCache(); + configureState?.Invoke(state); + + // Load context + LoadContext(context); + return context; + } + + /// + /// Converts an to a that used in the virtual machine. + /// + /// The to convert. + /// The converted . + protected internal StackItem Convert(object? value) + { + if (value is IDisposable disposable) Disposables.Add(disposable); + return value switch + { + null => StackItem.Null, + bool b => b, + sbyte i => i, + byte i => (BigInteger)i, + short i => i, + ushort i => (BigInteger)i, + int i => i, + uint i => i, + long i => i, + ulong i => i, + Enum e => Convert(System.Convert.ChangeType(e, e.GetTypeCode())), + byte[] data => data, + ReadOnlyMemory m => m, + string s => s, + BigInteger i => i, + JObject o => o.ToByteArray(false), + IInteroperable interoperable => interoperable.ToStackItem(ReferenceCounter), + ISerializable i => i.ToArray(), + StackItem item => item, + (object a, object b) => new Struct(ReferenceCounter) { Convert(a), Convert(b) }, + Array array => new VMArray(ReferenceCounter, array.OfType().Select(p => Convert(p))), + _ => StackItem.FromInterface(value) + }; + } + + /// + /// Converts a to an that to be used as an argument of an interoperable service or native contract. + /// + /// The to convert. + /// The descriptor of the parameter. + /// The converted . + protected internal object? Convert(StackItem item, InteropParameterDescriptor descriptor) + { + if (item.IsNull && !descriptor.IsNullable && descriptor.Type != typeof(StackItem)) + throw new InvalidOperationException($"The argument `{descriptor.Name}` can't be null."); + descriptor.Validate(item); + if (descriptor.IsArray) + { + Array av; + if (item is VMArray array) + { + av = Array.CreateInstance(descriptor.Type.GetElementType()!, array.Count); + for (int i = 0; i < av.Length; i++) + { + if (array[i].IsNull && !descriptor.IsElementNullable) + throw new InvalidOperationException($"The element of `{descriptor.Name}` can't be null."); + av.SetValue(descriptor.Converter(array[i]), i); + } + } + else + { + int count = (int)item.GetInteger(); + if (count > Limits.MaxStackSize) throw new InvalidOperationException(); + av = Array.CreateInstance(descriptor.Type.GetElementType()!, count); + for (int i = 0; i < av.Length; i++) + { + StackItem popped = Pop(); + if (popped.IsNull && !descriptor.IsElementNullable) + throw new InvalidOperationException($"The element of `{descriptor.Name}` can't be null."); + av.SetValue(descriptor.Converter(popped), i); + } + } + return av; + } + else + { + object? value = descriptor.Converter(item); + if (descriptor.IsEnum) + value = Enum.ToObject(descriptor.Type, value!); + else if (descriptor.IsInterface) + value = ((InteropInterface?)value)?.GetInterface(); + return value; + } + } + + public override void Dispose() + { + Diagnostic?.Disposed(); + if (disposables != null) + { + foreach (var disposable in disposables) + disposable.Dispose(); + disposables = null; + } + base.Dispose(); + } + + /// + /// Determines whether the of the current context meets the specified requirements. + /// + /// The requirements to check. + internal protected void ValidateCallFlags(CallFlags requiredCallFlags) + { + ExecutionContextState state = CurrentContext!.GetState(); + if (!state.CallFlags.HasFlag(requiredCallFlags)) + throw new InvalidOperationException($"Cannot call this SYSCALL with the flag {state.CallFlags}."); + } + + /// + /// Invokes the specified interoperable service. + /// + /// The descriptor of the interoperable service. + protected virtual void OnSysCall(InteropDescriptor descriptor) + { + ValidateCallFlags(descriptor.RequiredCallFlags); + AddFee(descriptor.FixedPrice * ExecFeeFactor); + + object?[] parameters = new object?[descriptor.Parameters.Count]; + for (int i = 0; i < parameters.Length; i++) + parameters[i] = Convert(Pop(), descriptor.Parameters[i]); + + object? returnValue = descriptor.Handler.Invoke(this, parameters); + if (descriptor.Handler.ReturnType != typeof(void)) + Push(Convert(returnValue)); + } + + protected override void PreExecuteInstruction(Instruction instruction) + { + Diagnostic?.PreExecuteInstruction(instruction); + AddFee(ExecFeeFactor * OpCodePriceTable[(byte)instruction.OpCode]); + } + + protected override void PostExecuteInstruction(Instruction instruction) + { + base.PostExecuteInstruction(instruction); + Diagnostic?.PostExecuteInstruction(instruction); + } + + private static Block CreateDummyBlock(IReadOnlyStore snapshot, ProtocolSettings settings) + { + UInt256 hash = NativeContract.Ledger.CurrentHash(snapshot); + Block currentBlock = NativeContract.Ledger.GetBlock(snapshot, hash)!; + return new Block + { + Header = new Header + { + Version = 0, + PrevHash = hash, + MerkleRoot = new UInt256(), + Timestamp = currentBlock.Timestamp + settings.MillisecondsPerBlock, + Index = currentBlock.Index + 1, + NextConsensus = currentBlock.NextConsensus, + Witness = Witness.Empty, + }, + Transactions = [], + }; + } + + protected static InteropDescriptor Register(string name, string handler, long fixedPrice, CallFlags requiredCallFlags, Hardfork? hardfork = null) + { + var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; + var method = typeof(ApplicationEngine).GetMethod(handler, flags) + ?? typeof(ApplicationEngine).GetProperty(handler, flags)?.GetMethod + ?? throw new ArgumentException($"Handler {handler} is not found.", nameof(handler)); + var descriptor = new InteropDescriptor() + { + Name = name, + Handler = method, + Hardfork = hardfork, + FixedPrice = fixedPrice, + RequiredCallFlags = requiredCallFlags + }; + services ??= []; + services.Add(descriptor.Hash, descriptor); + return descriptor; + } + + /// + /// Get Interop Descriptor + /// + /// Method Hash + /// InteropDescriptor + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static InteropDescriptor GetInteropDescriptor(uint methodHash) + { + return services![methodHash]; + } + + /// + /// Creates a new instance of the class, and use it to run the specified script. + /// + /// The script to be executed. + /// The snapshot used by the engine during execution. + /// The container of the script. + /// The block being persisted. + /// The used by the engine. + /// The initial position of the instruction pointer. + /// The maximum gas, in the unit of datoshi, used in this execution. The execution will fail when the gas is exhausted. + /// The diagnostic to be used by the . + /// The engine instance created. + public static ApplicationEngine Run(ReadOnlyMemory script, DataCache snapshot, IVerifiable? container = null, Block? persistingBlock = null, ProtocolSettings? settings = null, int offset = 0, long gas = TestModeGas, IDiagnostic? diagnostic = null) + { + persistingBlock ??= CreateDummyBlock(snapshot, settings ?? ProtocolSettings.Default); + ApplicationEngine engine = Create(TriggerType.Application, container, snapshot, persistingBlock, settings, gas, diagnostic); + engine.LoadScript(script, initialPosition: offset); + engine.Execute(); + return engine; + } + + public T? GetState() where T : notnull + { + if (states is null) return default; + if (!states.TryGetValue(typeof(T), out object? state)) return default; + return (T)state; + } + + public T GetState(Func factory) where T : notnull + { + if (states is null) + { + T state = factory(); + SetState(state); + return state; + } + else + { + if (!states.TryGetValue(typeof(T), out object? state)) + { + state = factory(); + SetState(state); + } + return (T)state; + } + } + + public void SetState(T state) where T : notnull + { + states ??= new Dictionary(); + states[typeof(T)] = state; + } + + public bool IsHardforkEnabled(Hardfork hardfork) + { + if (ProtocolSettings == null) + return false; + + // Return true if PersistingBlock is null and Hardfork is enabled + if (PersistingBlock is null) + return ProtocolSettings.Hardforks.ContainsKey(hardfork); + + return ProtocolSettings.IsHardforkEnabled(hardfork, PersistingBlock.Index); + } +} diff --git a/src/Neo/SmartContract/BinarySerializer.cs b/src/Neo/SmartContract/BinarySerializer.cs new file mode 100644 index 0000000000..a002819ac5 --- /dev/null +++ b/src/Neo/SmartContract/BinarySerializer.cs @@ -0,0 +1,245 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BinarySerializer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; +using Array = Neo.VM.Types.Array; +using Boolean = Neo.VM.Types.Boolean; +using Buffer = Neo.VM.Types.Buffer; + +namespace Neo.SmartContract; + +/// +/// A binary serializer for . +/// +public static class BinarySerializer +{ + private class ContainerPlaceholder : StackItem + { + public override StackItemType Type { get; } + public int ElementCount { get; } + + public ContainerPlaceholder(StackItemType type, int count) + { + Type = type; + ElementCount = count; + } + + public override bool Equals(StackItem? other) => throw new NotSupportedException(); + + public override int GetHashCode() => throw new NotSupportedException(); + + public override bool GetBoolean() => throw new NotSupportedException(); + } + + /// + /// Deserializes a from byte array. + /// + /// The byte array to parse. + /// The limits for the deserialization. + /// The used by the . + /// The deserialized . + public static StackItem Deserialize(ReadOnlyMemory data, ExecutionEngineLimits limits, IReferenceCounter? referenceCounter = null) + { + MemoryReader reader = new(data); + return Deserialize(ref reader, (uint)Math.Min(data.Length, limits.MaxItemSize), limits.MaxStackSize, referenceCounter); + } + + /// + /// Deserializes a from . + /// + /// The for reading data. + /// The limits for the deserialization. + /// The used by the . + /// The deserialized . + public static StackItem Deserialize(ref MemoryReader reader, ExecutionEngineLimits limits, IReferenceCounter? referenceCounter = null) + { + return Deserialize(ref reader, limits.MaxItemSize, limits.MaxStackSize, referenceCounter); + } + + /// + /// Deserializes a from . + /// + /// The for reading data. + /// The maximum size of the result. + /// The max of items to serialize + /// The used by the . + /// The deserialized . + public static StackItem Deserialize(ref MemoryReader reader, uint maxSize, uint maxItems, IReferenceCounter? referenceCounter = null) + { + Stack deserialized = new(); + var undeserialized = 1; + while (undeserialized-- > 0) + { + var type = (StackItemType)reader.ReadByte(); + switch (type) + { + case StackItemType.Any: + deserialized.Push(StackItem.Null); + break; + case StackItemType.Boolean: + deserialized.Push(reader.ReadBoolean()); + break; + case StackItemType.Integer: + deserialized.Push(new BigInteger(reader.ReadVarMemory(Integer.MaxSize).Span)); + break; + case StackItemType.ByteString: + deserialized.Push(reader.ReadVarMemory((int)maxSize)); + break; + case StackItemType.Buffer: + var memory = reader.ReadVarMemory((int)maxSize); + deserialized.Push(new Buffer(memory.Span)); + break; + case StackItemType.Array: + case StackItemType.Struct: + { + int count = (int)reader.ReadVarInt(maxItems); + deserialized.Push(new ContainerPlaceholder(type, count)); + undeserialized += count; + } + break; + case StackItemType.Map: + { + int count = (int)reader.ReadVarInt(maxItems); + deserialized.Push(new ContainerPlaceholder(type, count)); + undeserialized += count * 2; + } + break; + default: + throw new FormatException($"Invalid StackItemType({type})"); + } + if (deserialized.Count > maxItems) + throw new FormatException($"Deserialized count({deserialized.Count}) is out of range (max:{maxItems})"); + } + + var stackTemp = new Stack(); + while (deserialized.Count > 0) + { + StackItem item = deserialized.Pop(); + if (item is ContainerPlaceholder placeholder) + { + switch (placeholder.Type) + { + case StackItemType.Array: + Array array = new(referenceCounter); + for (int i = 0; i < placeholder.ElementCount; i++) + array.Add(stackTemp.Pop()); + item = array; + break; + case StackItemType.Struct: + Struct @struct = new(referenceCounter); + for (int i = 0; i < placeholder.ElementCount; i++) + @struct.Add(stackTemp.Pop()); + item = @struct; + break; + case StackItemType.Map: + Map map = new(referenceCounter); + for (int i = 0; i < placeholder.ElementCount; i++) + { + var key = stackTemp.Pop(); + var value = stackTemp.Pop(); + map[(PrimitiveType)key] = value; + } + item = map; + break; + } + } + stackTemp.Push(item); + } + return stackTemp.Peek(); + } + + /// + /// Serializes a to byte array. + /// + /// The to be serialized. + /// The used to ensure the limits. + /// The serialized byte array. + public static byte[] Serialize(StackItem item, ExecutionEngineLimits limits) + { + return Serialize(item, limits.MaxItemSize, limits.MaxStackSize); + } + + /// + /// Serializes a to byte array. + /// + /// The to be serialized. + /// The maximum size of the result. + /// The max of items to serialize + /// The serialized byte array. + public static byte[] Serialize(StackItem item, long maxSize, long maxItems) + { + using MemoryStream ms = new(); + using BinaryWriter writer = new(ms, Utility.StrictUTF8, true); + Serialize(writer, item, maxSize, maxItems); + writer.Flush(); + return ms.ToArray(); + } + + /// + /// Serializes a into . + /// + /// The for writing data. + /// The to be serialized. + /// The maximum size of the result. + /// The max of items to serialize + public static void Serialize(BinaryWriter writer, StackItem item, long maxSize, long maxItems) + { + HashSet serialized = new(ReferenceEqualityComparer.Instance); + Stack unserialized = new(); + unserialized.Push(item); + while (unserialized.Count > 0) + { + if (--maxItems < 0) + throw new FormatException("Too many items to serialize"); + item = unserialized.Pop(); + writer.Write((byte)item.Type); + switch (item) + { + case Null _: + break; + case Boolean _: + writer.Write(item.GetBoolean()); + break; + case Integer _: + case ByteString _: + case Buffer _: + writer.WriteVarBytes(item.GetSpan()); + break; + case Array array: + if (!serialized.Add(array)) + throw new NotSupportedException(); + writer.WriteVarInt(array.Count); + for (int i = array.Count - 1; i >= 0; i--) + unserialized.Push(array[i]); + break; + case Map map: + if (!serialized.Add(map)) + throw new NotSupportedException(); + writer.WriteVarInt(map.Count); + foreach (var pair in map.Reverse()) + { + unserialized.Push(pair.Value); + unserialized.Push(pair.Key); + } + break; + default: + throw new NotSupportedException(); + } + writer.Flush(); + if (writer.BaseStream.Position > maxSize) + throw new InvalidOperationException(); + } + } +} diff --git a/src/Neo/SmartContract/CallFlags.cs b/src/Neo/SmartContract/CallFlags.cs new file mode 100644 index 0000000000..9fe6eca4ec --- /dev/null +++ b/src/Neo/SmartContract/CallFlags.cs @@ -0,0 +1,59 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CallFlags.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.SmartContract; + +/// +/// Represents the operations allowed when a contract is called. +/// +[Flags] +public enum CallFlags : byte +{ + /// + /// No flag is set. + /// + None = 0, + + /// + /// Indicates that the called contract is allowed to read states. + /// + ReadStates = 0b00000001, + + /// + /// Indicates that the called contract is allowed to write states. + /// + WriteStates = 0b00000010, + + /// + /// Indicates that the called contract is allowed to call another contract. + /// + AllowCall = 0b00000100, + + /// + /// Indicates that the called contract is allowed to send notifications. + /// + AllowNotify = 0b00001000, + + /// + /// Indicates that the called contract is allowed to read or write states. + /// + States = ReadStates | WriteStates, + + /// + /// Indicates that the called contract is allowed to read states or call another contract. + /// + ReadOnly = ReadStates | AllowCall, + + /// + /// All flags are set. + /// + All = States | AllowCall | AllowNotify +} diff --git a/src/Neo/SmartContract/Contract.cs b/src/Neo/SmartContract/Contract.cs new file mode 100644 index 0000000000..59a4744412 --- /dev/null +++ b/src/Neo/SmartContract/Contract.cs @@ -0,0 +1,151 @@ +// Copyright (C) 2015-2025 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. + +using Neo.Cryptography.ECC; +using Neo.VM; + +namespace Neo.SmartContract; + +/// +/// Represents a contract that can be invoked. +/// +public class Contract +{ + /// + /// The script of the contract. + /// + public byte[] Script { get => field ??= []; set; } + + /// + /// The parameters of the contract. + /// + public ContractParameterType[] ParameterList { get => field ?? []; set; } + + private UInt160? _scriptHash; + /// + /// The hash of the contract. + /// + public virtual UInt160 ScriptHash + { + get + { + if (_scriptHash == null) + { + _scriptHash = Script.ToScriptHash(); + } + return _scriptHash; + } + } + + /// + /// Creates a new instance of the class. + /// + /// The parameters of the contract. + /// The script of the contract. + /// The created contract. + public static Contract Create(ContractParameterType[] parameterList, byte[] redeemScript) + { + return new Contract + { + Script = redeemScript, + ParameterList = parameterList + }; + } + + /// + /// Constructs a special contract with empty script, will get the script with scriptHash from blockchain when doing the verification. + /// + /// The hash of the contract. + /// The parameters of the contract. + /// The created contract. + public static Contract Create(UInt160 scriptHash, params ContractParameterType[] parameterList) + { + return new Contract + { + Script = Array.Empty(), + _scriptHash = scriptHash, + ParameterList = parameterList + }; + } + + /// + /// Creates a multi-sig contract. + /// + /// The number of correct signatures that need to be provided in order for the verification to pass. + /// The public keys of the contract. + /// The created contract. + public static Contract CreateMultiSigContract(int m, IReadOnlyCollection publicKeys) + { + return new Contract + { + Script = CreateMultiSigRedeemScript(m, publicKeys), + ParameterList = Enumerable.Repeat(ContractParameterType.Signature, m).ToArray() + }; + } + + /// + /// Creates the script of multi-sig contract. + /// + /// The number of correct signatures that need to be provided in order for the verification to pass. + /// The public keys of the contract. + /// The created script. + public static byte[] CreateMultiSigRedeemScript(int m, IReadOnlyCollection publicKeys) + { + if (!(1 <= m && m <= publicKeys.Count && publicKeys.Count <= 1024)) + throw new ArgumentException($"Invalid multisig parameters: m={m}, publicKeys.Count={publicKeys.Count}"); + using ScriptBuilder sb = new(2 /* m */ + 34 * publicKeys.Count + 8 /* extra space */); + sb.EmitPush(m); + foreach (ECPoint publicKey in publicKeys.OrderBy(p => p)) + { + sb.EmitPush(publicKey.EncodePoint(true)); + } + sb.EmitPush(publicKeys.Count); + sb.EmitSysCall(ApplicationEngine.System_Crypto_CheckMultisig); + return sb.ToArray(); + } + + /// + /// Creates a signature contract. + /// + /// The public key of the contract. + /// The created contract. + public static Contract CreateSignatureContract(ECPoint publicKey) + { + return new Contract + { + Script = CreateSignatureRedeemScript(publicKey), + ParameterList = new[] { ContractParameterType.Signature } + }; + } + + /// + /// Creates the script of signature contract. + /// + /// The public key of the contract. + /// The created script. + public static byte[] CreateSignatureRedeemScript(ECPoint publicKey) + { + using ScriptBuilder sb = new(); + sb.EmitPush(publicKey.EncodePoint(true)); + sb.EmitSysCall(ApplicationEngine.System_Crypto_CheckSig); + return sb.ToArray(); + } + + /// + /// Gets the BFT address for the specified public keys. + /// + /// The public keys to be used. + /// The BFT address. + public static UInt160 GetBFTAddress(IReadOnlyCollection pubkeys) + { + return CreateMultiSigRedeemScript(pubkeys.Count - (pubkeys.Count - 1) / 3, pubkeys).ToScriptHash(); + } +} diff --git a/src/Neo/SmartContract/ContractBasicMethod.cs b/src/Neo/SmartContract/ContractBasicMethod.cs new file mode 100644 index 0000000000..7f9521dfc3 --- /dev/null +++ b/src/Neo/SmartContract/ContractBasicMethod.cs @@ -0,0 +1,121 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractBasicMethod.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.SmartContract; + +/// +/// This class provides a guideline for basic methods used in the Neo blockchain, offering +/// a generalized interaction mechanism for smart contract deployment, verification, updates, and destruction. +/// +public record ContractBasicMethod +{ + /// + /// The verification method. This must be called when withdrawing tokens from the contract. + /// If the contract address is included in the transaction signature, this method verifies the signature. + /// Example: + /// + /// public static bool Verify() => Runtime.CheckWitness(Owner); + /// + /// + /// { + /// "name": "verify", + /// "safe": false, + /// "parameters": [], + /// "returntype": "bool" + /// } + /// + /// + public static string Verify { get; } = "verify"; + + /// + /// The initialization method. Compiled into the file if any function uses the initialize statement. + /// These functions are executed first when loading the contract. + /// Example: + /// + /// private static readonly UInt160 owner = "NdUL5oDPD159KeFpD5A9zw5xNF1xLX6nLT"; + /// + /// + public static string Initialize { get; } = "_initialize"; + + /// + /// The deployment method. Automatically executed by the ContractManagement contract when a contract is first deployed or updated. + /// + /// { + /// "name": "_deploy", + /// "safe": false, + /// "parameters": [ + /// { + /// "name": "data", + /// "type": "Any" + /// }, + /// { + /// "name": "update", + /// "type": "Boolean" + /// } + /// ], + /// "returntype": "Void" + /// } + /// + /// + public static string Deploy { get; } = "_deploy"; + + /// + /// The update method. Requires or , or both, and is passed to _deploy. + /// Should verify the signer's address using SYSCALL Neo.Runtime.CheckWitness. + /// + /// { + /// "name": "update", + /// "safe": false, + /// "parameters": [ + /// { + /// "name": "nefFile", + /// "type": "ByteArray" + /// }, + /// { + /// "name": "manifest", + /// "type": "ByteArray" + /// }, + /// { + /// "name": "data", + /// "type": "Any" + /// } + /// ], + /// "returntype": "Void" + /// } + /// + /// + public static string Update { get; } = "update"; + + /// + /// The destruction method. Deletes all the storage of the contract. + /// Should verify the signer's address using SYSCALL Neo.Runtime.CheckWitness. + /// Any tokens in the contract must be transferred before destruction. + /// + /// { + /// "name": "destroy", + /// "safe": false, + /// "parameters": [], + /// "returntype": "Void" + /// } + /// + /// + public static string Destroy { get; } = "destroy"; + + /// + /// Parameter counts for the methods. + /// -1 represents the method can take arbitrary parameters. + /// + public static int VerifyPCount { get; } = -1; + public static int InitializePCount { get; } = 0; + public static int DeployPCount { get; } = 2; + public static int UpdatePCount { get; } = 3; + public static int DestroyPCount { get; } = 0; +} diff --git a/src/Neo/SmartContract/ContractParameter.cs b/src/Neo/SmartContract/ContractParameter.cs new file mode 100644 index 0000000000..50c77776a6 --- /dev/null +++ b/src/Neo/SmartContract/ContractParameter.cs @@ -0,0 +1,247 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractParameter.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 System.Numerics; +using System.Text; + +namespace Neo.SmartContract; + +/// +/// Represents a parameter of a contract method. +/// +public class ContractParameter +{ + /// + /// The type of the parameter. + /// + public ContractParameterType Type; + + /// + /// The value of the parameter. + /// + public object? Value; + + /// + /// Initializes a new instance of the class. + /// + public ContractParameter() { } + + /// + /// Initializes a new instance of the class with the specified type. + /// + /// The type of the parameter. + public ContractParameter(ContractParameterType type) + { + Type = type; + Value = type switch + { + ContractParameterType.Any => null, + ContractParameterType.Signature => new byte[64], + ContractParameterType.Boolean => false, + ContractParameterType.Integer => 0, + ContractParameterType.Hash160 => new UInt160(), + ContractParameterType.Hash256 => new UInt256(), + ContractParameterType.ByteArray => Array.Empty(), + ContractParameterType.PublicKey => ECCurve.Secp256r1.G, + ContractParameterType.String => "", + ContractParameterType.Array => new List(), + ContractParameterType.Map => new List>(), + _ => throw new ArgumentException($"Parameter type '{type}' is not supported.", nameof(type)), + }; + } + + /// + /// Converts the parameter from a JSON object. + /// + /// The parameter represented by a JSON object. + /// The converted parameter. + public static ContractParameter FromJson(JObject json) + { + ContractParameter parameter = new() + { + Type = Enum.Parse(json["type"]!.GetString()) + }; + if (json["value"] != null) + { + parameter.Value = parameter.Type switch + { + ContractParameterType.Signature or ContractParameterType.ByteArray => Convert.FromBase64String(json["value"]!.AsString()), + ContractParameterType.Boolean => json["value"]!.AsBoolean(), + ContractParameterType.Integer => BigInteger.Parse(json["value"]!.AsString()), + ContractParameterType.Hash160 => UInt160.Parse(json["value"]!.AsString()), + ContractParameterType.Hash256 => UInt256.Parse(json["value"]!.AsString()), + ContractParameterType.PublicKey => ECPoint.Parse(json["value"]!.AsString(), ECCurve.Secp256r1), + ContractParameterType.String => json["value"]!.AsString(), + ContractParameterType.Array => ((JArray)json["value"]!).Select(p => FromJson((JObject)p!)).ToList(), + ContractParameterType.Map => ((JArray)json["value"]!).Select(p => new KeyValuePair(FromJson((JObject)p!["key"]!), FromJson((JObject)p["value"]!))).ToList(), + _ => throw new ArgumentException($"Parameter type '{parameter.Type}' is not supported.", nameof(json)), + }; + } + return parameter; + } + + /// + /// Sets the value of the parameter. + /// + /// The form of the value. + public void SetValue(string text) + { + switch (Type) + { + case ContractParameterType.Signature: + byte[] signature = text.HexToBytes(); + if (signature.Length != 64) throw new FormatException($"Signature length({signature.Length}) is not 64"); + Value = signature; + break; + case ContractParameterType.Boolean: + Value = string.Equals(text, bool.TrueString, StringComparison.OrdinalIgnoreCase); + break; + case ContractParameterType.Integer: + Value = BigInteger.Parse(text); + break; + case ContractParameterType.Hash160: + Value = UInt160.Parse(text); + break; + case ContractParameterType.Hash256: + Value = UInt256.Parse(text); + break; + case ContractParameterType.ByteArray: + Value = text.HexToBytes(); + break; + case ContractParameterType.PublicKey: + Value = ECPoint.Parse(text, ECCurve.Secp256r1); + break; + case ContractParameterType.String: + Value = text; + break; + default: + throw new ArgumentException($"Parameter type '{Type}' is not supported for value setting."); + } + } + + /// + /// Converts the parameter to a JSON object. + /// + /// The parameter represented by a JSON object. + public JObject ToJson() + { + return ToJson(this, null); + } + + private static JObject ToJson(ContractParameter parameter, HashSet? context) + { + JObject json = new(); + json["type"] = parameter.Type; + if (parameter.Value != null) + { + switch (parameter.Type) + { + case ContractParameterType.Signature: + case ContractParameterType.ByteArray: + json["value"] = Convert.ToBase64String((byte[])parameter.Value); + break; + case ContractParameterType.Boolean: + json["value"] = (bool)parameter.Value; + break; + case ContractParameterType.Integer: + case ContractParameterType.Hash160: + case ContractParameterType.Hash256: + case ContractParameterType.PublicKey: + case ContractParameterType.String: + json["value"] = parameter.Value.ToString(); + break; + case ContractParameterType.Array: + context ??= []; + if (!context.Add(parameter)) throw new InvalidOperationException("Circular reference."); + json["value"] = new JArray(((IList)parameter.Value).Select(p => ToJson(p, context))); + if (!context.Remove(parameter)) throw new InvalidOperationException("Circular reference."); + break; + case ContractParameterType.Map: + context ??= []; + if (!context.Add(parameter)) throw new InvalidOperationException("Circular reference."); + json["value"] = new JArray(((IList>)parameter.Value).Select(p => + { + JObject item = new(); + item["key"] = ToJson(p.Key, context); + item["value"] = ToJson(p.Value, context); + return item; + })); + if (!context.Remove(parameter)) throw new InvalidOperationException("Circular reference."); + break; + } + } + return json; + } + + public override string ToString() + { + return ToString(this, null); + } + + private static string ToString(ContractParameter parameter, HashSet? context) + { + switch (parameter.Value) + { + case null: + return "(null)"; + case byte[] data: + return data.ToHexString(); + case IList data: + context ??= new HashSet(); + if (context.Add(parameter)) + { + StringBuilder sb = new(); + sb.Append('['); + foreach (ContractParameter item in data) + { + sb.Append(ToString(item, context)); + sb.Append(", "); + } + if (data.Count > 0) + sb.Length -= 2; + sb.Append(']'); + return sb.ToString(); + } + else + { + return "(array)"; + } + case IList> data: + context ??= new HashSet(); + if (context.Add(parameter)) + { + StringBuilder sb = new(); + sb.Append('['); + foreach (var item in data) + { + sb.Append('{'); + sb.Append(ToString(item.Key, context)); + sb.Append(','); + sb.Append(ToString(item.Value, context)); + sb.Append('}'); + sb.Append(", "); + } + if (data.Count > 0) + sb.Length -= 2; + sb.Append(']'); + return sb.ToString(); + } + else + { + return "(map)"; + } + default: + return parameter.Value.ToString()!; + } + } +} diff --git a/src/Neo/SmartContract/ContractParameterType.cs b/src/Neo/SmartContract/ContractParameterType.cs new file mode 100644 index 0000000000..1892a040bd --- /dev/null +++ b/src/Neo/SmartContract/ContractParameterType.cs @@ -0,0 +1,83 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractParameterType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.SmartContract; + +/// +/// Represents the type of . +/// +public enum ContractParameterType : byte +{ + /// + /// Indicates that the parameter can be of any type. + /// + Any = 0x00, + + /// + /// Indicates that the parameter is of Boolean type. + /// + Boolean = 0x10, + + /// + /// Indicates that the parameter is an integer. + /// + Integer = 0x11, + + /// + /// Indicates that the parameter is a byte array. + /// + ByteArray = 0x12, + + /// + /// Indicates that the parameter is a string. + /// + String = 0x13, + + /// + /// Indicates that the parameter is a 160-bit hash. + /// + Hash160 = 0x14, + + /// + /// Indicates that the parameter is a 256-bit hash. + /// + Hash256 = 0x15, + + /// + /// Indicates that the parameter is a public key. + /// + PublicKey = 0x16, + + /// + /// Indicates that the parameter is a signature. + /// + Signature = 0x17, + + /// + /// Indicates that the parameter is an array. + /// + Array = 0x20, + + /// + /// Indicates that the parameter is a map. + /// + Map = 0x22, + + /// + /// Indicates that the parameter is an interoperable interface. + /// + InteropInterface = 0x30, + + /// + /// It can be only used as the return type of a method, meaning that the method has no return value. + /// + Void = 0xff +} diff --git a/src/Neo/SmartContract/ContractParametersContext.cs b/src/Neo/SmartContract/ContractParametersContext.cs new file mode 100644 index 0000000000..50caa99aa7 --- /dev/null +++ b/src/Neo/SmartContract/ContractParametersContext.cs @@ -0,0 +1,420 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractParametersContext.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.VM; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Native; +using Neo.VM; +using System.Reflection; +using static Neo.SmartContract.Helper; + +namespace Neo.SmartContract; + +/// +/// The context used to add witnesses for . +/// +public class ContractParametersContext +{ + private class ContextItem + { + public readonly byte[]? Script; + public readonly ContractParameter[] Parameters; + public readonly Dictionary Signatures; + + public ContextItem(Contract contract) + { + Script = contract.Script; + Parameters = contract.ParameterList.Select(p => new ContractParameter { Type = p }).ToArray(); + Signatures = new(); + } + + public ContextItem(JObject json) + { + Script = json["script"] is JToken.Null ? null : Convert.FromBase64String(json["script"]!.AsString()); + Parameters = ((JArray)json["parameters"]!).Select(p => ContractParameter.FromJson((JObject)p!)).ToArray(); + Signatures = ((JObject)json["signatures"]!).Properties.Select(p => new + { + PublicKey = ECPoint.Parse(p.Key, ECCurve.Secp256r1), + Signature = Convert.FromBase64String(p.Value!.AsString()) + }).ToDictionary(p => p.PublicKey, p => p.Signature); + } + + public JObject ToJson() + { + JObject json = new(); + json["script"] = Script == null ? null : Convert.ToBase64String(Script); + json["parameters"] = new JArray(Parameters.Select(p => p.ToJson())); + json["signatures"] = new JObject(); + foreach (var signature in Signatures) + json["signatures"]![signature.Key.ToString()] = Convert.ToBase64String(signature.Value); + return json; + } + } + + /// + /// The to add witnesses. + /// + public readonly IVerifiable Verifiable; + + /// + /// The snapshotcache used to read data. + /// + public readonly DataCache SnapshotCache; + + // /// + // /// The snapshot used to read data. + // /// + // public readonly DataCache Snapshot; + + /// + /// The magic number of the network. + /// + public readonly uint Network; + + private readonly Dictionary ContextItems; + + /// + /// Determines whether all witnesses are ready to be added. + /// + public bool Completed + { + get + { + if (ContextItems.Count < ScriptHashes.Count) + return false; + return ContextItems.Values.All(p => p != null && p.Parameters.All(q => q.Value != null)); + } + } + + /// + /// Gets the script hashes to be verified for the . + /// + public IReadOnlyList ScriptHashes => field ??= Verifiable.GetScriptHashesForVerifying(SnapshotCache); + + /// + /// Initializes a new instance of the class. + /// + /// The snapshot used to read data. + /// The to add witnesses. + /// The magic number of the network. + public ContractParametersContext(DataCache snapshotCache, IVerifiable verifiable, uint network) + { + Verifiable = verifiable; + SnapshotCache = snapshotCache; + ContextItems = new(); + Network = network; + } + + /// + /// Adds a parameter to the specified witness script. + /// + /// The contract contains the script. + /// The index of the parameter. + /// The value of the parameter. + /// + /// + /// If the contract script hash not exists in context, return false; otherwise, return true. + /// + public bool Add(Contract contract, int index, object parameter) + { + var item = CreateItem(contract); + if (item == null) return false; + item.Parameters[index].Value = parameter; + return true; + } + + /// + /// Adds parameters to the specified witness script. + /// + /// The contract contains the script. + /// The values of the parameters. + /// + /// If the contract script hash not exists in context, return false; otherwise, return true. + /// + public bool Add(Contract contract, params object[] parameters) + { + var item = CreateItem(contract); + if (item == null) return false; + for (int index = 0; index < parameters.Length; index++) + { + item.Parameters[index].Value = parameters[index]; + } + return true; + } + + /// + /// Adds a signature to the specified witness script. + /// + /// The contract contains the script. + /// The public key for the signature. + /// The signature. + /// + /// Thrown when the contract is single-signature contract and the contract parameters have multiple signatures. + /// + /// + /// If: + /// 1. The contract is a multi-signature contract and the public key is not in the multi-signature contract; + /// 2. The contract script hash not exists in script hash list; + /// 3. The contract parameters are all added; + /// 4. The contract is single-signature contract and the contract parameters haven't signature; + /// It will return false; Otherwise, return true. + /// + public bool AddSignature(Contract contract, ECPoint pubkey, byte[] signature) + { + if (IsMultiSigContract(contract.Script, out _, out ECPoint[]? points)) + { + if (!points.Contains(pubkey)) return false; // the public key not in the multi-signature contract + + var item = CreateItem(contract); + if (item == null) return false; // the contract script hash not exists in context + if (item.Parameters.All(p => p.Value != null)) return false; // the contract parameters are all added + if (!item.Signatures.TryAdd(pubkey, signature)) return false; // already added + + if (item.Signatures.Count == contract.ParameterList.Length) + { + var dic = points.Select((p, i) => new + { + PublicKey = p, + Index = i + }).ToDictionary(p => p.PublicKey, p => p.Index); + + var sigs = item.Signatures.Select(p => new + { + Signature = p.Value, + Index = dic[p.Key] + }).OrderByDescending(p => p.Index).Select(p => p.Signature).ToArray(); + + for (int i = 0; i < sigs.Length; i++) + { + // `Add` should always be true because the line `var item = CreateItem(contract)` + // has already checked the contract script hash exists in context + if (!Add(contract, i, sigs[i])) throw new InvalidOperationException(); + } + } + return true; + } + else + { + int index = -1; + for (int i = 0; i < contract.ParameterList.Length; i++) + { + if (contract.ParameterList[i] == ContractParameterType.Signature) + { + if (index >= 0) throw new NotSupportedException("more than one signature parameter"); + index = i; + } + } + + if (index == -1) + { + // unable to find ContractParameterType.Signature in contract.ParameterList + // return now to prevent array index out of bounds exception + return false; + } + + var item = CreateItem(contract); + if (item == null) return false; // the contract script hash not exists in context + if (!item.Signatures.TryAdd(pubkey, signature)) return false; // already added + + item.Parameters[index].Value = signature; + return true; + } + } + + /// + /// Try to add a deployed contract(get from ContractManagement by scriptHash) to this context. + /// + /// The script hash of the contract. + /// + /// if the contract is added successfully; otherwise, . + /// + public bool AddWithScriptHash(UInt160 scriptHash) + { + // Try Smart contract verification + var contract = NativeContract.ContractManagement.GetContract(SnapshotCache, scriptHash); + if (contract != null) + { + var deployed = new DeployedContract(contract); + + // Only works with verify without parameters + if (deployed.ParameterList.Length == 0) + { + return Add(deployed); + } + } + return false; + } + + private ContextItem? CreateItem(Contract contract) + { + if (ContextItems.TryGetValue(contract.ScriptHash, out var item)) + return item; + if (!ScriptHashes.Contains(contract.ScriptHash)) + return null; + item = new ContextItem(contract); + ContextItems.Add(contract.ScriptHash, item); + return item; + } + + /// + /// Converts the context from a JSON object. + /// + /// The context represented by a JSON object. + /// The snapshot used to read data. + /// The converted context. + public static ContractParametersContext FromJson(JObject json, DataCache snapshot) + { + var typeName = json["type"]!.AsString(); + var type = typeof(ContractParametersContext).GetTypeInfo().Assembly.GetType(typeName); + if (!typeof(IVerifiable).IsAssignableFrom(type)) + throw new FormatException($"json['type']({typeName}) is not an {nameof(IVerifiable)}"); + + var verifiable = (IVerifiable)Activator.CreateInstance(type)!; + var data = Convert.FromBase64String(json["data"]!.AsString()); + var reader = new MemoryReader(data); + + verifiable.DeserializeUnsigned(ref reader); + if (json.ContainsProperty("hash")) + { + var hash = json["hash"]!.GetString(); + var h256 = UInt256.Parse(hash); + if (h256 != verifiable.Hash) throw new FormatException($"json['hash']({hash}) != {verifiable.Hash}"); + } + + var context = new ContractParametersContext(snapshot, verifiable, (uint)json["network"]!.GetInt32()); + foreach (var (key, value) in ((JObject)json["items"]!).Properties) + { + context.ContextItems.Add(UInt160.Parse(key), new ContextItem((JObject)value!)); + } + return context; + } + + /// + /// Gets the parameter with the specified index from the witness script. + /// + /// The hash of the witness script. + /// The specified index. + /// The parameter with the specified index, null if the script hash not exists in context. + public ContractParameter? GetParameter(UInt160 scriptHash, int index) + { + return GetParameters(scriptHash)?[index]; + } + + /// + /// Gets the parameters from the witness script. + /// + /// The hash of the witness script. + /// The parameters from the witness script, null if the script hash not exists in context. + public IReadOnlyList? GetParameters(UInt160 scriptHash) + { + if (!ContextItems.TryGetValue(scriptHash, out var item)) + return null; + return item.Parameters; + } + + /// + /// Gets the signatures from the witness script. + /// + /// The hash of the witness script. + /// The signatures from the witness script. null if the script hash not exists in context. + public IReadOnlyDictionary? GetSignatures(UInt160 scriptHash) + { + if (!ContextItems.TryGetValue(scriptHash, out var item)) + return null; + return item.Signatures; + } + + /// + /// Gets the witness script with the specified hash. + /// + /// The hash of the witness script. + /// The witness script, null if the script hash not exists in context. + public byte[]? GetScript(UInt160 scriptHash) + { + if (!ContextItems.TryGetValue(scriptHash, out var item)) + return null; + return item.Script; + } + + /// + /// Gets the witnesses of the . + /// + /// The witnesses of the . + /// The witnesses are not ready, i.e Completed is false. + public Witness[] GetWitnesses() + { + if (!Completed) throw new InvalidOperationException("Witnesses are not ready"); + + var witnesses = new Witness[ScriptHashes.Count]; + for (int i = 0; i < ScriptHashes.Count; i++) + { + var item = ContextItems[ScriptHashes[i]]; + using var sb = new ScriptBuilder(); + for (int j = item.Parameters.Length - 1; j >= 0; j--) + { + sb.EmitPush(item.Parameters[j]); + } + witnesses[i] = new Witness + { + InvocationScript = sb.ToArray(), + VerificationScript = item.Script ?? ReadOnlyMemory.Empty, + }; + } + return witnesses; + } + + /// + /// Parses the context from a JSON . + /// + /// The JSON . + /// The snapshot used to read data. + /// The parsed context. + public static ContractParametersContext Parse(string value, DataCache snapshot) + { + return FromJson((JObject)JToken.Parse(value)!, snapshot); + } + + /// + /// Converts the context to a JSON object. + /// + /// The context represented by a JSON object. + public JObject ToJson() + { + var json = new JObject() + { + ["type"] = Verifiable.GetType().FullName!, + ["hash"] = Verifiable.Hash.ToString() + }; + + using (var ms = new MemoryStream()) + using (var writer = new BinaryWriter(ms, Utility.StrictUTF8)) + { + Verifiable.SerializeUnsigned(writer); + writer.Flush(); + json["data"] = Convert.ToBase64String(ms.ToArray()); + } + + json["items"] = new JObject(); + foreach (var item in ContextItems) + json["items"]![item.Key.ToString()] = item.Value.ToJson(); + json["network"] = Network; + return json; + } + + public override string ToString() + { + return ToJson().ToString(); + } +} diff --git a/src/Neo/SmartContract/ContractState.cs b/src/Neo/SmartContract/ContractState.cs new file mode 100644 index 0000000000..c8f271a22d --- /dev/null +++ b/src/Neo/SmartContract/ContractState.cs @@ -0,0 +1,112 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Json; +using Neo.SmartContract.Manifest; +using Neo.VM; +using Neo.VM.Types; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract; + +/// +/// Represents a deployed contract. +/// +public class ContractState : IInteroperableVerifiable +{ + /// + /// The id of the contract. + /// + public int Id; + + /// + /// Indicates the number of times the contract has been updated. + /// + public ushort UpdateCounter; + + /// + /// The hash of the contract. + /// + public required UInt160 Hash; + + /// + /// The nef of the contract. + /// + public required NefFile Nef; + + /// + /// The manifest of the contract. + /// + public required ContractManifest Manifest; + + /// + /// The script of the contract. + /// + public ReadOnlyMemory Script => Nef.Script; + + void IInteroperable.FromReplica(IInteroperable replica) + { + var from = (ContractState)replica; + Id = from.Id; + UpdateCounter = from.UpdateCounter; + Hash = from.Hash; + Nef = from.Nef; + Manifest = from.Manifest; + } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + ((IInteroperableVerifiable)this).FromStackItem(stackItem, true); + } + + void IInteroperableVerifiable.FromStackItem(StackItem stackItem, bool verify) + { + var array = (Array)stackItem; + Id = (int)array[0].GetInteger(); + UpdateCounter = (ushort)array[1].GetInteger(); + Hash = new UInt160(array[2].GetSpan()); + Nef = NefFile.Parse(((ByteString)array[3]).Memory, verify); + Manifest = array[4].ToInteroperable(); + } + + /// + /// Determines whether the current contract has the permission to call the specified contract. + /// + /// The contract to be called. + /// The method to be called. + /// if the contract allows to be called; otherwise, . + public bool CanCall(ContractState targetContract, string targetMethod) + { + return Manifest.Permissions.Any(u => u.IsAllowed(targetContract, targetMethod)); + } + + /// + /// Converts the contract to a JSON object. + /// + /// The contract represented by a JSON object. + public JObject ToJson() + { + return new JObject + { + ["id"] = Id, + ["updatecounter"] = UpdateCounter, + ["hash"] = Hash.ToString(), + ["nef"] = Nef.ToJson(), + ["manifest"] = Manifest.ToJson() + }; + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Array(referenceCounter, [Id, (int)UpdateCounter, Hash.ToArray(), Nef.ToArray(), Manifest.ToStackItem(referenceCounter)]); + } +} diff --git a/src/Neo/SmartContract/ContractTask.cs b/src/Neo/SmartContract/ContractTask.cs new file mode 100644 index 0000000000..106d7ab955 --- /dev/null +++ b/src/Neo/SmartContract/ContractTask.cs @@ -0,0 +1,55 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractTask.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Runtime.CompilerServices; + +namespace Neo.SmartContract; + +[AsyncMethodBuilder(typeof(ContractTaskMethodBuilder))] +class ContractTask +{ + protected readonly ContractTaskAwaiter Awaiter; + + public static ContractTask CompletedTask { get; } + + static ContractTask() + { + CompletedTask = new ContractTask(); + CompletedTask.GetAwaiter().SetResult(); + } + + public ContractTask() + { + Awaiter = CreateAwaiter(); + } + + protected virtual ContractTaskAwaiter CreateAwaiter() => new(); + public ContractTaskAwaiter GetAwaiter() => Awaiter; + public virtual object? GetResult() => null; +} + +[AsyncMethodBuilder(typeof(ContractTaskMethodBuilder<>))] +class ContractTask : ContractTask +{ + public new static ContractTask CompletedTask { get; } + + static ContractTask() + { + CompletedTask = new ContractTask(); + CompletedTask.GetAwaiter().SetResult(); + } + + protected override ContractTaskAwaiter CreateAwaiter() => new ContractTaskAwaiter(); +#pragma warning disable CS0108 // Member hides inherited member; missing new keyword + public ContractTaskAwaiter GetAwaiter() => (ContractTaskAwaiter)Awaiter; +#pragma warning restore CS0108 // Member hides inherited member; missing new keyword + public override object? GetResult() => GetAwaiter().GetResult(); +} diff --git a/src/Neo/SmartContract/ContractTaskAwaiter.cs b/src/Neo/SmartContract/ContractTaskAwaiter.cs new file mode 100644 index 0000000000..bac77f3f5d --- /dev/null +++ b/src/Neo/SmartContract/ContractTaskAwaiter.cs @@ -0,0 +1,71 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractTaskAwaiter.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Runtime.CompilerServices; + +namespace Neo.SmartContract; + +internal class ContractTaskAwaiter : INotifyCompletion +{ + private Action? _continuation; + private Exception? _exception; + + public bool IsCompleted { get; private set; } + + public void GetResult() + { + if (_exception is not null) + throw _exception; + } + + public void SetResult() => RunContinuation(); + + public virtual void SetResult(ApplicationEngine engine) => SetResult(); + + public void SetException(Exception exception) + { + _exception = exception; + RunContinuation(); + } + + public void OnCompleted(Action continuation) + { + Interlocked.CompareExchange(ref _continuation, continuation, null); + } + + protected void RunContinuation() + { + IsCompleted = true; + _continuation?.Invoke(); + } +} + +internal class ContractTaskAwaiter : ContractTaskAwaiter +{ + private T? _result; + + public new T? GetResult() + { + base.GetResult(); + return _result; + } + + public void SetResult(T result) + { + _result = result; + RunContinuation(); + } + + public override void SetResult(ApplicationEngine engine) + { + SetResult((T)engine.Convert(engine.Pop(), new InteropParameterDescriptor(typeof(T)))!); + } +} diff --git a/src/Neo/SmartContract/ContractTaskMethodBuilder.cs b/src/Neo/SmartContract/ContractTaskMethodBuilder.cs new file mode 100644 index 0000000000..6f806745cf --- /dev/null +++ b/src/Neo/SmartContract/ContractTaskMethodBuilder.cs @@ -0,0 +1,96 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractTaskMethodBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable CA1822 + +using System.Runtime.CompilerServices; + +namespace Neo.SmartContract; + +sealed class ContractTaskMethodBuilder +{ + public ContractTask Task => field ??= new ContractTask(); + + public static ContractTaskMethodBuilder Create() => new(); + + public void SetException(Exception exception) + { + Task.GetAwaiter().SetException(exception); + } + + public void SetResult() + { + Task.GetAwaiter().SetResult(); + } + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + awaiter.OnCompleted(stateMachine.MoveNext); + } + + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + awaiter.OnCompleted(stateMachine.MoveNext); + } + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + { + stateMachine.MoveNext(); + } + + public void SetStateMachine(IAsyncStateMachine stateMachine) + { + } +} + +sealed class ContractTaskMethodBuilder +{ + public ContractTask Task => field ??= new ContractTask(); + + public static ContractTaskMethodBuilder Create() => new(); + + public void SetException(Exception exception) + { + Task.GetAwaiter().SetException(exception); + } + + public void SetResult(T result) + { + ((ContractTaskAwaiter)Task.GetAwaiter()).SetResult(result); + } + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + awaiter.OnCompleted(stateMachine.MoveNext); + } + + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + awaiter.OnCompleted(stateMachine.MoveNext); + } + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + { + stateMachine.MoveNext(); + } + + public void SetStateMachine(IAsyncStateMachine stateMachine) + { + } +} diff --git a/src/Neo/SmartContract/DeployedContract.cs b/src/Neo/SmartContract/DeployedContract.cs new file mode 100644 index 0000000000..d0fdeedda9 --- /dev/null +++ b/src/Neo/SmartContract/DeployedContract.cs @@ -0,0 +1,34 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// DeployedContract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.SmartContract.Manifest; + +namespace Neo.SmartContract; + +/// +/// Represents a deployed contract that can be invoked. +/// +public class DeployedContract : Contract +{ + public override UInt160 ScriptHash { get; } + + /// + /// Initializes a new instance of the class with the specified . + /// + /// The corresponding to the contract. + public DeployedContract(ContractState contract) + { + ScriptHash = contract.Hash; + ContractMethodDescriptor descriptor = contract.Manifest.Abi.GetMethod(ContractBasicMethod.Verify, ContractBasicMethod.VerifyPCount) + ?? throw new NotSupportedException("The smart contract haven't got verify method."); + ParameterList = descriptor.Parameters.Select(u => u.Type).ToArray(); + } +} diff --git a/src/Neo/SmartContract/ExecutionContextState.cs b/src/Neo/SmartContract/ExecutionContextState.cs new file mode 100644 index 0000000000..ffe18090fc --- /dev/null +++ b/src/Neo/SmartContract/ExecutionContextState.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ExecutionContextState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract; + +/// +/// Represents the custom state in . +/// +public class ExecutionContextState +{ + /// + /// The script hash of the current context. + /// + public UInt160? ScriptHash { get; set; } + + /// + /// The calling context. + /// + public VM.ExecutionContext? CallingContext { get; set; } + + /// + /// The script hash of the calling native contract. Used in native contracts only. + /// + internal UInt160? NativeCallingScriptHash { get; set; } + + /// + /// The of the current context. + /// + public ContractState? Contract { get; set; } + + /// + /// The of the current context. + /// + public CallFlags CallFlags { get; set; } = CallFlags.All; + + public DataCache? SnapshotCache { get; set; } + + public int NotificationCount { get; set; } + + public bool IsDynamicCall { get; set; } + + /// + /// True if the execution is whitelisted by committee + /// + public bool WhiteListed { get; set; } +} diff --git a/src/Neo/SmartContract/FindOptions.cs b/src/Neo/SmartContract/FindOptions.cs new file mode 100644 index 0000000000..d9a85178c0 --- /dev/null +++ b/src/Neo/SmartContract/FindOptions.cs @@ -0,0 +1,64 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FindOptions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.SmartContract; + +/// +/// Specify the options to be used during the search. +/// +[Flags] +public enum FindOptions : byte +{ + /// + /// No option is set. The results will be an iterator of (key, value). + /// + None = 0, + + /// + /// Indicates that only keys need to be returned. The results will be an iterator of keys. + /// + KeysOnly = 1 << 0, + + /// + /// Indicates that the prefix byte of keys should be removed before return. + /// + RemovePrefix = 1 << 1, + + /// + /// Indicates that only values need to be returned. The results will be an iterator of values. + /// + ValuesOnly = 1 << 2, + + /// + /// Indicates that values should be deserialized before return. + /// + DeserializeValues = 1 << 3, + + /// + /// Indicates that only the field 0 of the deserialized values need to be returned. This flag must be set together with . + /// + PickField0 = 1 << 4, + + /// + /// Indicates that only the field 1 of the deserialized values need to be returned. This flag must be set together with . + /// + PickField1 = 1 << 5, + + /// + /// Indicates that results should be returned in backwards (descending) order. + /// + Backwards = 1 << 7, + + /// + /// This value is only for internal use, and shouldn't be used in smart contracts. + /// + All = KeysOnly | RemovePrefix | ValuesOnly | DeserializeValues | PickField0 | PickField1 | Backwards +} diff --git a/src/Neo/SmartContract/Helper.cs b/src/Neo/SmartContract/Helper.cs new file mode 100644 index 0000000000..e12311029c --- /dev/null +++ b/src/Neo/SmartContract/Helper.cs @@ -0,0 +1,375 @@ +// Copyright (C) 2015-2025 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.Cryptography; +using Neo.Cryptography.ECC; +using Neo.Extensions.VM; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Neo.SmartContract; + +/// +/// A helper class related to smart contract. +/// +public static class Helper +{ + /// + /// The maximum GAS that can be consumed when is called. + /// The unit is datoshi, 1 datoshi = 1e-8 GAS + /// + public const long MaxVerificationGas = 1_50000000; + + /// + /// Calculates the verification fee for a signature address. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS + /// + /// The calculated cost. + public static long SignatureContractCost() => + ApplicationEngine.OpCodePriceTable[(byte)OpCode.PUSHDATA1] * 2 + + ApplicationEngine.OpCodePriceTable[(byte)OpCode.SYSCALL] + + ApplicationEngine.CheckSigPrice; + + /// + /// Calculates the verification fee for a multi-signature address. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS + /// + /// The number of correct signatures that need to be provided in order for the verification to pass. + /// The number of public keys in the account. + /// The calculated cost. + public static long MultiSignatureContractCost(int m, int n) + { + long fee = ApplicationEngine.OpCodePriceTable[(byte)OpCode.PUSHDATA1] * (m + n); + using (ScriptBuilder sb = new()) + fee += ApplicationEngine.OpCodePriceTable[(byte)(OpCode)sb.EmitPush(m).ToArray()[0]]; + using (ScriptBuilder sb = new()) + fee += ApplicationEngine.OpCodePriceTable[(byte)(OpCode)sb.EmitPush(n).ToArray()[0]]; + fee += ApplicationEngine.OpCodePriceTable[(byte)OpCode.SYSCALL]; + fee += ApplicationEngine.CheckSigPrice * n; + return fee; + } + + /// + /// Check the correctness of the script and ABI. + /// + /// The script of the contract. + /// The ABI of the contract. + public static void Check(byte[] script, ContractAbi abi) + { + Check(new Script(script, true), abi); + } + + /// + /// Check the correctness of the script and ABI. + /// + /// The script of the contract. + /// The ABI of the contract. + /// Note: The passed to this method should be constructed with strict mode. + public static void Check(this Script script, ContractAbi abi) + { + foreach (ContractMethodDescriptor method in abi.Methods) + script.GetInstruction(method.Offset); + abi.GetMethod(string.Empty, 0); // Trigger the construction of ContractAbi.methodDictionary to check the uniqueness of the method names. + _ = abi.Events.ToDictionary(p => p.Name); // Check the uniqueness of the event names. + } + + /// + /// Computes the hash of a deployed contract. + /// + /// The sender of the transaction that deployed the contract. + /// The checksum of the nef file of the contract. + /// The name of the contract. + /// The hash of the contract. + public static UInt160 GetContractHash(UInt160 sender, uint nefCheckSum, string name) + { + using var sb = new ScriptBuilder(); + sb.Emit(OpCode.ABORT); + sb.EmitPush(sender); + sb.EmitPush(nefCheckSum); + sb.EmitPush(name); + + return sb.ToArray().ToScriptHash(); + } + + /// + /// Gets the script hash of the specified . + /// + /// The specified . + /// The script hash of the context. + public static UInt160 GetScriptHash(this VM.ExecutionContext context) + { + return context.GetState().ScriptHash!; + } + + /// + /// Determines whether the specified contract is a multi-signature contract. + /// + /// The script of the contract. + /// if the contract is a multi-signature contract; otherwise, . + public static bool IsMultiSigContract(ReadOnlySpan script) + { + return IsMultiSigContract(script, out _, out _, null); + } + + /// + /// Determines whether the specified contract is a multi-signature contract. + /// + /// The script of the contract. + /// The number of correct signatures that need to be provided in order for the verification to pass. + /// The number of public keys in the account. + /// if the contract is a multi-signature contract; otherwise, . + public static bool IsMultiSigContract(ReadOnlySpan script, out int m, out int n) + { + return IsMultiSigContract(script, out m, out n, null); + } + + /// + /// Determines whether the specified contract is a multi-signature contract. + /// + /// The script of the contract. + /// The number of correct signatures that need to be provided in order for the verification to pass. + /// The public keys in the account. + /// if the contract is a multi-signature contract; otherwise, . + public static bool IsMultiSigContract(ReadOnlySpan script, out int m, [NotNullWhen(true)] out ECPoint[]? points) + { + List list = new(); + if (IsMultiSigContract(script, out m, out _, list)) + { + points = list.ToArray(); + return true; + } + else + { + points = null; + return false; + } + } + + private static bool IsMultiSigContract(ReadOnlySpan script, out int m, out int n, List? points) + { + m = 0; n = 0; + int i = 0; + if (script.Length < 42) return false; + switch (script[i]) + { + case (byte)OpCode.PUSHINT8: + m = script[++i]; + ++i; + break; + case (byte)OpCode.PUSHINT16: + m = BinaryPrimitives.ReadUInt16LittleEndian(script[++i..]); + i += 2; + break; + case byte b when b >= (byte)OpCode.PUSH1 && b <= (byte)OpCode.PUSH16: + m = b - (byte)OpCode.PUSH0; + ++i; + break; + default: + return false; + } + if (m < 1 || m > 1024) return false; + while (script[i] == (byte)OpCode.PUSHDATA1) + { + if (script.Length <= i + 35) return false; + if (script[++i] != 33) return false; + try + { + points?.Add(ECPoint.DecodePoint(script.Slice(i + 1, 33), ECCurve.Secp256r1)); + } + catch (Exception) // Script may contain any data, thus exceptions are allowed on point decoding. + { + return false; + } + i += 34; + ++n; + } + if (n < m || n > 1024) return false; + switch (script[i]) + { + case (byte)OpCode.PUSHINT8: + if (script.Length <= i + 1 || n != script[++i]) return false; + ++i; + break; + case (byte)OpCode.PUSHINT16: + if (script.Length < i + 3 || n != BinaryPrimitives.ReadUInt16LittleEndian(script[++i..])) return false; + i += 2; + break; + case byte b when b >= (byte)OpCode.PUSH1 && b <= (byte)OpCode.PUSH16: + if (n != b - (byte)OpCode.PUSH0) return false; + ++i; + break; + default: + return false; + } + if (script.Length != i + 5) return false; + if (script[i++] != (byte)OpCode.SYSCALL) return false; + if (BinaryPrimitives.ReadUInt32LittleEndian(script[i..]) != ApplicationEngine.System_Crypto_CheckMultisig) + return false; + return true; + } + + /// + /// Determines whether the specified contract is a signature contract. + /// + /// The script of the contract. + /// if the contract is a signature contract; otherwise, . + public static bool IsSignatureContract(ReadOnlySpan script) + { + if (script.Length != 40) return false; + if (script[0] != (byte)OpCode.PUSHDATA1 + || script[1] != 33 + || script[35] != (byte)OpCode.SYSCALL + || BinaryPrimitives.ReadUInt32LittleEndian(script[36..]) != ApplicationEngine.System_Crypto_CheckSig) + return false; + return true; + } + + /// + /// Determines whether the specified contract is a standard contract. A standard contract is either a signature contract or a multi-signature contract. + /// + /// The script of the contract. + /// if the contract is a standard contract; otherwise, . + public static bool IsStandardContract(ReadOnlySpan script) + { + return IsSignatureContract(script) || IsMultiSigContract(script); + } + + /// + /// Convert the to an . + /// + /// The type of the . + /// The to convert. + /// The converted . + public static T ToInteroperable(this StackItem item) where T : IInteroperable + { + T t = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); + t.FromStackItem(item); + return t; + } + + /// + /// Computes the hash of the specified script. + /// + /// The specified script. + /// The hash of the script. + public static UInt160 ToScriptHash(this byte[] script) + { + return new UInt160(Crypto.Hash160(script)); + } + + /// + /// Computes the hash of the specified script. + /// + /// The specified script. + /// The hash of the script. + public static UInt160 ToScriptHash(this ReadOnlySpan script) + { + return new UInt160(Crypto.Hash160(script)); + } + + /// + /// Verifies the witnesses of the specified . + /// + /// The to be verified. + /// The to be used for the verification. + /// The snapshot used to read data. + /// The maximum GAS that can be used, in the unit of datoshi, 1 datoshi = 1e-8 GAS. + /// if the is verified as valid; otherwise, . + public static bool VerifyWitnesses(this IVerifiable verifiable, ProtocolSettings settings, DataCache snapshot, long datoshi) + { + if (datoshi < 0) return false; + if (datoshi > MaxVerificationGas) datoshi = MaxVerificationGas; + + UInt160[] hashes; + try + { + hashes = verifiable.GetScriptHashesForVerifying(snapshot); + } + catch (InvalidOperationException) + { + return false; + } + if (verifiable.Witnesses == null) return false; + if (hashes.Length != verifiable.Witnesses.Length) return false; + for (int i = 0; i < hashes.Length; i++) + { + if (!verifiable.VerifyWitness(settings, snapshot, hashes[i], verifiable.Witnesses[i], datoshi, out long fee)) + return false; + datoshi -= fee; + } + return true; + } + + internal static bool VerifyWitness(this IVerifiable verifiable, ProtocolSettings settings, DataCache snapshot, UInt160 hash, Witness witness, long datoshi, out long fee) + { + fee = 0; + Script invocationScript; + try + { + invocationScript = new Script(witness.InvocationScript, true); + } + catch (BadScriptException) + { + return false; + } + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, verifiable, snapshot.CloneCache(), null, settings, datoshi); + if (witness.VerificationScript.Length == 0) + { + if (snapshot is null) throw new ArgumentNullException(nameof(snapshot), "Snapshot cannot be null when verification script is empty."); + ContractState? cs = NativeContract.ContractManagement.GetContract(snapshot, hash); + if (cs is null) return false; + ContractMethodDescriptor? md = cs.Manifest.Abi.GetMethod(ContractBasicMethod.Verify, ContractBasicMethod.VerifyPCount); + if (md?.ReturnType != ContractParameterType.Boolean) return false; + engine.LoadContract(cs, md, CallFlags.ReadOnly); + } + else + { + if (NativeContract.IsNative(hash)) return false; + if (hash != witness.ScriptHash) return false; + Script verificationScript; + try + { + verificationScript = new Script(witness.VerificationScript, true); + } + catch (BadScriptException) + { + return false; + } + engine.LoadScript(verificationScript, initialPosition: 0, configureState: p => + { + p.CallFlags = CallFlags.ReadOnly; + p.ScriptHash = hash; + }); + } + + engine.LoadScript(invocationScript, configureState: p => p.CallFlags = CallFlags.None); + + if (engine.Execute() == VMState.FAULT) return false; + if (engine.ResultStack.Count != 1) return false; + try + { + if (!engine.ResultStack.Peek().GetBoolean()) return false; + } + catch + { + return false; + } + fee = engine.FeeConsumed; + return true; + } +} diff --git a/src/Neo/SmartContract/IApplicationEngineProvider.cs b/src/Neo/SmartContract/IApplicationEngineProvider.cs new file mode 100644 index 0000000000..6bdae38147 --- /dev/null +++ b/src/Neo/SmartContract/IApplicationEngineProvider.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IApplicationEngineProvider.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.VM; + +namespace Neo.SmartContract; + +/// +/// A provider for creating instances. +/// +public interface IApplicationEngineProvider +{ + /// + /// Creates a new instance of the class or its subclass. This method will be called by . + /// + /// The trigger of the execution. + /// The container of the script. + /// The snapshot used by the engine during execution. + /// The block being persisted. It should be if the is . + /// The used by the engine. + /// The maximum gas used in this execution. The execution will fail when the gas is exhausted. + /// The diagnostic to be used by the . + /// The jump table to be used by the . + /// The engine instance created. + ApplicationEngine Create(TriggerType trigger, IVerifiable? container, DataCache snapshot, Block? persistingBlock, ProtocolSettings settings, long gas, IDiagnostic? diagnostic, JumpTable jumpTable); +} diff --git a/src/Neo/SmartContract/IDiagnostic.cs b/src/Neo/SmartContract/IDiagnostic.cs new file mode 100644 index 0000000000..a1bf54dc5d --- /dev/null +++ b/src/Neo/SmartContract/IDiagnostic.cs @@ -0,0 +1,24 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IDiagnostic.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; + +namespace Neo.SmartContract; + +public interface IDiagnostic +{ + void Initialized(ApplicationEngine engine); + void Disposed(); + void ContextLoaded(VM.ExecutionContext context); + void ContextUnloaded(VM.ExecutionContext context); + void PreExecuteInstruction(Instruction instruction); + void PostExecuteInstruction(Instruction instruction); +} diff --git a/src/Neo/SmartContract/IInteroperable.cs b/src/Neo/SmartContract/IInteroperable.cs new file mode 100644 index 0000000000..f7eb0e637a --- /dev/null +++ b/src/Neo/SmartContract/IInteroperable.cs @@ -0,0 +1,46 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IInteroperable.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.SmartContract; + +/// +/// Represents the object that can be converted to and from . +/// +public interface IInteroperable +{ + /// + /// Convert a to the current object. + /// + /// The to convert. + void FromStackItem(StackItem stackItem); + + /// + /// Convert the current object to a . + /// + /// The used by the . + /// The converted . + StackItem ToStackItem(IReferenceCounter? referenceCounter); + + public IInteroperable Clone() + { + var result = (IInteroperable)Activator.CreateInstance(GetType())!; + result.FromStackItem(ToStackItem(null)); + return result; + } + + public void FromReplica(IInteroperable replica) + { + FromStackItem(replica.ToStackItem(null)); + } +} diff --git a/src/Neo/SmartContract/IInteroperableVerifiable.cs b/src/Neo/SmartContract/IInteroperableVerifiable.cs new file mode 100644 index 0000000000..029682d786 --- /dev/null +++ b/src/Neo/SmartContract/IInteroperableVerifiable.cs @@ -0,0 +1,28 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IInteroperableVerifiable.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM.Types; + +namespace Neo.SmartContract; + +/// +/// Represents the object that can be converted to and from +/// and allows you to specify whether a verification is required. +/// +public interface IInteroperableVerifiable : IInteroperable +{ + /// + /// Convert a to the current object. + /// + /// The to convert. + /// Verify the content + void FromStackItem(StackItem stackItem, bool verify = true); +} diff --git a/src/Neo/SmartContract/InteropDescriptor.cs b/src/Neo/SmartContract/InteropDescriptor.cs new file mode 100644 index 0000000000..05a07c2758 --- /dev/null +++ b/src/Neo/SmartContract/InteropDescriptor.cs @@ -0,0 +1,72 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// InteropDescriptor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 System.Buffers.Binary; +using System.Reflection; +using System.Text; + +namespace Neo.SmartContract; + +/// +/// Represents a descriptor of an interoperable service. +/// +public record InteropDescriptor +{ + /// + /// The name of the interoperable service. + /// + public required string Name { get; init; } + + private uint _hash; + /// + /// The hash of the interoperable service. + /// + public uint Hash + { + get + { + if (_hash == 0) + _hash = BinaryPrimitives.ReadUInt32LittleEndian(Encoding.ASCII.GetBytes(Name).Sha256()); + return _hash; + } + } + + /// + /// The used to handle the interoperable service. + /// + public required MethodInfo Handler { get; init; } + + /// + /// The parameters of the interoperable service. + /// + public IReadOnlyList Parameters => field ??= Handler.GetParameters().Select(p => new InteropParameterDescriptor(p)).ToList().AsReadOnly(); + + /// + /// The fixed price for calling the interoperable service. It can be 0 if the interoperable service has a variable price. + /// + public long FixedPrice { get; init; } + + /// + /// Required Hardfork to be active. + /// + public Hardfork? Hardfork { get; init; } + + /// + /// The required for the interoperable service. + /// + public CallFlags RequiredCallFlags { get; init; } + + public static implicit operator uint(InteropDescriptor descriptor) + { + return descriptor.Hash; + } +} diff --git a/src/Neo/SmartContract/InteropParameterDescriptor.cs b/src/Neo/SmartContract/InteropParameterDescriptor.cs new file mode 100644 index 0000000000..377d99734f --- /dev/null +++ b/src/Neo/SmartContract/InteropParameterDescriptor.cs @@ -0,0 +1,135 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// InteropParameterDescriptor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.VM.Types; +using System.Numerics; +using System.Reflection; +using Array = Neo.VM.Types.Array; +using Pointer = Neo.VM.Types.Pointer; + +namespace Neo.SmartContract; + +/// +/// Represents a descriptor of an interoperable service parameter. +/// +public class InteropParameterDescriptor +{ + private readonly ValidatorAttribute[] _validators; + + /// + /// The name of the parameter. + /// + public string? Name { get; } + + /// + /// The type of the parameter. + /// + public Type Type { get; } + + /// + /// The converter to convert the parameter from to . + /// + public Func Converter { get; } + + public bool IsNullable { get; } + + public bool IsElementNullable { get; } + + /// + /// Indicates whether the parameter is an enumeration. + /// + public bool IsEnum => Type.IsEnum; + + /// + /// Indicates whether the parameter is an array. + /// + public bool IsArray => Type.IsArray && Type.GetElementType() != typeof(byte); + + /// + /// Indicates whether the parameter is an . + /// + public bool IsInterface { get; } + + private static readonly Dictionary> converters = new() + { + [typeof(StackItem)] = p => p, + [typeof(Pointer)] = p => p, + [typeof(Array)] = p => p, + [typeof(Map)] = p => p, + [typeof(InteropInterface)] = p => p, + [typeof(bool)] = p => p.GetBoolean(), + [typeof(sbyte)] = p => (sbyte)p.GetInteger(), + [typeof(byte)] = p => (byte)p.GetInteger(), + [typeof(short)] = p => (short)p.GetInteger(), + [typeof(ushort)] = p => (ushort)p.GetInteger(), + [typeof(int)] = p => (int)p.GetInteger(), + [typeof(uint)] = p => (uint)p.GetInteger(), + [typeof(long)] = p => (long)p.GetInteger(), + [typeof(ulong)] = p => (ulong)p.GetInteger(), + [typeof(BigInteger)] = p => p.GetInteger(), + [typeof(byte[])] = p => p.IsNull ? null : p.GetSpan().ToArray(), + [typeof(string)] = p => p.IsNull ? null : p.GetString(), + [typeof(UInt160)] = p => p.IsNull ? null : new UInt160(p.GetSpan()), + [typeof(UInt256)] = p => p.IsNull ? null : new UInt256(p.GetSpan()), + [typeof(ECPoint)] = p => p.IsNull ? null : ECPoint.DecodePoint(p.GetSpan(), ECCurve.Secp256r1), + }; + + internal InteropParameterDescriptor(ParameterInfo parameterInfo) + : this(parameterInfo.ParameterType, parameterInfo.GetCustomAttributes(true).ToArray()) + { + Name = parameterInfo.Name; + if (!parameterInfo.ParameterType.IsValueType) + { + var context = new NullabilityInfoContext(); + var info = context.Create(parameterInfo); + if (info.ReadState == NullabilityState.Nullable) + IsNullable = true; + if (info.ElementType?.ReadState == NullabilityState.Nullable) + IsElementNullable = true; + } + } + + internal InteropParameterDescriptor(Type type, params ValidatorAttribute[] validators) + { + Type = type; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + type = type.GenericTypeArguments[0]; + IsNullable = true; + } + _validators = validators; + if (IsEnum) + { + Converter = converters[type.GetEnumUnderlyingType()]; + } + else if (IsArray) + { + Converter = converters[type.GetElementType()!]; + } + else if (converters.TryGetValue(type, out var converter)) + { + IsInterface = false; + Converter = converter; + } + else + { + IsInterface = true; + Converter = converters[typeof(InteropInterface)]; + } + } + + public void Validate(StackItem item) + { + foreach (var validator in _validators) + validator.Validate(item); + } +} diff --git a/src/Neo/SmartContract/Iterators/IIterator.cs b/src/Neo/SmartContract/Iterators/IIterator.cs new file mode 100644 index 0000000000..0be5ffc09d --- /dev/null +++ b/src/Neo/SmartContract/Iterators/IIterator.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IIterator.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.SmartContract.Iterators; + +/// +/// Represents iterators in smart contract. +/// +public interface IIterator : IDisposable +{ + /// + /// Advances the iterator to the next element of the collection. + /// + /// if the iterator was successfully advanced to the next element; if the iterator has passed the end of the collection. + bool Next(); + + /// + /// Gets the element in the collection at the current position of the iterator. + /// + /// The element in the collection at the current position of the iterator. + StackItem Value(IReferenceCounter? referenceCounter); +} diff --git a/src/Neo/SmartContract/Iterators/StorageIterator.cs b/src/Neo/SmartContract/Iterators/StorageIterator.cs new file mode 100644 index 0000000000..e308906e9b --- /dev/null +++ b/src/Neo/SmartContract/Iterators/StorageIterator.cs @@ -0,0 +1,64 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StorageIterator.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; +using Neo.VM.Types; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Iterators; + +internal class StorageIterator : IIterator +{ + private readonly IEnumerator<(StorageKey Key, StorageItem Value)> enumerator; + private readonly int prefixLength; + private readonly FindOptions options; + + public StorageIterator(IEnumerator<(StorageKey, StorageItem)> enumerator, int prefixLength, FindOptions options) + { + this.enumerator = enumerator; + this.prefixLength = prefixLength; + this.options = options; + } + + public void Dispose() + { + enumerator.Dispose(); + } + + public bool Next() + { + return enumerator.MoveNext(); + } + + public StackItem Value(IReferenceCounter? referenceCounter) + { + ReadOnlyMemory key = enumerator.Current.Key.Key; + ReadOnlyMemory value = enumerator.Current.Value.Value; + + if (options.HasFlag(FindOptions.RemovePrefix)) + key = key[prefixLength..]; + + StackItem item = options.HasFlag(FindOptions.DeserializeValues) + ? BinarySerializer.Deserialize(value, ExecutionEngineLimits.Default, referenceCounter) + : value; + + if (options.HasFlag(FindOptions.PickField0)) + item = ((Array)item)[0]; + else if (options.HasFlag(FindOptions.PickField1)) + item = ((Array)item)[1]; + + if (options.HasFlag(FindOptions.KeysOnly)) + return key; + if (options.HasFlag(FindOptions.ValuesOnly)) + return item; + return new Struct(referenceCounter) { key, item }; + } +} diff --git a/src/Neo/SmartContract/JsonSerializer.cs b/src/Neo/SmartContract/JsonSerializer.cs new file mode 100644 index 0000000000..ee86891439 --- /dev/null +++ b/src/Neo/SmartContract/JsonSerializer.cs @@ -0,0 +1,165 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// JsonSerializer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Collections; +using System.Globalization; +using System.Numerics; +using System.Text.Json; +using Array = Neo.VM.Types.Array; +using Boolean = Neo.VM.Types.Boolean; +using Buffer = Neo.VM.Types.Buffer; + +namespace Neo.SmartContract; + +/// +/// A JSON serializer for . +/// +public static class JsonSerializer +{ + /// + /// Serializes a to JSON. + /// + /// The to convert. + /// The maximum size of the JSON output. + /// A byte array containing the JSON output. + public static byte[] SerializeToByteArray(StackItem item, uint maxSize) + { + using MemoryStream ms = new(); + using Utf8JsonWriter writer = new(ms, new JsonWriterOptions + { + Indented = false, + SkipValidation = false + }); + Stack stack = new(); + stack.Push(item); + while (stack.Count > 0) + { + switch (stack.Pop()) + { + case Array array: + writer.WriteStartArray(); + stack.Push(JsonTokenType.EndArray); + for (int i = array.Count - 1; i >= 0; i--) + stack.Push(array[i]); + break; + case JsonTokenType.EndArray: + writer.WriteEndArray(); + break; + case StackItem buffer when buffer is ByteString || buffer is Buffer: + writer.WriteStringValue(buffer.GetString()); + break; + case Integer num: + { + var integer = num.GetInteger(); + if (integer > JNumber.MAX_SAFE_INTEGER || integer < JNumber.MIN_SAFE_INTEGER) + throw new InvalidOperationException(); + writer.WriteNumberValue((double)integer); + break; + } + case Boolean boolean: + writer.WriteBooleanValue(boolean.GetBoolean()); + break; + case Map map: + writer.WriteStartObject(); + stack.Push(JsonTokenType.EndObject); + foreach (var pair in map.Reverse()) + { + if (pair.Key is not ByteString) throw new FormatException("Key is not a ByteString"); + stack.Push(pair.Value); + stack.Push(pair.Key); + stack.Push(JsonTokenType.PropertyName); + } + break; + case JsonTokenType.EndObject: + writer.WriteEndObject(); + break; + case JsonTokenType.PropertyName: + writer.WritePropertyName(((StackItem)stack.Pop()!).GetString()!); + break; + case Null _: + writer.WriteNullValue(); + break; + default: + throw new InvalidOperationException("Invalid StackItemType"); + } + if (ms.Position + writer.BytesPending > maxSize) throw new InvalidOperationException(); + } + writer.Flush(); + if (ms.Position > maxSize) throw new InvalidOperationException(); + return ms.ToArray(); + } + + /// + /// Deserializes a from . + /// + /// The used. + /// The to deserialize. + /// The limits for the deserialization. + /// The used by the . + /// The deserialized . + public static StackItem Deserialize(ApplicationEngine engine, JToken json, ExecutionEngineLimits limits, IReferenceCounter? referenceCounter = null) + { + uint maxStackSize = limits.MaxStackSize; + return Deserialize(engine, json, ref maxStackSize, referenceCounter); + } + + private static StackItem Deserialize(ApplicationEngine engine, JToken? json, ref uint maxStackSize, IReferenceCounter? referenceCounter) + { + if (maxStackSize-- == 0) throw new FormatException("Max stack size reached"); + switch (json) + { + case null: + { + return StackItem.Null; + } + case JArray array: + { + List list = new(array.Count); + foreach (JToken? obj in array) + list.Add(Deserialize(engine, obj, ref maxStackSize, referenceCounter)); + return new Array(referenceCounter, list); + } + case JString str: + { + return str.Value; + } + case JNumber num: + { + if ((num.Value % 1) != 0) throw new FormatException("Decimal value is not allowed"); + return BigInteger.Parse(num.Value.ToString(CultureInfo.InvariantCulture), NumberStyles.Float, CultureInfo.InvariantCulture); + } + case JBoolean boolean: + { + return boolean.Value ? StackItem.True : StackItem.False; + } + case JObject obj: + { + var item = new Map(referenceCounter); + + foreach (var entry in obj.Properties) + { + if (maxStackSize-- == 0) throw new FormatException("Max stack size reached"); + + var key = entry.Key; + var value = Deserialize(engine, entry.Value, ref maxStackSize, referenceCounter); + + item[key] = value; + } + + return item; + } + default: throw new FormatException($"Invalid JTokenType({json.GetType()})"); + } + } +} diff --git a/src/Neo/SmartContract/KeyBuilder.cs b/src/Neo/SmartContract/KeyBuilder.cs new file mode 100644 index 0000000000..76cc95b107 --- /dev/null +++ b/src/Neo/SmartContract/KeyBuilder.cs @@ -0,0 +1,115 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// KeyBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Buffers.Binary; +using System.Collections; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Neo.SmartContract; + +/// +/// Used to build storage keys for native contracts. +/// +public class KeyBuilder : IEnumerable +{ + public const int PrefixLength = sizeof(int) + sizeof(byte); + + private readonly byte[] _cacheData; + private int _keyLength; + + /// + /// Initializes a new instance of the class. + /// + /// The id of the contract. + /// The prefix of the key. + public KeyBuilder(int id, byte prefix) + { + _cacheData = new byte[sizeof(int) + ApplicationEngine.MaxStorageKeySize]; + BinaryPrimitives.WriteInt32LittleEndian(_cacheData, id); + _keyLength = sizeof(int); + _cacheData[_keyLength++] = prefix; + } + + IEnumerator IEnumerable.GetEnumerator() => throw new NotSupportedException(); + + private void CheckLength(int length) + { + if ((length + _keyLength) > _cacheData.Length) + throw new OverflowException("Input data too Large!"); + } + + /// + /// Adds part of the key to the builder. + /// + /// Part of the key. + /// A reference to this instance after the add operation has completed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public KeyBuilder Add(byte key) + { + CheckLength(1); + _cacheData[_keyLength++] = key; + return this; + } + + /// + /// Adds part of the key to the builder. + /// + /// Part of the key. + /// A reference to this instance after the add operation has completed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public KeyBuilder Add(ReadOnlySpan key) + { + CheckLength(key.Length); + key.CopyTo(_cacheData.AsSpan(_keyLength..)); + _keyLength += key.Length; + return this; + } + + /// + /// Adds part of the key to the builder. + /// + /// Part of the key represented by a byte array. + /// A reference to this instance after the add operation has completed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public KeyBuilder Add(byte[] key) => Add(key.AsSpan()); + + /// + /// Adds part of the key to the builder. + /// + /// Part of the key. + /// A reference to this instance after the add operation has completed. + public KeyBuilder Add(ISerializableSpan key) => Add(key.GetSpan()); + + /// + /// Adds part of the key to the builder in BigEndian. + /// + /// Part of the key. + /// A reference to this instance after the add operation has completed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public KeyBuilder Add(T key) where T : unmanaged + { + if (!typeof(T).IsPrimitive) + throw new InvalidOperationException("The argument must be a primitive."); + Span data = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref key, 1)); + if (BitConverter.IsLittleEndian) data.Reverse(); + return Add(data); + } + + /// + /// Gets the storage key generated by the builder. + /// + /// The storage key. + public byte[] ToArray() => _cacheData[.._keyLength]; + + public static implicit operator StorageKey(KeyBuilder builder) => new(builder._cacheData.AsMemory(0, builder._keyLength), false); +} diff --git a/src/Neo/SmartContract/LengthAttribute.cs b/src/Neo/SmartContract/LengthAttribute.cs new file mode 100644 index 0000000000..84513d980b --- /dev/null +++ b/src/Neo/SmartContract/LengthAttribute.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// LengthAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM.Types; + +namespace Neo.SmartContract; + +class LengthAttribute : ValidatorAttribute +{ + public int MinLength { get; } + public int MaxLength { get; } + + public LengthAttribute(int maxLength) + { + ArgumentOutOfRangeException.ThrowIfNegative(maxLength); + MaxLength = maxLength; + } + + public LengthAttribute(int minLength, int maxLength) : this(maxLength) + { + ArgumentOutOfRangeException.ThrowIfNegative(minLength); + ArgumentOutOfRangeException.ThrowIfGreaterThan(minLength, maxLength); + MinLength = minLength; + } + + public override void Validate(StackItem item) + { + int length = item.GetSpan().Length; + if (length < MinLength || length > MaxLength) + throw new InvalidOperationException("The length of the input data is out of range."); + } +} diff --git a/src/Neo/SmartContract/LogEventArgs.cs b/src/Neo/SmartContract/LogEventArgs.cs new file mode 100644 index 0000000000..ebaab48c88 --- /dev/null +++ b/src/Neo/SmartContract/LogEventArgs.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// LogEventArgs.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P.Payloads; + +namespace Neo.SmartContract; + +/// +/// The of . +/// +public class LogEventArgs : EventArgs +{ + /// + /// The container that containing the executed script. + /// + public IVerifiable? ScriptContainer { get; } + + /// + /// The script hash of the contract that sends the log. + /// + public UInt160 ScriptHash { get; } + + /// + /// The message of the log. + /// + public string Message { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The container that containing the executed script. + /// The script hash of the contract that sends the log. + /// The message of the log. + public LogEventArgs(IVerifiable? container, UInt160 scriptHash, string message) + { + ScriptContainer = container; + ScriptHash = scriptHash; + Message = message; + } +} diff --git a/src/Neo/SmartContract/Manifest/ContractAbi.cs b/src/Neo/SmartContract/Manifest/ContractAbi.cs new file mode 100644 index 0000000000..9cb20e40e4 --- /dev/null +++ b/src/Neo/SmartContract/Manifest/ContractAbi.cs @@ -0,0 +1,109 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractAbi.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Manifest; + +/// +/// Represents the ABI of a smart contract. +/// +/// For more details, see NEP-14. +public class ContractAbi : IInteroperable +{ + private Dictionary<(string, int), ContractMethodDescriptor>? methodDictionary; + + /// + /// Gets the methods in the ABI. + /// + public required ContractMethodDescriptor[] Methods { get; set; } + + /// + /// Gets the events in the ABI. + /// + public required ContractEventDescriptor[] Events { get; set; } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + Methods = ((Array)@struct[0]).Select(p => p.ToInteroperable()).ToArray(); + Events = ((Array)@struct[1]).Select(p => p.ToInteroperable()).ToArray(); + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) + { + new Array(referenceCounter, Methods.Select(p => p.ToStackItem(referenceCounter))), + new Array(referenceCounter, Events.Select(p => p.ToStackItem(referenceCounter))), + }; + } + + /// + /// Converts the ABI from a JSON object. + /// + /// The ABI represented by a JSON object. + /// The converted ABI. + public static ContractAbi FromJson(JObject json) + { + ContractAbi abi = new() + { + Methods = ((JArray?)json["methods"])?.Select(u => ContractMethodDescriptor.FromJson((JObject)u!)).ToArray() ?? [], + Events = ((JArray?)json["events"])?.Select(u => ContractEventDescriptor.FromJson((JObject)u!)).ToArray() ?? [] + }; + if (abi.Methods.Length == 0) throw new FormatException("Methods in ContractAbi is empty"); + return abi; + } + + /// + /// Gets the method with the specified name. + /// + /// The name of the method. + /// + /// The number of parameters of the method. + /// It can be set to -1 to search for the method with the specified name and any number of parameters. + /// + /// + /// The method that matches the specified name and number of parameters. + /// If is set to -1, the first method with the specified name will be returned. + /// + public ContractMethodDescriptor? GetMethod(string name, int pcount) + { + if (pcount < -1 || pcount > ushort.MaxValue) + throw new ArgumentOutOfRangeException(nameof(pcount), $"`pcount` must be between [-1, {ushort.MaxValue}]"); + if (pcount >= 0) + { + methodDictionary ??= Methods.ToDictionary(p => (p.Name, p.Parameters.Length)); + methodDictionary.TryGetValue((name, pcount), out var method); + return method; + } + else + { + return Methods.FirstOrDefault(p => p.Name == name); + } + } + + /// + /// Converts the ABI to a JSON object. + /// + /// The ABI represented by a JSON object. + public JObject ToJson() + { + return new JObject() + { + ["methods"] = new JArray(Methods.Select(u => u.ToJson()).ToArray()), + ["events"] = new JArray(Events.Select(u => u.ToJson()).ToArray()) + }; + } +} diff --git a/src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs b/src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs new file mode 100644 index 0000000000..19c107b96f --- /dev/null +++ b/src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs @@ -0,0 +1,119 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractEventDescriptor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Runtime.CompilerServices; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Manifest; + +/// +/// Represents an event in a smart contract ABI. +/// +public class ContractEventDescriptor : IInteroperable, IEquatable +{ + /// + /// The name of the event or method. + /// + public required string Name { get; set; } + + /// + /// The parameters of the event or method. + /// + public required ContractParameterDefinition[] Parameters { get; set; } + + public virtual void FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + Name = @struct[0].GetString()!; + Parameters = ((Array)@struct[1]).Select(p => p.ToInteroperable()).ToArray(); + } + + public virtual StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) + { + Name, + new Array(referenceCounter, Parameters.Select(p => p.ToStackItem(referenceCounter))) + }; + } + + /// + /// Converts the event from a JSON object. + /// + /// The event represented by a JSON object. + /// The converted event. + public static ContractEventDescriptor FromJson(JObject json) + { + ContractEventDescriptor descriptor = new() + { + Name = json["name"]!.GetString(), + Parameters = ((JArray)json["parameters"]!).Select(u => ContractParameterDefinition.FromJson((JObject)u!)).ToArray(), + }; + if (string.IsNullOrEmpty(descriptor.Name)) throw new FormatException("Name in ContractEventDescriptor is empty"); + _ = descriptor.Parameters.ToDictionary(p => p.Name); + return descriptor; + } + + /// + /// Converts the event to a JSON object. + /// + /// The event represented by a JSON object. + public virtual JObject ToJson() + { + return new JObject() + { + ["name"] = Name, + ["parameters"] = new JArray(Parameters.Select(u => u.ToJson()).ToArray()) + }; + } + + public bool Equals(ContractEventDescriptor? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + + return Name == other.Name && Parameters.SequenceEqual(other.Parameters); + } + + public override bool Equals(object? other) + { + if (other is not ContractEventDescriptor ev) + return false; + + return Equals(ev); + } + + public override int GetHashCode() + { + return HashCode.Combine(Name, Parameters); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(ContractEventDescriptor left, ContractEventDescriptor right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(ContractEventDescriptor left, ContractEventDescriptor right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/SmartContract/Manifest/ContractGroup.cs b/src/Neo/SmartContract/Manifest/ContractGroup.cs new file mode 100644 index 0000000000..71faad75b5 --- /dev/null +++ b/src/Neo/SmartContract/Manifest/ContractGroup.cs @@ -0,0 +1,89 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractGroup.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography; +using Neo.Cryptography.ECC; +using Neo.Extensions.IO; +using Neo.Json; +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.SmartContract.Manifest; + +/// +/// Represents a set of mutually trusted contracts. +/// A contract will trust and allow any contract in the same group to invoke it, and the user interface will not give any warnings. +/// A group is identified by a public key and must be accompanied by a signature for the contract hash to prove that the contract is indeed included in the group. +/// +public class ContractGroup : IInteroperable +{ + /// + /// The public key of the group. + /// + public required ECPoint PubKey { get; set; } + + /// + /// The signature of the contract hash which can be verified by . + /// + public required byte[] Signature { get; set; } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + PubKey = ECPoint.DecodePoint(@struct[0].GetSpan(), ECCurve.Secp256r1); + Signature = @struct[1].GetSpan().ToArray(); + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { PubKey.ToArray(), Signature }; + } + + /// + /// Converts the group from a JSON object. + /// + /// The group represented by a JSON object. + /// The converted group. + public static ContractGroup FromJson(JObject json) + { + ContractGroup group = new() + { + PubKey = ECPoint.Parse(json["pubkey"]!.GetString(), ECCurve.Secp256r1), + Signature = Convert.FromBase64String(json["signature"]!.GetString()), + }; + if (group.Signature.Length != 64) + throw new FormatException($"Signature length({group.Signature.Length}) is not 64"); + return group; + } + + /// + /// Determines whether the signature in the group is valid. + /// + /// The hash of the contract. + /// if the signature is valid; otherwise, . + public bool IsValid(UInt160 hash) + { + return Crypto.VerifySignature(hash.ToArray(), Signature, PubKey); + } + + /// + /// Converts the group to a JSON object. + /// + /// The group represented by a JSON object. + public JObject ToJson() + { + return new JObject() + { + ["pubkey"] = PubKey.ToString(), + ["signature"] = Convert.ToBase64String(Signature) + }; + } +} diff --git a/src/Neo/SmartContract/Manifest/ContractManifest.cs b/src/Neo/SmartContract/Manifest/ContractManifest.cs new file mode 100644 index 0000000000..df6952c43a --- /dev/null +++ b/src/Neo/SmartContract/Manifest/ContractManifest.cs @@ -0,0 +1,193 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractManifest.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Manifest; + +/// +/// Represents the manifest of a smart contract. +/// When a smart contract is deployed, it must explicitly declare the features and permissions it will use. +/// When it is running, it will be limited by its declared list of features and permissions, and cannot make any behavior beyond the scope of the list. +/// +/// For more details, see NEP-15. +public class ContractManifest : IInteroperable +{ + /// + /// The maximum length of a manifest. + /// + public const int MaxLength = ushort.MaxValue; + + /// + /// The name of the contract. + /// + public required string Name { get; set; } + + /// + /// The groups of the contract. + /// + public required ContractGroup[] Groups { get; set; } + + /// + /// Indicates which standards the contract supports. It can be a list of NEPs. + /// + public required string[] SupportedStandards { get; set; } + + /// + /// The ABI of the contract. + /// + public required ContractAbi Abi { get; set; } + + /// + /// The permissions of the contract. + /// + public required ContractPermission[] Permissions { get; set; } + + /// + /// The trusted contracts and groups of the contract. + /// If a contract is trusted, the user interface will not give any warnings when called by the contract. + /// + public required WildcardContainer Trusts { get; set; } + + /// + /// Custom user data. + /// + public JObject? Extra { get; set; } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + Name = @struct[0].GetString()!; + Groups = ((Array)@struct[1]).Select(p => p.ToInteroperable()).ToArray(); + if (((Map)@struct[2]).Count != 0) + throw new ArgumentException("Features field must be empty", nameof(stackItem)); + + SupportedStandards = ((Array)@struct[3]).Select(p => p.GetString()!).ToArray(); + Abi = @struct[4].ToInteroperable(); + Permissions = ((Array)@struct[5]).Select(p => p.ToInteroperable()).ToArray(); + Trusts = @struct[6] switch + { + Null _ => WildcardContainer.CreateWildcard(), + // Array array when array.Any(p => ((ByteString)p).Size == 0) => WildcardContainer.CreateWildcard(), + Array array => WildcardContainer.Create(array.Select(ContractPermissionDescriptor.Create).ToArray()), + _ => throw new ArgumentException("Trusts field must be null or array", nameof(stackItem)) + }; + Extra = (JObject?)JToken.Parse(@struct[7].GetSpan()); + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) + { + Name, + new Array(referenceCounter, Groups.Select(p => p.ToStackItem(referenceCounter))), + new Map(referenceCounter), + new Array(referenceCounter, SupportedStandards.Select(p => (StackItem)p)), + Abi.ToStackItem(referenceCounter), + new Array(referenceCounter, Permissions.Select(p => p.ToStackItem(referenceCounter))), + Trusts.IsWildcard ? StackItem.Null : new Array(referenceCounter, Trusts.Select(p => p.ToArray()?? StackItem.Null)), + Extra is null ? "null" : Extra.ToByteArray(false) + }; + } + + /// + /// Converts the manifest from a JSON object. + /// + /// The manifest represented by a JSON object. + /// The converted manifest. + public static ContractManifest FromJson(JObject json) + { + ContractManifest manifest = new() + { + Name = json["name"]!.GetString(), + Groups = ((JArray?)json["groups"])?.Select(u => ContractGroup.FromJson((JObject)u!)).ToArray() ?? [], + SupportedStandards = ((JArray?)json["supportedstandards"])?.Select(u => u!.GetString()).ToArray() ?? [], + Abi = ContractAbi.FromJson((JObject)json["abi"]!), + Permissions = ((JArray?)json["permissions"])?.Select(u => ContractPermission.FromJson((JObject)u!)).ToArray() ?? [], + Trusts = WildcardContainer.FromJson(json["trusts"]!, u => ContractPermissionDescriptor.FromJson((JString)u)), + Extra = (JObject?)json["extra"] + }; + + if (string.IsNullOrEmpty(manifest.Name)) + throw new FormatException("Name in ContractManifest is empty"); + _ = manifest.Groups.ToDictionary(p => p.PubKey); + if (json["features"] is not JObject features || features.Count != 0) + throw new FormatException("Features field must be empty"); + if (manifest.SupportedStandards.Any(string.IsNullOrEmpty)) + throw new FormatException("SupportedStandards in ContractManifest has empty string"); + _ = manifest.SupportedStandards.ToDictionary(p => p); + _ = manifest.Permissions.ToDictionary(p => p.Contract); + _ = manifest.Trusts.ToDictionary(p => p); + return manifest; + } + + /// + /// Parse the manifest from a byte array containing JSON data. + /// + /// The byte array containing JSON data. + /// The parsed manifest. + public static ContractManifest Parse(ReadOnlySpan json) + { + if (json.Length > MaxLength) + throw new ArgumentException($"JSON content length {json.Length} exceeds maximum allowed size of {MaxLength} bytes", nameof(json)); + return FromJson((JObject)JToken.Parse(json)!); + } + + /// + /// Parse the manifest from a JSON . + /// + /// The JSON . + /// The parsed manifest. + public static ContractManifest Parse(string json) => Parse(json.ToStrictUtf8Bytes()); + + /// + /// Converts the manifest to a JSON object. + /// + /// The manifest represented by a JSON object. + public JObject ToJson() + { + return new JObject + { + ["name"] = Name, + ["groups"] = Groups.Select(u => u.ToJson()).ToArray(), + ["features"] = new JObject(), + ["supportedstandards"] = SupportedStandards.Select(u => new JString(u)).ToArray(), + ["abi"] = Abi.ToJson(), + ["permissions"] = Permissions.Select(p => p.ToJson()).ToArray(), + ["trusts"] = Trusts.ToJson(p => p.ToJson()), + ["extra"] = Extra + }; + } + + /// + /// Determines whether the manifest is valid. + /// + /// The used for test serialization. + /// The hash of the contract. + /// if the manifest is valid; otherwise, . + public bool IsValid(ExecutionEngineLimits limits, UInt160 hash) + { + // Ensure that is serializable + try + { + _ = BinarySerializer.Serialize(ToStackItem(null), limits); + } + catch + { + return false; + } + // Check groups + return Groups.All(u => u.IsValid(hash)); + } +} diff --git a/src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs b/src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs new file mode 100644 index 0000000000..c634244bd4 --- /dev/null +++ b/src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs @@ -0,0 +1,140 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractMethodDescriptor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Runtime.CompilerServices; + +namespace Neo.SmartContract.Manifest; + +/// +/// Represents a method in a smart contract ABI. +/// +public class ContractMethodDescriptor : ContractEventDescriptor, IEquatable +{ + /// + /// Indicates the return type of the method. It can be any value of . + /// + public ContractParameterType ReturnType { get; set; } + + /// + /// The position of the method in the contract script. + /// + public int Offset { get; set; } + + /// + /// Indicates whether the method is a safe method. + /// If a method is marked as safe, the user interface will not give any warnings when it is called by other contracts. + /// + public bool Safe { get; set; } + + public override void FromStackItem(StackItem stackItem) + { + base.FromStackItem(stackItem); + Struct @struct = (Struct)stackItem; + ReturnType = (ContractParameterType)(byte)@struct[2].GetInteger(); + Offset = (int)@struct[3].GetInteger(); + Safe = @struct[4].GetBoolean(); + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + Struct @struct = (Struct)base.ToStackItem(referenceCounter); + @struct.Add((byte)ReturnType); + @struct.Add(Offset); + @struct.Add(Safe); + return @struct; + } + + /// + /// Converts the method from a JSON object. + /// + /// The method represented by a JSON object. + /// The converted method. + public new static ContractMethodDescriptor FromJson(JObject json) + { + ContractMethodDescriptor descriptor = new() + { + Name = json["name"]!.GetString(), + Parameters = ((JArray)json["parameters"]!).Select(u => ContractParameterDefinition.FromJson((JObject)u!)).ToArray(), + ReturnType = Enum.Parse(json["returntype"]!.GetString()), + Offset = json["offset"]!.GetInt32(), + Safe = json["safe"]!.GetBoolean() + }; + + if (string.IsNullOrEmpty(descriptor.Name)) + throw new FormatException("Name in ContractMethodDescriptor is empty"); + + _ = descriptor.Parameters.ToDictionary(p => p.Name); + + if (!Enum.IsDefined(descriptor.ReturnType)) + throw new FormatException($"ReturnType({descriptor.ReturnType}) in ContractMethodDescriptor is not valid"); + if (descriptor.Offset < 0) + throw new FormatException($"Offset({descriptor.Offset}) in ContractMethodDescriptor is not valid"); + return descriptor; + } + + /// + /// Converts the method to a JSON object. + /// + /// The method represented by a JSON object. + public override JObject ToJson() + { + var json = base.ToJson(); + json["returntype"] = ReturnType.ToString(); + json["offset"] = Offset; + json["safe"] = Safe; + return json; + } + + public bool Equals(ContractMethodDescriptor? other) + { + if (ReferenceEquals(this, other)) return true; + + return + base.Equals(other) && // Already check null + ReturnType == other.ReturnType + && Offset == other.Offset + && Safe == other.Safe; + } + + public override bool Equals(object? other) + { + if (other is not ContractMethodDescriptor ev) + return false; + + return Equals(ev); + } + + public override int GetHashCode() + { + return HashCode.Combine(ReturnType, Offset, Safe, base.GetHashCode()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(ContractMethodDescriptor left, ContractMethodDescriptor right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(ContractMethodDescriptor left, ContractMethodDescriptor right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs b/src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs new file mode 100644 index 0000000000..9d1049e569 --- /dev/null +++ b/src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs @@ -0,0 +1,116 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractParameterDefinition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Runtime.CompilerServices; + +namespace Neo.SmartContract.Manifest; + +/// +/// Represents a parameter of an event or method in ABI. +/// +public class ContractParameterDefinition : IInteroperable, IEquatable +{ + /// + /// The name of the parameter. + /// + public required string Name { get; set; } + + /// + /// The type of the parameter. It can be any value of except . + /// + public ContractParameterType Type { get; set; } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + Name = @struct[0].GetString()!; + Type = (ContractParameterType)(byte)@struct[1].GetInteger(); + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { Name, (byte)Type }; + } + + /// + /// Converts the parameter from a JSON object. + /// + /// The parameter represented by a JSON object. + /// The converted parameter. + public static ContractParameterDefinition FromJson(JObject json) + { + ContractParameterDefinition parameter = new() + { + Name = json["name"]!.GetString(), + Type = Enum.Parse(json["type"]!.GetString()) + }; + if (string.IsNullOrEmpty(parameter.Name)) + throw new FormatException("Name in ContractParameterDefinition is empty"); + if (!Enum.IsDefined(parameter.Type) || parameter.Type == ContractParameterType.Void) + throw new FormatException($"Type({parameter.Type}) in ContractParameterDefinition is not valid"); + return parameter; + } + + /// + /// Converts the parameter to a JSON object. + /// + /// The parameter represented by a JSON object. + public JObject ToJson() + { + return new JObject() + { + ["name"] = Name, + ["type"] = Type.ToString() + }; + } + + public bool Equals(ContractParameterDefinition? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + + return Name == other.Name && Type == other.Type; + } + + public override bool Equals(object? other) + { + if (other is not ContractParameterDefinition parm) + return false; + + return Equals(parm); + } + + public override int GetHashCode() + { + return HashCode.Combine(Name, Type); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(ContractParameterDefinition left, ContractParameterDefinition right) + { + if (left is null || right is null) + return Equals(left, right); + + return left.Equals(right); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(ContractParameterDefinition left, ContractParameterDefinition right) + { + if (left is null || right is null) + return !Equals(left, right); + + return !left.Equals(right); + } +} diff --git a/src/Neo/SmartContract/Manifest/ContractPermission.cs b/src/Neo/SmartContract/Manifest/ContractPermission.cs new file mode 100644 index 0000000000..5e7687e466 --- /dev/null +++ b/src/Neo/SmartContract/Manifest/ContractPermission.cs @@ -0,0 +1,127 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractPermission.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Json; +using Neo.VM; +using Neo.VM.Types; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Manifest; + +/// +/// Represents a permission of a contract. It describes which contracts may be +/// invoked and which methods are called. +/// If a contract invokes a contract or method that is not declared in the manifest +/// at runtime, the invocation will fail. +/// +public class ContractPermission : IInteroperable +{ + /// + /// Indicates which contract to be invoked. + /// It can be a hash of a contract, a public key of a group, or a wildcard *. + /// If it specifies a hash of a contract, then the contract will be invoked; + /// If it specifies a public key of a group, then any contract in this group + /// may be invoked; If it specifies a wildcard *, then any contract may be invoked. + /// + public required ContractPermissionDescriptor Contract { get; set; } + + /// + /// Indicates which methods to be called. + /// It can also be assigned with a wildcard *. If it is a wildcard *, + /// then it means that any method can be called. + /// + public required WildcardContainer Methods { get; set; } + + /// + /// A default permission that both and fields are set to wildcard *. + /// + public static readonly ContractPermission DefaultPermission = new() + { + Contract = ContractPermissionDescriptor.CreateWildcard(), + Methods = WildcardContainer.CreateWildcard() + }; + + void IInteroperable.FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + Contract = @struct[0] switch + { + Null => ContractPermissionDescriptor.CreateWildcard(), + StackItem item => new ContractPermissionDescriptor(item.GetSpan()) + }; + Methods = @struct[1] switch + { + Null => WildcardContainer.CreateWildcard(), + Array array => WildcardContainer.Create(array.Select(p => p.GetString()!).ToArray()), + _ => throw new ArgumentException("Methods field must be null or array", nameof(stackItem)) + }; + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) + { + Contract.IsWildcard ? StackItem.Null : Contract.IsHash ? Contract.Hash.ToArray() : Contract.Group!.ToArray(), + Methods.IsWildcard ? StackItem.Null : new Array(referenceCounter, Methods.Select(p => (StackItem)p)), + }; + } + + /// + /// Converts the permission from a JSON object. + /// + /// The permission represented by a JSON object. + /// The converted permission. + public static ContractPermission FromJson(JObject json) + { + ContractPermission permission = new() + { + Contract = ContractPermissionDescriptor.FromJson((JString)json["contract"]!), + Methods = WildcardContainer.FromJson(json["methods"]!, u => u.GetString()), + }; + if (permission.Methods.Any(p => string.IsNullOrEmpty(p))) + throw new FormatException("Methods in ContractPermission has empty string"); + _ = permission.Methods.ToDictionary(p => p); + return permission; + } + + /// + /// Converts the permission to a JSON object. + /// + /// The permission represented by a JSON object. + public JObject ToJson() + { + return new JObject() + { + ["contract"] = Contract.ToJson(), + ["methods"] = Methods.ToJson(p => p) + }; + } + + /// + /// Determines whether the method of the specified contract can be called by this contract. + /// + /// The contract being called. + /// The method of the specified contract. + /// if the contract allows to be called; otherwise, . + public bool IsAllowed(ContractState targetContract, string targetMethod) + { + if (Contract.IsHash) + { + if (!Contract.Hash.Equals(targetContract.Hash)) return false; + } + else if (Contract.IsGroup) + { + if (targetContract.Manifest.Groups.All(p => !p.PubKey.Equals(Contract.Group))) return false; + } + return Methods.IsWildcard || Methods.Contains(targetMethod); + } +} diff --git a/src/Neo/SmartContract/Manifest/ContractPermissionDescriptor.cs b/src/Neo/SmartContract/Manifest/ContractPermissionDescriptor.cs new file mode 100644 index 0000000000..3b4c369ea5 --- /dev/null +++ b/src/Neo/SmartContract/Manifest/ContractPermissionDescriptor.cs @@ -0,0 +1,164 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractPermissionDescriptor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.Json; +using Neo.VM.Types; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.SmartContract.Manifest; + +/// +/// Indicates which contracts are authorized to be called. +/// +public class ContractPermissionDescriptor : IEquatable +{ + /// + /// The hash of the contract. It can't be set with . + /// + public UInt160? Hash { get; } + + /// + /// The group of the contracts. It can't be set with . + /// + public ECPoint? Group { get; } + + /// + /// Indicates whether is set. + /// + [MemberNotNullWhen(true, nameof(Hash))] + public bool IsHash => Hash != null; + + /// + /// Indicates whether is set. + /// + [MemberNotNullWhen(true, nameof(Group))] + public bool IsGroup => Group != null; + + /// + /// Indicates whether it is a wildcard. + /// + public bool IsWildcard => Hash is null && Group is null; + + private ContractPermissionDescriptor(UInt160? hash, ECPoint? group) + { + Hash = hash; + Group = group; + } + + internal ContractPermissionDescriptor(ReadOnlySpan span) + { + switch (span.Length) + { + case UInt160.Length: + Hash = new UInt160(span); + break; + case 33: + Group = ECPoint.DecodePoint(span, ECCurve.Secp256r1); + break; + default: + throw new ArgumentException($"Invalid span length: {span.Length}", nameof(span)); + } + } + + public static ContractPermissionDescriptor Create(StackItem item) + { + return item.Equals(StackItem.Null) ? CreateWildcard() : new ContractPermissionDescriptor(item.GetSpan()); + } + + /// + /// Creates a new instance of the class with the specified contract hash. + /// + /// The contract to be called. + /// The created permission descriptor. + public static ContractPermissionDescriptor Create(UInt160 hash) + { + return new ContractPermissionDescriptor(hash, null); + } + + /// + /// Creates a new instance of the class with the specified group. + /// + /// The group of the contracts to be called. + /// The created permission descriptor. + public static ContractPermissionDescriptor Create(ECPoint group) + { + return new ContractPermissionDescriptor(null, group); + } + + /// + /// Creates a new instance of the class with wildcard. + /// + /// The created permission descriptor. + public static ContractPermissionDescriptor CreateWildcard() + { + return new ContractPermissionDescriptor(null, null); + } + + public override bool Equals(object? obj) + { + if (obj is not ContractPermissionDescriptor other) return false; + return Equals(other); + } + + public bool Equals(ContractPermissionDescriptor? other) + { + if (other is null) return false; + if (this == other) return true; + if (IsWildcard == other.IsWildcard) return true; + if (IsHash) return Hash.Equals(other.Hash); + if (IsGroup) return Group.Equals(other.Group); + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(Hash, Group); + } + + /// + /// Converts the permission descriptor from a JSON object. + /// + /// The permission descriptor represented by a JSON object. + /// The converted permission descriptor. + public static ContractPermissionDescriptor FromJson(JString json) + { + string str = json.GetString(); + if (str.Length == 42) + return Create(UInt160.Parse(str)); + if (str.Length == 66) + return Create(ECPoint.Parse(str, ECCurve.Secp256r1)); + if (str == "*") + return CreateWildcard(); + throw new FormatException($"Invalid ContractPermissionDescriptor({str})"); + } + + /// + /// Converts the permission descriptor to a JSON object. + /// + /// The permission descriptor represented by a JSON object. + public JString ToJson() + { + if (IsHash) return Hash.ToString(); + if (IsGroup) return Group.ToString(); + return "*"; + } + + /// + /// Converts the permission descriptor to byte array. + /// + /// The converted byte array. Or if it is a wildcard. + public byte[]? ToArray() + { + return Hash?.ToArray() ?? Group?.EncodePoint(true); + } +} diff --git a/src/Neo/SmartContract/Manifest/WildCardContainer.cs b/src/Neo/SmartContract/Manifest/WildCardContainer.cs new file mode 100644 index 0000000000..29292ad1b5 --- /dev/null +++ b/src/Neo/SmartContract/Manifest/WildCardContainer.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WildCardContainer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.SmartContract.Manifest; + +/// +/// A list that supports wildcard. +/// +/// The type of the elements. +public class WildcardContainer : IReadOnlyList +{ + private readonly T[]? _data; + + public T this[int index] => _data![index]; + + public int Count => _data?.Length ?? 0; + + /// + /// Indicates whether the list is a wildcard. + /// + public bool IsWildcard => _data is null; + + private WildcardContainer(T[]? data) + { + _data = data; + } + + /// + /// Creates a new instance of the class with the initial elements. + /// + /// The initial elements. + /// The created list. + public static WildcardContainer Create(params T[] data) => new(data); + + /// + /// Creates a new instance of the class with wildcard. + /// + /// The created list. + public static WildcardContainer CreateWildcard() => new(null); + + /// + /// Converts the list from a JSON object. + /// + /// The list represented by a JSON object. + /// A converter for elements. + /// The converted list. + public static WildcardContainer FromJson(JToken json, Func elementSelector) + { + switch (json) + { + case JString str: + if (str.Value != "*") throw new FormatException($"Invalid wildcard('{str.Value}')"); + return CreateWildcard(); + case JArray array: + return Create(array.Select(p => elementSelector(p!)).ToArray()); + default: + throw new FormatException($"Invalid json type for wildcard({json.GetType()})"); + } + } + + public IEnumerator GetEnumerator() + { + if (_data == null) return ((IReadOnlyList)Array.Empty()).GetEnumerator(); + + return ((IReadOnlyList)_data).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Converts the list to a JSON object. + /// + /// The list represented by a JSON object. + public JToken ToJson(Func elementSelector) + { + if (IsWildcard) return "*"; + return _data!.Select(p => elementSelector(p)).ToArray(); + } +} diff --git a/src/Neo/SmartContract/MethodToken.cs b/src/Neo/SmartContract/MethodToken.cs new file mode 100644 index 0000000000..659a3470d9 --- /dev/null +++ b/src/Neo/SmartContract/MethodToken.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MethodToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; + +namespace Neo.SmartContract; + +/// +/// Represents the methods that a contract will call statically. +/// +public class MethodToken : ISerializable +{ + /// + /// The hash of the contract to be called. + /// + public required UInt160 Hash; + + /// + /// The name of the method to be called. + /// + public required string Method; + + /// + /// The number of parameters of the method to be called. + /// + public ushort ParametersCount; + + /// + /// Indicates whether the method to be called has a return value. + /// + public bool HasReturnValue; + + /// + /// The to be used to call the contract. + /// + public CallFlags CallFlags; + + public int Size => + UInt160.Length + // Hash + Method.GetVarSize() + // Method + sizeof(ushort) + // ParametersCount + sizeof(bool) + // HasReturnValue + sizeof(CallFlags); // CallFlags + + void ISerializable.Deserialize(ref MemoryReader reader) + { + Hash = reader.ReadSerializable(); + Method = reader.ReadVarString(32); + if (Method.StartsWith('_')) throw new FormatException($"Method('{Method}') cannot start with '_'"); + ParametersCount = reader.ReadUInt16(); + HasReturnValue = reader.ReadBoolean(); + CallFlags = (CallFlags)reader.ReadByte(); + if ((CallFlags & ~CallFlags.All) != 0) throw new FormatException($"CallFlags({CallFlags}) is not valid"); + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(Hash); + writer.WriteVarString(Method); + writer.Write(ParametersCount); + writer.Write(HasReturnValue); + writer.Write((byte)CallFlags); + } + + /// + /// Converts the token to a JSON object. + /// + /// The token represented by a JSON object. + public JObject ToJson() + { + return new JObject + { + ["hash"] = Hash.ToString(), + ["method"] = Method, + ["paramcount"] = ParametersCount, + ["hasreturnvalue"] = HasReturnValue, + ["callflags"] = CallFlags + }; + } +} diff --git a/src/Neo/SmartContract/Native/AccountState.cs b/src/Neo/SmartContract/Native/AccountState.cs new file mode 100644 index 0000000000..b947a8827e --- /dev/null +++ b/src/Neo/SmartContract/Native/AccountState.cs @@ -0,0 +1,37 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// AccountState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// The base class of account state for all native tokens. +/// +public class AccountState : IInteroperable +{ + /// + /// The balance of the account. + /// + public BigInteger Balance; + + public virtual void FromStackItem(StackItem stackItem) + { + Balance = ((Struct)stackItem)[0].GetInteger(); + } + + public virtual StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { Balance }; + } +} diff --git a/src/Neo/SmartContract/Native/ContractEventAttribute.cs b/src/Neo/SmartContract/Native/ContractEventAttribute.cs new file mode 100644 index 0000000000..b9cbf129f3 --- /dev/null +++ b/src/Neo/SmartContract/Native/ContractEventAttribute.cs @@ -0,0 +1,134 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractEventAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.SmartContract.Manifest; +using System.Diagnostics; + +namespace Neo.SmartContract.Native; + +[DebuggerDisplay("{Descriptor.Name}")] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +class ContractEventAttribute : Attribute, IHardforkActivable +{ + public int Order { get; } + public ContractEventDescriptor Descriptor { get; } + public Hardfork? ActiveIn { get; init; } + public Hardfork? DeprecatedIn { get; init; } + + public ContractEventAttribute(int order, string name, string arg1Name, ContractParameterType arg1Value) + { + Order = order; + Descriptor = new ContractEventDescriptor() + { + Name = name, + Parameters = + [ + new ContractParameterDefinition() + { + Name = arg1Name, + Type = arg1Value + } + ] + }; + } + + public ContractEventAttribute(int order, string name, + string arg1Name, ContractParameterType arg1Value, + string arg2Name, ContractParameterType arg2Value) + { + Order = order; + Descriptor = new ContractEventDescriptor() + { + Name = name, + Parameters = + [ + new ContractParameterDefinition() + { + Name = arg1Name, + Type = arg1Value + }, + new ContractParameterDefinition() + { + Name = arg2Name, + Type = arg2Value + } + ] + }; + } + + public ContractEventAttribute(int order, string name, + string arg1Name, ContractParameterType arg1Value, + string arg2Name, ContractParameterType arg2Value, + string arg3Name, ContractParameterType arg3Value + ) + { + Order = order; + Descriptor = new ContractEventDescriptor() + { + Name = name, + Parameters = + [ + new ContractParameterDefinition() + { + Name = arg1Name, + Type = arg1Value + }, + new ContractParameterDefinition() + { + Name = arg2Name, + Type = arg2Value + }, + new ContractParameterDefinition() + { + Name = arg3Name, + Type = arg3Value + } + ] + }; + } + + public ContractEventAttribute(int order, string name, + string arg1Name, ContractParameterType arg1Value, + string arg2Name, ContractParameterType arg2Value, + string arg3Name, ContractParameterType arg3Value, + string arg4Name, ContractParameterType arg4Value + ) + { + Order = order; + Descriptor = new ContractEventDescriptor() + { + Name = name, + Parameters = + [ + new ContractParameterDefinition() + { + Name = arg1Name, + Type = arg1Value + }, + new ContractParameterDefinition() + { + Name = arg2Name, + Type = arg2Value + }, + new ContractParameterDefinition() + { + Name = arg3Name, + Type = arg3Value + }, + new ContractParameterDefinition() + { + Name = arg4Name, + Type = arg4Value + } + ] + }; + } +} diff --git a/src/Neo/SmartContract/Native/ContractManagement.cs b/src/Neo/SmartContract/Native/ContractManagement.cs new file mode 100644 index 0000000000..a53ab1889e --- /dev/null +++ b/src/Neo/SmartContract/Native/ContractManagement.cs @@ -0,0 +1,385 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractManagement.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable IDE0051 + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Iterators; +using Neo.SmartContract.Manifest; +using Neo.VM; +using Neo.VM.Types; +using System.Buffers.Binary; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// A native contract used to manage all deployed smart contracts. +/// +[ContractEvent(0, name: "Deploy", "Hash", ContractParameterType.Hash160)] +[ContractEvent(1, name: "Update", "Hash", ContractParameterType.Hash160)] +[ContractEvent(2, name: "Destroy", "Hash", ContractParameterType.Hash160)] +public sealed class ContractManagement : NativeContract +{ + private const byte Prefix_MinimumDeploymentFee = 20; + private const byte Prefix_NextAvailableId = 15; + private const byte Prefix_Contract = 8; + private const byte Prefix_ContractHash = 12; + + internal ContractManagement() : base(-1) { } + + private int GetNextAvailableId(DataCache snapshot) + { + StorageItem item = snapshot.GetAndChange(CreateStorageKey(Prefix_NextAvailableId))!; + int value = (int)(BigInteger)item; + item.Add(1); + return value; + } + + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) + { + if (hardfork == ActiveIn) + { + engine.SnapshotCache.Add(CreateStorageKey(Prefix_MinimumDeploymentFee), new StorageItem(10_00000000)); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_NextAvailableId), new StorageItem(1)); + } + return ContractTask.CompletedTask; + } + + private async ContractTask OnDeployAsync(ApplicationEngine engine, ContractState contract, StackItem data, bool update) + { + ContractMethodDescriptor? md = contract.Manifest.Abi.GetMethod(ContractBasicMethod.Deploy, ContractBasicMethod.DeployPCount); + if (md is not null) + await engine.CallFromNativeContractAsync(Hash, contract.Hash, md.Name, data, update); + Notify(engine, update ? "Update" : "Deploy", contract.Hash); + } + + internal override async ContractTask OnPersistAsync(ApplicationEngine engine) + { + foreach (NativeContract contract in Contracts) + { + if (contract.IsInitializeBlock(engine.ProtocolSettings, engine.PersistingBlock!.Index, out var hfs)) + { + ContractState contractState = contract.GetContractState(engine.ProtocolSettings, engine.PersistingBlock.Index); + StorageItem? state = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Contract, contract.Hash)); + + if (state is null) + { + // Create the contract state + engine.SnapshotCache.Add(CreateStorageKey(Prefix_Contract, contract.Hash), new StorageItem(contractState)); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_ContractHash, contract.Id), new StorageItem(contract.Hash.ToArray())); + + // Initialize the native smart contract if it's active starting from the genesis. + // If it's not the case, then hardfork-based initialization will be performed down below. + if (contract.ActiveIn is null) + { + await contract.InitializeAsync(engine, null); + } + } + else + { + // Parse old contract + using var sealInterop = state.GetInteroperable(out ContractState oldContract, false); + // Increase the update counter + oldContract.UpdateCounter++; + // Modify nef and manifest + oldContract.Nef = contractState.Nef; + oldContract.Manifest = contractState.Manifest; + } + + // Initialize native contract for all hardforks that are active starting from the persisting block. + // If the contract is active starting from some non-nil hardfork, then this hardfork is also included into hfs. + if (hfs?.Length > 0) + { + foreach (var hf in hfs) + { + await contract.InitializeAsync(engine, hf); + } + } + + // Emit native contract notification + Notify(engine, state is null ? "Deploy" : "Update", contract.Hash); + } + } + } + + /// + /// Gets the minimum deployment fee for deploying a contract. + /// + /// The snapshot used to read data. + /// The minimum deployment fee for deploying a contract. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] +#pragma warning disable CA1859 + private long GetMinimumDeploymentFee(IReadOnlyStore snapshot) +#pragma warning restore CA1859 + { + // In the unit of datoshi, 1 datoshi = 1e-8 GAS + return (long)(BigInteger)snapshot[CreateStorageKey(Prefix_MinimumDeploymentFee)]; + } + + /// + /// Sets the minimum deployment fee for deploying a contract. Only committee members can call this method. + /// + /// The engine used to write data. + /// The minimum deployment fee for deploying a contract. + /// Thrown when the caller is not a committee member. + /// Thrown when the value is negative. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetMinimumDeploymentFee(ApplicationEngine engine, BigInteger value/* In the unit of datoshi, 1 datoshi = 1e-8 GAS*/) + { + if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "cannot be negative"); + AssertCommittee(engine); + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_MinimumDeploymentFee))!.Set(value); + } + + /// + /// Gets the deployed contract with the specified hash. + /// + /// The snapshot used to read data. + /// The hash of the deployed contract. + /// The deployed contract. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public ContractState? GetContract(IReadOnlyStore snapshot, UInt160 hash) + { + var key = CreateStorageKey(Prefix_Contract, hash); + return snapshot.TryGet(key, out var item) ? item.GetInteroperable(false) : null; + } + + /// + /// Check if exists the deployed contract with the specified hash. + /// + /// The snapshot used to read data. + /// The hash of the deployed contract. + /// True if deployed contract exists. + [ContractMethod(CpuFee = 1 << 14, RequiredCallFlags = CallFlags.ReadStates)] + public bool IsContract(IReadOnlyStore snapshot, UInt160 hash) + { + var key = CreateStorageKey(Prefix_Contract, hash); + return snapshot.Contains(key); + } + + /// + /// Maps specified ID to deployed contract. + /// + /// The snapshot used to read data. + /// Contract ID. + /// The deployed contract. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public ContractState? GetContractById(IReadOnlyStore snapshot, int id) + { + var key = CreateStorageKey(Prefix_ContractHash, id); + return snapshot.TryGet(key, out var item) ? GetContract(snapshot, new UInt160(item.Value.Span)) : null; + } + + /// + /// Gets hashes of all non native deployed contracts. + /// + /// The snapshot used to read data. + /// Iterator with hashes of all deployed contracts. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + private StorageIterator GetContractHashes(IReadOnlyStore snapshot) + { + const FindOptions options = FindOptions.RemovePrefix; + var prefixKey = CreateStorageKey(Prefix_ContractHash); + var enumerator = snapshot.Find(prefixKey) + .Select(p => (p.Key, p.Value, Id: BinaryPrimitives.ReadInt32BigEndian(p.Key.Key.Span[1..]))) + .Where(p => p.Id >= 0) + .Select(p => (p.Key, p.Value)) + .GetEnumerator(); + return new StorageIterator(enumerator, 1, options); + } + + /// + /// Check if a method exists in a contract. + /// + /// The snapshot used to read data. + /// The hash of the deployed contract. + /// The name of the method + /// The number of parameters + /// True if the method exists. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public bool HasMethod(IReadOnlyStore snapshot, UInt160 hash, string method, int pcount) + { + var contract = GetContract(snapshot, hash); + if (contract is null) return false; + var methodDescriptor = contract.Manifest.Abi.GetMethod(method, pcount); + return methodDescriptor is not null; + } + + /// + /// Gets all deployed contracts. + /// + /// The snapshot used to read data. + /// The deployed contracts. + public IEnumerable ListContracts(IReadOnlyStore snapshot) + { + var listContractsPrefix = CreateStorageKey(Prefix_Contract); + return snapshot.Find(listContractsPrefix).Select(kvp => kvp.Value.GetInteroperableClone(false)); + } + + /// + /// Deploys a contract. It needs to pay the deployment fee and storage fee. + /// + /// The engine used to write data. + /// The NEF file of the contract. + /// The manifest of the contract. + /// The deployed contract. + [ContractMethod(RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private ContractTask Deploy(ApplicationEngine engine, byte[] nefFile, byte[] manifest) + { + return Deploy(engine, nefFile, manifest, StackItem.Null); + } + + /// + /// Deploys a contract. It needs to pay the deployment fee and storage fee. + /// + /// The engine used to write data. + /// The NEF file of the contract. + /// The manifest of the contract. + /// The data of the contract. + /// The deployed contract. + [ContractMethod(RequiredCallFlags = CallFlags.All)] + private async ContractTask Deploy(ApplicationEngine engine, byte[] nefFile, byte[] manifest, StackItem data) + { + if (engine.ScriptContainer is not Transaction tx) + throw new InvalidOperationException(); + if (nefFile.Length == 0) + throw new ArgumentException($"NEF file length cannot be zero."); + if (manifest.Length == 0) + throw new ArgumentException($"Manifest length cannot be zero."); + + engine.AddFee(Math.Max( + engine.StoragePrice * (nefFile.Length + manifest.Length), + GetMinimumDeploymentFee(engine.SnapshotCache) + )); + + NefFile nef = nefFile.AsSerializable(); + ContractManifest parsedManifest = ContractManifest.Parse(manifest); + Helper.Check(new Script(nef.Script, true), parsedManifest.Abi); + UInt160 hash = Helper.GetContractHash(tx.Sender, nef.CheckSum, parsedManifest.Name); + + if (Policy.IsBlocked(engine.SnapshotCache, hash)) + throw new InvalidOperationException($"The contract {hash} has been blocked."); + + StorageKey key = CreateStorageKey(Prefix_Contract, hash); + if (engine.SnapshotCache.Contains(key)) + throw new InvalidOperationException($"Contract Already Exists: {hash}"); + ContractState contract = new() + { + Id = GetNextAvailableId(engine.SnapshotCache), + UpdateCounter = 0, + Nef = nef, + Hash = hash, + Manifest = parsedManifest + }; + + if (!contract.Manifest.IsValid(engine.Limits, hash)) throw new InvalidOperationException($"Invalid Manifest: {hash}"); + + engine.SnapshotCache.Add(key, StorageItem.CreateSealed(contract)); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_ContractHash, contract.Id), new StorageItem(hash.ToArray())); + + await OnDeployAsync(engine, contract, data, false); + + return contract; + } + + /// + /// Updates a contract. It needs to pay the storage fee. + /// + /// The engine used to write data. + /// The NEF file of the contract. + /// The manifest of the contract. + /// The updated contract. + [ContractMethod(RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private ContractTask Update(ApplicationEngine engine, byte[]? nefFile, byte[]? manifest) + { + return Update(engine, nefFile, manifest, StackItem.Null); + } + + /// + /// Updates a contract. It needs to pay the storage fee. + /// + /// The engine used to write data. + /// The NEF file of the contract. + /// The manifest of the contract. + /// The data of the contract. + /// The updated contract. + [ContractMethod(RequiredCallFlags = CallFlags.All)] + private ContractTask Update(ApplicationEngine engine, byte[]? nefFile, byte[]? manifest, StackItem data) + { + if (nefFile is null && manifest is null) + throw new ArgumentException("NEF file and manifest cannot both be null."); + + engine.AddFee(engine.StoragePrice * ((nefFile?.Length ?? 0) + (manifest?.Length ?? 0))); + + var contractState = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Contract, engine.CallingScriptHash!)) + ?? throw new InvalidOperationException($"Updating Contract Does Not Exist: {engine.CallingScriptHash}"); + + using var sealInterop = contractState.GetInteroperable(out ContractState contract, false); + if (contract is null) + throw new InvalidOperationException($"Updating Contract Does Not Exist: {engine.CallingScriptHash}"); + if (contract.UpdateCounter == ushort.MaxValue) + throw new InvalidOperationException($"The contract reached the maximum number of updates."); + + if (nefFile != null) + { + if (nefFile.Length == 0) + throw new ArgumentException($"NEF file length cannot be zero."); + + // Update nef + contract.Nef = nefFile.AsSerializable(); + } + // Clean whitelist (emit event if exists with the old manifest information) + Policy.CleanWhitelist(engine, contract); + if (manifest != null) + { + if (manifest.Length == 0) + throw new ArgumentException($"Manifest length cannot be zero."); + + var manifestNew = ContractManifest.Parse(manifest); + if (manifestNew.Name != contract.Manifest.Name) + throw new InvalidOperationException("The name of the contract can't be changed."); + if (!manifestNew.IsValid(engine.Limits, contract.Hash)) + throw new InvalidOperationException($"Invalid Manifest: {contract.Hash}"); + contract.Manifest = manifestNew; + } + Helper.Check(new Script(contract.Nef.Script, true), contract.Manifest.Abi); + // Increase update counter + contract.UpdateCounter++; + return OnDeployAsync(engine, contract, data, true); + } + + /// + /// Destroys a contract. + /// + /// The engine used to write data. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private async ContractTask Destroy(ApplicationEngine engine) + { + UInt160 hash = engine.CallingScriptHash!; + StorageKey ckey = CreateStorageKey(Prefix_Contract, hash); + ContractState? contract = engine.SnapshotCache.TryGet(ckey)?.GetInteroperable(false); + if (contract is null) return; + engine.SnapshotCache.Delete(ckey); + engine.SnapshotCache.Delete(CreateStorageKey(Prefix_ContractHash, contract.Id)); + foreach (var (key, _) in engine.SnapshotCache.Find(StorageKey.CreateSearchPrefix(contract.Id, ReadOnlySpan.Empty))) + engine.SnapshotCache.Delete(key); + // lock contract + await Policy.BlockAccountInternal(engine, hash); + // Clean whitelist (emit event if exists with the old manifest information) + Policy.CleanWhitelist(engine, contract); + // emit event + Notify(engine, "Destroy", hash); + } +} diff --git a/src/Neo/SmartContract/Native/ContractMethodAttribute.cs b/src/Neo/SmartContract/Native/ContractMethodAttribute.cs new file mode 100644 index 0000000000..6186ea8886 --- /dev/null +++ b/src/Neo/SmartContract/Native/ContractMethodAttribute.cs @@ -0,0 +1,27 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractMethodAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Diagnostics; + +namespace Neo.SmartContract.Native; + +[DebuggerDisplay("{Name}")] +// We allow multiple attributes because the fees or requiredCallFlags may change between hard forks. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)] +internal class ContractMethodAttribute : Attribute, IHardforkActivable +{ + public string? Name { get; init; } + public CallFlags RequiredCallFlags { get; init; } + public long CpuFee { get; init; } + public long StorageFee { get; init; } + public Hardfork? ActiveIn { get; init; } + public Hardfork? DeprecatedIn { get; init; } +} diff --git a/src/Neo/SmartContract/Native/ContractMethodMetadata.cs b/src/Neo/SmartContract/Native/ContractMethodMetadata.cs new file mode 100644 index 0000000000..29ae57e915 --- /dev/null +++ b/src/Neo/SmartContract/Native/ContractMethodMetadata.cs @@ -0,0 +1,112 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ContractMethodMetadata.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Persistence; +using Neo.SmartContract.Manifest; +using Neo.VM.Types; +using System.Diagnostics; +using System.Numerics; +using System.Reflection; +using Array = Neo.VM.Types.Array; +using Boolean = Neo.VM.Types.Boolean; +using Buffer = Neo.VM.Types.Buffer; + +namespace Neo.SmartContract.Native; + +[DebuggerDisplay("{Name}")] +internal class ContractMethodMetadata : IHardforkActivable +{ + public string Name { get; } + public MethodInfo Handler { get; } + public InteropParameterDescriptor[] Parameters { get; } + public bool NeedApplicationEngine { get; } + public bool NeedSnapshot { get; } + public long CpuFee { get; } + public long StorageFee { get; } + 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) + { + Name = attribute.Name ?? member.Name; + Name = Name.ToLowerInvariant()[0] + Name[1..]; + Handler = member switch + { + MethodInfo m => m, + PropertyInfo p => p.GetMethod ?? throw new ArgumentException("Property must have a get method."), + _ => throw new ArgumentException("Member type not supported", nameof(member)) + }; + ParameterInfo[] parameterInfos = Handler.GetParameters(); + if (parameterInfos.Length > 0) + { + NeedApplicationEngine = parameterInfos[0].ParameterType.IsAssignableFrom(typeof(ApplicationEngine)); + // snapshot is a DataCache instance, and DataCache implements IReadOnlyStoreView + NeedSnapshot = parameterInfos[0].ParameterType.IsAssignableFrom(typeof(DataCache)); + } + if (NeedApplicationEngine || NeedSnapshot) + Parameters = parameterInfos.Skip(1).Select(p => new InteropParameterDescriptor(p)).ToArray(); + else + Parameters = parameterInfos.Select(p => new InteropParameterDescriptor(p)).ToArray(); + CpuFee = attribute.CpuFee; + StorageFee = attribute.StorageFee; + RequiredCallFlags = attribute.RequiredCallFlags; + ActiveIn = attribute.ActiveIn; + DeprecatedIn = attribute.DeprecatedIn; + Descriptor = new ContractMethodDescriptor + { + Name = Name, + ReturnType = ToParameterType(Handler.ReturnType), + Parameters = Parameters.Select(p => new ContractParameterDefinition { Type = ToParameterType(p.Type), Name = p.Name! }).ToArray(), + Safe = (attribute.RequiredCallFlags & ~CallFlags.ReadOnly) == 0 + }; + } + + private static ContractParameterType ToParameterType(Type type) + { + if (type.BaseType == typeof(ContractTask)) return ToParameterType(type.GenericTypeArguments[0]); + if (type == typeof(ContractTask)) return ContractParameterType.Void; + if (type == typeof(void)) return ContractParameterType.Void; + if (type == typeof(bool)) return ContractParameterType.Boolean; + if (type == typeof(sbyte)) return ContractParameterType.Integer; + if (type == typeof(byte)) return ContractParameterType.Integer; + if (type == typeof(short)) return ContractParameterType.Integer; + if (type == typeof(ushort)) return ContractParameterType.Integer; + if (type == typeof(int)) return ContractParameterType.Integer; + if (type == typeof(uint)) return ContractParameterType.Integer; + if (type == typeof(long)) return ContractParameterType.Integer; + if (type == typeof(ulong)) return ContractParameterType.Integer; + if (type == typeof(BigInteger)) return ContractParameterType.Integer; + if (type == typeof(byte[])) return ContractParameterType.ByteArray; + if (type == typeof(string)) return ContractParameterType.String; + if (type == typeof(UInt160)) return ContractParameterType.Hash160; + if (type == typeof(UInt256)) return ContractParameterType.Hash256; + if (type == typeof(ECPoint)) return ContractParameterType.PublicKey; + if (type == typeof(Boolean)) return ContractParameterType.Boolean; + if (type == typeof(Integer)) return ContractParameterType.Integer; + if (type == typeof(ByteString)) return ContractParameterType.ByteArray; + if (type == typeof(Buffer)) return ContractParameterType.ByteArray; + if (type == typeof(Array)) return ContractParameterType.Array; + if (type == typeof(Struct)) return ContractParameterType.Array; + if (type == typeof(Map)) return ContractParameterType.Map; + if (type == typeof(StackItem)) return ContractParameterType.Any; + if (type == typeof(object)) return ContractParameterType.Any; + if (typeof(IInteroperable).IsAssignableFrom(type)) return ContractParameterType.Array; + if (typeof(ISerializable).IsAssignableFrom(type)) return ContractParameterType.ByteArray; + if (type.IsArray) return ContractParameterType.Array; + if (type.IsEnum) return ContractParameterType.Integer; + if (type.IsValueType) return ContractParameterType.Array; + return ContractParameterType.InteropInterface; + } +} diff --git a/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs new file mode 100644 index 0000000000..c310360b12 --- /dev/null +++ b/src/Neo/SmartContract/Native/CryptoLib.BLS12_381.cs @@ -0,0 +1,144 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CryptoLib.BLS12_381.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.BLS12_381; +using Neo.VM.Types; + +namespace Neo.SmartContract.Native; + +partial class CryptoLib +{ + /// + /// Serialize a bls12381 point. + /// + /// The point to be serialized. + /// + [ContractMethod(CpuFee = 1 << 19)] + public static byte[] Bls12381Serialize(InteropInterface g) + { + return g.GetInterface() switch + { + G1Affine p => p.ToCompressed(), + G1Projective p => new G1Affine(p).ToCompressed(), + G2Affine p => p.ToCompressed(), + G2Projective p => new G2Affine(p).ToCompressed(), + Gt p => p.ToArray(), + _ => throw new ArgumentException("BLS12-381 type mismatch") + }; + } + + /// + /// Deserialize a bls12381 point. + /// + /// The point as byte array. + /// + [ContractMethod(CpuFee = 1 << 19)] + public static InteropInterface Bls12381Deserialize(byte[] data) + { + return data.Length switch + { + 48 => new InteropInterface(G1Affine.FromCompressed(data)), + 96 => new InteropInterface(G2Affine.FromCompressed(data)), + 576 => new InteropInterface(Gt.FromBytes(data)), + _ => throw new ArgumentException("Invalid BLS12-381 point length"), + }; + } + + /// + /// Determines whether the specified points are equal. + /// + /// The first point. + /// Teh second point. + /// true if the specified points are equal; otherwise, false. + [ContractMethod(CpuFee = 1 << 5)] + public static bool Bls12381Equal(InteropInterface x, InteropInterface y) + { + return (x.GetInterface(), y.GetInterface()) switch + { + (G1Affine p1, G1Affine p2) => p1.Equals(p2), + (G1Projective p1, G1Projective p2) => p1.Equals(p2), + (G2Affine p1, G2Affine p2) => p1.Equals(p2), + (G2Projective p1, G2Projective p2) => p1.Equals(p2), + (Gt p1, Gt p2) => p1.Equals(p2), + _ => throw new ArgumentException("BLS12-381 type mismatch") + }; + } + + /// + /// Add operation of two points. + /// + /// The first point. + /// The second point. + /// + [ContractMethod(CpuFee = 1 << 19)] + public static InteropInterface Bls12381Add(InteropInterface x, InteropInterface y) + { + return (x.GetInterface(), y.GetInterface()) switch + { + (G1Affine p1, G1Affine p2) => new(new G1Projective(p1) + p2), + (G1Affine p1, G1Projective p2) => new(p1 + p2), + (G1Projective p1, G1Affine p2) => new(p1 + p2), + (G1Projective p1, G1Projective p2) => new(p1 + p2), + (G2Affine p1, G2Affine p2) => new(new G2Projective(p1) + p2), + (G2Affine p1, G2Projective p2) => new(p1 + p2), + (G2Projective p1, G2Affine p2) => new(p1 + p2), + (G2Projective p1, G2Projective p2) => new(p1 + p2), + (Gt p1, Gt p2) => new(p1 + p2), + _ => throw new ArgumentException("BLS12-381 type mismatch") + }; + } + + /// + /// Mul operation of gt point and multiplier + /// + /// The point + /// Multiplier,32 bytes,little-endian + /// negative number + /// + [ContractMethod(CpuFee = 1 << 21)] + public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool neg) + { + Scalar X = neg ? -Scalar.FromBytes(mul) : Scalar.FromBytes(mul); + return x.GetInterface() switch + { + G1Affine p => new(p * X), + G1Projective p => new(p * X), + G2Affine p => new(p * X), + G2Projective p => new(p * X), + Gt p => new(p * X), + _ => throw new ArgumentException("BLS12-381 type mismatch") + }; + } + + /// + /// Pairing operation of g1 and g2 + /// + /// The g1 point. + /// The g2 point. + /// + [ContractMethod(CpuFee = 1 << 23)] + public static InteropInterface Bls12381Pairing(InteropInterface g1, InteropInterface g2) + { + G1Affine g1a = g1.GetInterface() switch + { + G1Affine g => g, + G1Projective g => new(g), + _ => throw new ArgumentException("BLS12-381 type mismatch") + }; + G2Affine g2a = g2.GetInterface() switch + { + G2Affine g => g, + G2Projective g => new(g), + _ => throw new ArgumentException("BLS12-381 type mismatch") + }; + return new(Bls12.Pairing(in g1a, in g2a)); + } +} diff --git a/src/Neo/SmartContract/Native/CryptoLib.cs b/src/Neo/SmartContract/Native/CryptoLib.cs new file mode 100644 index 0000000000..1c3069f5ed --- /dev/null +++ b/src/Neo/SmartContract/Native/CryptoLib.cs @@ -0,0 +1,141 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CryptoLib.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; + +namespace Neo.SmartContract.Native; + +/// +/// A native contract library that provides cryptographic algorithms. +/// +public sealed partial class CryptoLib : NativeContract +{ + private static readonly Dictionary s_curves = new() + { + [NamedCurveHash.secp256k1SHA256] = (ECCurve.Secp256k1, HashAlgorithm.SHA256), + [NamedCurveHash.secp256r1SHA256] = (ECCurve.Secp256r1, HashAlgorithm.SHA256), + [NamedCurveHash.secp256k1Keccak256] = (ECCurve.Secp256k1, HashAlgorithm.Keccak256), + [NamedCurveHash.secp256r1Keccak256] = (ECCurve.Secp256r1, HashAlgorithm.Keccak256), + }; + + internal CryptoLib() : base(-3) { } + + /// + /// Recovers the public key from a secp256k1 signature in a single byte array format. + /// + /// The hash of the message that was signed. + /// The 65-byte signature in format: r[32] + s[32] + v[1]. 64-bytes for eip-2098, where v must be 27 or 28. + /// The recovered public key in compressed format, or null if recovery fails. + [ContractMethod(CpuFee = 1 << 15, Name = "recoverSecp256K1")] + public static byte[]? RecoverSecp256K1(byte[] messageHash, byte[] signature) + { + // It will be checked in Crypto.ECRecover + // if (signature.Length != 65 && signature.Length != 64) + // throw new ArgumentException("Signature must be 65 or 64 bytes", nameof(signature)); + + try + { + var point = Crypto.ECRecover(signature, messageHash); + return point.EncodePoint(true); + } + catch + { + return null; + } + } + + /// + /// Computes the hash value for the specified byte array using the ripemd160 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [ContractMethod(CpuFee = 1 << 15, Name = "ripemd160")] + public static byte[] RIPEMD160(byte[] data) + { + return data.RIPEMD160(); + } + + /// + /// Computes the hash value for the specified byte array using the sha256 algorithm. + /// + /// The input to compute the hash code for. + /// The computed hash code. + [ContractMethod(CpuFee = 1 << 15)] + public static byte[] Sha256(byte[] data) + { + return data.Sha256(); + } + + /// + /// Computes the hash value for the specified byte array using the murmur32 algorithm. + /// + /// The input to compute the hash code for. + /// The seed of the murmur32 hash function + /// The computed hash code. + [ContractMethod(CpuFee = 1 << 13)] + public static byte[] Murmur32(byte[] data, uint seed) + { + Murmur32 murmur = new(seed); + return murmur.ComputeHash(data); + } + + /// + /// Computes the hash value for the specified byte array using the keccak256 algorithm. + /// + /// The input to compute the hash code for. + /// Computed hash + [ContractMethod(CpuFee = 1 << 15)] + public static byte[] Keccak256(byte[] data) + { + return data.Keccak256(); + } + + /// + /// Verifies that a digital signature is appropriate for the provided key and message using the ECDSA algorithm. + /// + /// The signed message. + /// The public key to be used. + /// 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)] + public static bool VerifyWithECDsa(byte[] message, byte[] pubkey, byte[] signature, NamedCurveHash curveHash) + { + if (!s_curves.TryGetValue(curveHash, out var ch)) + throw new NotSupportedException($"Unsupported curve or hash algorithm: {curveHash}"); + return Crypto.VerifySignature(message, signature, pubkey, ch.Curve, ch.HashAlgorithm); + } + + /// + /// Verifies that a digital signature is appropriate for the provided key and message using the Ed25519 algorithm. + /// + /// The signed message. + /// The Ed25519 public key to be used. + /// The signature to be verified. + /// if the signature is valid; otherwise, . + [ContractMethod(CpuFee = 1 << 15)] + public static bool VerifyWithEd25519(byte[] message, byte[] pubkey, byte[] signature) + { + if (signature.Length != Ed25519.SignatureSize) + throw new FormatException($"Signature size should be {Ed25519.SignatureSize}"); + + if (pubkey.Length != Ed25519.PublicKeySize) + throw new FormatException($"Public key size should be {Ed25519.PublicKeySize}"); + + var verifier = new Ed25519Signer(); + verifier.Init(false, new Ed25519PublicKeyParameters(pubkey, 0)); + verifier.BlockUpdate(message, 0, message.Length); + return verifier.VerifySignature(signature); + } +} diff --git a/src/Neo/SmartContract/Native/FungibleToken.cs b/src/Neo/SmartContract/Native/FungibleToken.cs new file mode 100644 index 0000000000..eb4cb97d13 --- /dev/null +++ b/src/Neo/SmartContract/Native/FungibleToken.cs @@ -0,0 +1,191 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FungibleToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.SmartContract.Manifest; +using Neo.VM.Types; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// The base class of all native tokens that are compatible with NEP-17. +/// +/// The type of account state. +[ContractEvent(0, name: "Transfer", + "from", ContractParameterType.Hash160, + "to", ContractParameterType.Hash160, + "amount", ContractParameterType.Integer)] +public abstract class FungibleToken : NativeContract + where TState : AccountState, new() +{ + /// + /// The symbol of the token. + /// + [ContractMethod] + public abstract string Symbol { get; } + + /// + /// The number of decimal places of the token. + /// + [ContractMethod] + public abstract byte Decimals { get; } + + /// + /// The factor used when calculating the displayed value of the token value. + /// + public BigInteger Factor { get; } + + /// + /// The prefix for storing total supply. + /// + protected const byte Prefix_TotalSupply = 11; + + /// + /// The prefix for storing account states. + /// + protected internal const byte Prefix_Account = 20; + + /// + /// Initializes a new instance of the class. + /// + /// Native contract id + protected FungibleToken(int id) : base(id) + { + Factor = BigInteger.Pow(10, Decimals); + } + + protected override void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest) + { + manifest.SupportedStandards = new[] { "NEP-17" }; + } + + internal async ContractTask Mint(ApplicationEngine engine, UInt160 account, BigInteger amount, bool callOnPayment) + { + if (amount.Sign < 0) throw new ArgumentOutOfRangeException(nameof(amount), "cannot be negative"); + if (amount.IsZero) return; + StorageItem storage = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Account, account), () => new StorageItem(new TState())); + TState state = storage.GetInteroperable(); + OnBalanceChanging(engine, account, state, amount); + state.Balance += amount; + storage = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_TotalSupply), () => new StorageItem(BigInteger.Zero)); + storage.Add(amount); + await PostTransferAsync(engine, null, account, amount, StackItem.Null, callOnPayment); + } + + internal async ContractTask Burn(ApplicationEngine engine, UInt160 account, BigInteger amount) + { + if (amount.Sign < 0) throw new ArgumentOutOfRangeException(nameof(amount), "cannot be negative"); + if (amount.IsZero) return; + StorageKey key = CreateStorageKey(Prefix_Account, account); + StorageItem storage = engine.SnapshotCache.GetAndChange(key)!; + TState state = storage.GetInteroperable(); + if (state.Balance < amount) throw new InvalidOperationException(); + OnBalanceChanging(engine, account, state, -amount); + if (state.Balance == amount) + engine.SnapshotCache.Delete(key); + else + state.Balance -= amount; + storage = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_TotalSupply))!; + storage.Add(-amount); + await PostTransferAsync(engine, account, null, amount, StackItem.Null, false); + } + + /// + /// Gets the total supply of the token. + /// + /// The snapshot used to read data. + /// The total supply of the token. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public virtual BigInteger TotalSupply(IReadOnlyStore snapshot) + { + var key = CreateStorageKey(Prefix_TotalSupply); + return snapshot.TryGet(key, out var item) ? item : BigInteger.Zero; + } + + /// + /// Gets the balance of the specified account. + /// + /// The snapshot used to read data. + /// The owner of the account. + /// The balance of the account. Or 0 if the account doesn't exist. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public virtual BigInteger BalanceOf(IReadOnlyStore snapshot, UInt160 account) + { + var key = CreateStorageKey(Prefix_Account, account); + if (snapshot.TryGet(key, out var item)) + return item.GetInteroperable().Balance; + return BigInteger.Zero; + } + + [ContractMethod(CpuFee = 1 << 17, StorageFee = 50, RequiredCallFlags = CallFlags.States | CallFlags.AllowCall | CallFlags.AllowNotify)] + private protected async ContractTask Transfer(ApplicationEngine engine, UInt160 from, UInt160 to, BigInteger amount, StackItem data) + { + if (amount.Sign < 0) throw new ArgumentOutOfRangeException(nameof(amount), "cannot be negative"); + if (!from.Equals(engine.CallingScriptHash) && !engine.CheckWitnessInternal(from)) + return false; + + StorageKey keyFrom = CreateStorageKey(Prefix_Account, from); + StorageItem? storageFrom = engine.SnapshotCache.GetAndChange(keyFrom); + if (amount.IsZero) + { + if (storageFrom != null) + { + TState stateFrom = storageFrom.GetInteroperable(); + OnBalanceChanging(engine, from, stateFrom, amount); + } + } + else + { + if (storageFrom is null) return false; + TState stateFrom = storageFrom.GetInteroperable(); + if (stateFrom.Balance < amount) return false; + if (from.Equals(to)) + { + OnBalanceChanging(engine, from, stateFrom, BigInteger.Zero); + } + else + { + OnBalanceChanging(engine, from, stateFrom, -amount); + if (stateFrom.Balance == amount) + engine.SnapshotCache.Delete(keyFrom); + else + stateFrom.Balance -= amount; + StorageKey keyTo = CreateStorageKey(Prefix_Account, to); + StorageItem storageTo = engine.SnapshotCache.GetAndChange(keyTo, () => new StorageItem(new TState())); + TState stateTo = storageTo.GetInteroperable(); + OnBalanceChanging(engine, to, stateTo, amount); + stateTo.Balance += amount; + } + } + await PostTransferAsync(engine, from, to, amount, data, true); + return true; + } + + internal virtual void OnBalanceChanging(ApplicationEngine engine, UInt160 account, TState state, BigInteger amount) + { + } + + private protected virtual async ContractTask PostTransferAsync(ApplicationEngine engine, UInt160? from, UInt160? to, BigInteger amount, StackItem data, bool callOnPayment) + { + // Send notification + + Notify(engine, "Transfer", from, to, amount); + + // Check if it's a wallet or smart contract + + if (!callOnPayment || to is null || !ContractManagement.IsContract(engine.SnapshotCache, to)) return; + + // Call onNEP17Payment method + + await engine.CallFromNativeContractAsync(Hash, to, "onNEP17Payment", from, amount, data); + } +} diff --git a/src/Neo/SmartContract/Native/GasToken.cs b/src/Neo/SmartContract/Native/GasToken.cs new file mode 100644 index 0000000000..51ed46c200 --- /dev/null +++ b/src/Neo/SmartContract/Native/GasToken.cs @@ -0,0 +1,57 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// GasToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.SmartContract.Native; + +/// +/// Represents the GAS token in the NEO system. +/// +public sealed class GasToken : FungibleToken +{ + public override string Symbol => "GAS"; + public override byte Decimals => 8; + + internal GasToken() : base(-6) { } + + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) + { + if (hardfork == ActiveIn) + { + UInt160 account = Contract.GetBFTAddress(engine.ProtocolSettings.StandbyValidators); + return Mint(engine, account, engine.ProtocolSettings.InitialGasDistribution, false); + } + return ContractTask.CompletedTask; + } + + internal override async ContractTask OnPersistAsync(ApplicationEngine engine) + { + long totalNetworkFee = 0; + foreach (Transaction tx in engine.PersistingBlock!.Transactions) + { + await Burn(engine, tx.Sender, tx.SystemFee + tx.NetworkFee); + totalNetworkFee += tx.NetworkFee; + + // Reward for NotaryAssisted attribute will be minted to designated notary nodes + // by Notary contract. + var notaryAssisted = tx.GetAttribute(); + if (notaryAssisted is not null) + { + totalNetworkFee -= (notaryAssisted.NKeys + 1) * Policy.GetAttributeFee(engine.SnapshotCache, (byte)notaryAssisted.Type); + } + } + ECPoint[] validators = NEO.GetNextBlockValidators(engine.SnapshotCache, engine.ProtocolSettings.ValidatorsCount); + UInt160 primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock.PrimaryIndex]).ToScriptHash(); + await Mint(engine, primary, totalNetworkFee, false); + } +} diff --git a/src/Neo/SmartContract/Native/HashIndexState.cs b/src/Neo/SmartContract/Native/HashIndexState.cs new file mode 100644 index 0000000000..b4b79c2491 --- /dev/null +++ b/src/Neo/SmartContract/Native/HashIndexState.cs @@ -0,0 +1,34 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// HashIndexState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.SmartContract.Native; + +class HashIndexState : IInteroperable +{ + public UInt256 Hash { get; set; } = UInt256.Zero; + public uint Index { get; set; } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + var @struct = (Struct)stackItem; + Hash = new UInt256(@struct[0].GetSpan()); + Index = (uint)@struct[1].GetInteger(); + } + + StackItem IInteroperable.ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { Hash.ToArray(), Index }; + } +} diff --git a/src/Neo/SmartContract/Native/IHardforkActivable.cs b/src/Neo/SmartContract/Native/IHardforkActivable.cs new file mode 100644 index 0000000000..4f1a26d2a7 --- /dev/null +++ b/src/Neo/SmartContract/Native/IHardforkActivable.cs @@ -0,0 +1,18 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IHardforkActivable.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.SmartContract.Native; + +internal interface IHardforkActivable +{ + public Hardfork? ActiveIn { get; } + public Hardfork? DeprecatedIn { get; } +} diff --git a/src/Neo/SmartContract/Native/InteroperableList.cs b/src/Neo/SmartContract/Native/InteroperableList.cs new file mode 100644 index 0000000000..50cd4081a1 --- /dev/null +++ b/src/Neo/SmartContract/Native/InteroperableList.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// InteroperableList.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; +using Neo.VM.Types; +using System.Collections; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Native; + +abstract class InteroperableList : IList, IInteroperable +{ + private List List => field ??= new(); + + public T this[int index] { get => List[index]; set => List[index] = value; } + public int Count => List.Count; + public bool IsReadOnly => false; + + public void Add(T item) => List.Add(item); + public void AddRange(IEnumerable collection) => List.AddRange(collection); + public void Clear() => List.Clear(); + public bool Contains(T item) => List.Contains(item); + public void CopyTo(T[] array, int arrayIndex) => List.CopyTo(array, arrayIndex); + IEnumerator IEnumerable.GetEnumerator() => List.GetEnumerator(); + public IEnumerator GetEnumerator() => List.GetEnumerator(); + public int IndexOf(T item) => List.IndexOf(item); + public void Insert(int index, T item) => List.Insert(index, item); + public bool Remove(T item) => List.Remove(item); + public void RemoveAt(int index) => List.RemoveAt(index); + public void Sort() => List.Sort(); + + protected abstract T ElementFromStackItem(StackItem item); + protected abstract StackItem ElementToStackItem(T element, IReferenceCounter? referenceCounter); + + public void FromStackItem(StackItem stackItem) + { + List.Clear(); + foreach (StackItem item in (Array)stackItem) + { + Add(ElementFromStackItem(item)); + } + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Array(referenceCounter, this.Select(p => ElementToStackItem(p, referenceCounter))); + } +} diff --git a/src/Neo/SmartContract/Native/LedgerContract.cs b/src/Neo/SmartContract/Native/LedgerContract.cs new file mode 100644 index 0000000000..9eb91afb85 --- /dev/null +++ b/src/Neo/SmartContract/Native/LedgerContract.cs @@ -0,0 +1,391 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// LedgerContract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable IDE0051 + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.VM; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// A native contract for storing all blocks and transactions. +/// +public sealed class LedgerContract : NativeContract +{ + private const byte Prefix_BlockHash = 9; + private const byte Prefix_CurrentBlock = 12; + private const byte Prefix_Block = 5; + private const byte Prefix_Transaction = 11; + + private readonly StorageKey _currentBlock; + + internal LedgerContract() : base(-4) + { + _currentBlock = CreateStorageKey(Prefix_CurrentBlock); + } + + internal override ContractTask OnPersistAsync(ApplicationEngine engine) + { + TransactionState[] transactions = engine.PersistingBlock!.Transactions.Select(p => new TransactionState + { + BlockIndex = engine.PersistingBlock.Index, + Transaction = p, + State = VMState.NONE + }).ToArray(); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_BlockHash, engine.PersistingBlock.Index), new StorageItem(engine.PersistingBlock.Hash.ToArray())); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_Block, engine.PersistingBlock.Hash), new StorageItem(TrimmedBlock.Create(engine.PersistingBlock).ToArray())); + foreach (TransactionState tx in transactions) + { + // It's possible that there are previously saved malicious conflict records for this transaction. + // If so, then remove it and store the relevant transaction itself. + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Transaction, tx.Transaction!.Hash), () => new StorageItem(new TransactionState())) + .FromReplica(new StorageItem(tx)); + + // Store transaction's conflicits. + var conflictingSigners = tx.Transaction.Signers.Select(s => s.Account); + foreach (var attr in tx.Transaction.GetAttributes()) + { + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Transaction, attr.Hash), () => new StorageItem(new TransactionState())) + .FromReplica(new StorageItem(new TransactionState() { BlockIndex = engine.PersistingBlock.Index })); + foreach (var signer in conflictingSigners) + { + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Transaction, attr.Hash, signer), () => new StorageItem(new TransactionState())) + .FromReplica(new StorageItem(new TransactionState() { BlockIndex = engine.PersistingBlock.Index })); + } + } + } + + engine.SetState(transactions); + return ContractTask.CompletedTask; + } + + internal override ContractTask PostPersistAsync(ApplicationEngine engine) + { + var state = engine.SnapshotCache.GetAndChange(_currentBlock, () => new StorageItem(new HashIndexState())) + // Don't need to seal because the size is fixed and it can't grow + .GetInteroperable(); + state.Hash = engine.PersistingBlock!.Hash; + state.Index = engine.PersistingBlock.Index; + return ContractTask.CompletedTask; + } + + internal bool Initialized(DataCache snapshot) + { + ArgumentNullException.ThrowIfNull(snapshot); + + return snapshot.Find(CreateStorageKey(Prefix_Block)).Any(); + } + + /// + /// Checks whether block with the specified index is reachable from the smart contract + /// based on the current state of application engine with respect to MaxTraceableBlocks + /// setting stored in native Policy smartcontract starting from HF_Echidna. + /// + /// The execution engine. + /// The index of the block. + /// Whether the block is traceable. + private bool IsTraceableBlock(ApplicationEngine engine, uint index) + { + return IsTraceableBlock(engine.SnapshotCache, index, engine.ProtocolSettings.MaxTraceableBlocks); + } + + /// + /// Checks whether block with the specified index is reachable from the smart contract + /// based on the current state of snapshot and provided maxTraceableBlocks value. It's + /// the caller's duty to provide proper maxTraceableBlocks value with respect to + /// MaxTraceableBlocks setting stored in native Policy smartcontract starting from + /// HF_Echidna. + /// + /// The snapshot used to read data. + /// The index of the block. + /// The maximum number of traceable blocks with respect to + /// MaxTraceableBlocks setting stored in native Policy smartcontract starting from + /// HF_Echidna. + /// Whether the block is traceable. + private bool IsTraceableBlock(IReadOnlyStore snapshot, uint index, uint maxTraceableBlocks) + { + uint currentIndex = CurrentIndex(snapshot); + if (index > currentIndex) return false; + return index + maxTraceableBlocks > currentIndex; + } + + /// + /// Gets the hash of the specified block. + /// + /// The snapshot used to read data. + /// The index of the block. + /// The hash of the block. + public UInt256? GetBlockHash(IReadOnlyStore snapshot, uint index) + { + ArgumentNullException.ThrowIfNull(snapshot); + + var key = CreateStorageKey(Prefix_BlockHash, index); + return snapshot.TryGet(key, out var item) ? new UInt256(item.Value.Span) : null; + } + + /// + /// Gets the hash of the current block. + /// + /// The snapshot used to read data. + /// The hash of the current block. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public UInt256 CurrentHash(IReadOnlyStore snapshot) + { + ArgumentNullException.ThrowIfNull(snapshot); + + return snapshot[_currentBlock].GetInteroperable().Hash; + } + + /// + /// Gets the index of the current block. + /// + /// The snapshot used to read data. + /// The index of the current block. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public uint CurrentIndex(IReadOnlyStore snapshot) + { + ArgumentNullException.ThrowIfNull(snapshot); + + return snapshot[_currentBlock].GetInteroperable().Index; + } + + /// + /// Determine whether the specified block is contained in the blockchain. + /// + /// The snapshot used to read data. + /// The hash of the block. + /// + /// if the blockchain contains the block; otherwise, . + /// + public bool ContainsBlock(IReadOnlyStore snapshot, UInt256 hash) + { + ArgumentNullException.ThrowIfNull(snapshot); + + return snapshot.Contains(CreateStorageKey(Prefix_Block, hash)); + } + + /// + /// Determine whether the specified transaction is contained in the blockchain. + /// + /// The snapshot used to read data. + /// The hash of the transaction. + /// + /// if the blockchain contains the transaction; otherwise, . + /// + public bool ContainsTransaction(IReadOnlyStore snapshot, UInt256 hash) + { + var txState = GetTransactionState(snapshot, hash); + return txState != null; + } + + /// + /// Determine whether the specified transaction hash is contained in the blockchain + /// as the hash of conflicting transaction. + /// + /// The snapshot used to read data. + /// The hash of the conflicting transaction. + /// The list of signer accounts of the conflicting transaction. + /// MaxTraceableBlocks protocol setting. + /// + /// if the blockchain contains the hash of the conflicting transaction; + /// otherwise, . + /// + public bool ContainsConflictHash(IReadOnlyStore snapshot, UInt256 hash, IEnumerable signers, uint maxTraceableBlocks) + { + ArgumentNullException.ThrowIfNull(snapshot); + + ArgumentNullException.ThrowIfNull(signers); + + // Check the dummy stub firstly to define whether there's exist at least one conflict record. + var key = CreateStorageKey(Prefix_Transaction, hash); + var stub = snapshot.TryGet(key, out var item) ? item.GetInteroperable() : null; + if (stub is null || stub.Transaction is not null || !IsTraceableBlock(snapshot, stub.BlockIndex, maxTraceableBlocks)) + return false; + + // At least one conflict record is found, then need to check signers intersection. + foreach (var signer in signers) + { + key = CreateStorageKey(Prefix_Transaction, hash, signer); + var state = snapshot.TryGet(key, out var tx) ? tx.GetInteroperable() : null; + if (state is not null && IsTraceableBlock(snapshot, state.BlockIndex, maxTraceableBlocks)) + return true; + } + + return false; + } + + /// + /// Gets a with the specified hash. + /// + /// The snapshot used to read data. + /// The hash of the block. + /// The trimmed block. + public TrimmedBlock? GetTrimmedBlock(IReadOnlyStore snapshot, UInt256 hash) + { + ArgumentNullException.ThrowIfNull(snapshot); + + var key = CreateStorageKey(Prefix_Block, hash); + if (snapshot.TryGet(key, out var item)) + return item.Value.AsSerializable(); + return null; + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + private TrimmedBlock? GetBlock(ApplicationEngine engine, byte[] indexOrHash) + { + UInt256? hash; + if (indexOrHash.Length < UInt256.Length) + hash = GetBlockHash(engine.SnapshotCache, (uint)new BigInteger(indexOrHash)); + else if (indexOrHash.Length == UInt256.Length) + hash = new UInt256(indexOrHash); + else + throw new ArgumentException($"Invalid indexOrHash length: {indexOrHash.Length}", nameof(indexOrHash)); + if (hash is null) return null; + TrimmedBlock? block = GetTrimmedBlock(engine.SnapshotCache, hash); + if (block is null || !IsTraceableBlock(engine, block.Index)) return null; + return block; + } + + /// + /// Gets a block with the specified hash. + /// + /// The snapshot used to read data. + /// The hash of the block. + /// The block with the specified hash. + public Block? GetBlock(IReadOnlyStore snapshot, UInt256 hash) + { + TrimmedBlock? state = GetTrimmedBlock(snapshot, hash); + if (state is null) return null; + return new Block + { + Header = state.Header, + Transactions = state.Hashes.Select(p => GetTransaction(snapshot, p)!).ToArray() + }; + } + + /// + /// Gets a block with the specified index. + /// + /// The snapshot used to read data. + /// The index of the block. + /// The block with the specified index. + public Block? GetBlock(IReadOnlyStore snapshot, uint index) + { + UInt256? hash = GetBlockHash(snapshot, index); + if (hash is null) return null; + return GetBlock(snapshot, hash); + } + + /// + /// Gets a block header with the specified hash. + /// + /// The snapshot used to read data. + /// The hash of the block. + /// The block header with the specified hash. + public Header? GetHeader(IReadOnlyStore snapshot, UInt256 hash) + { + return GetTrimmedBlock(snapshot, hash)?.Header; + } + + /// + /// Gets a block header with the specified index. + /// + /// The snapshot used to read data. + /// The index of the block. + /// The block header with the specified index. + public Header? GetHeader(DataCache snapshot, uint index) + { + UInt256? hash = GetBlockHash(snapshot, index); + if (hash is null) return null; + return GetHeader(snapshot, hash); + } + + /// + /// Gets a with the specified hash. + /// + /// The snapshot used to read data. + /// The hash of the transaction. + /// The with the specified hash. + public TransactionState? GetTransactionState(IReadOnlyStore snapshot, UInt256 hash) + { + ArgumentNullException.ThrowIfNull(snapshot); + + var key = CreateStorageKey(Prefix_Transaction, hash); + var state = snapshot.TryGet(key, out var item) ? item.GetInteroperable() : null; + return state?.Transaction is null ? null : state; + } + + /// + /// Gets a transaction with the specified hash. + /// + /// The snapshot used to read data. + /// The hash of the transaction. + /// The transaction with the specified hash. + public Transaction? GetTransaction(IReadOnlyStore snapshot, UInt256 hash) + { + return GetTransactionState(snapshot, hash)?.Transaction; + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates, Name = "getTransaction")] + private Transaction? GetTransactionForContract(ApplicationEngine engine, UInt256 hash) + { + TransactionState? state = GetTransactionState(engine.SnapshotCache, hash); + if (state is null || !IsTraceableBlock(engine, state.BlockIndex)) return null; + return state.Transaction; + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + private Signer[]? GetTransactionSigners(ApplicationEngine engine, UInt256 hash) + { + TransactionState? state = GetTransactionState(engine.SnapshotCache, hash); + if (state is null || !IsTraceableBlock(engine, state.BlockIndex)) return null; + return state.Transaction!.Signers; + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + private VMState GetTransactionVMState(ApplicationEngine engine, UInt256 hash) + { + TransactionState? state = GetTransactionState(engine.SnapshotCache, hash); + if (state is null || !IsTraceableBlock(engine, state.BlockIndex)) return VMState.NONE; + return state.State; + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + private int GetTransactionHeight(ApplicationEngine engine, UInt256 hash) + { + TransactionState? state = GetTransactionState(engine.SnapshotCache, hash); + if (state is null || !IsTraceableBlock(engine, state.BlockIndex)) return -1; + return (int)state.BlockIndex; + } + + [ContractMethod(CpuFee = 1 << 16, RequiredCallFlags = CallFlags.ReadStates)] + private Transaction? GetTransactionFromBlock(ApplicationEngine engine, byte[] blockIndexOrHash, int txIndex) + { + UInt256? hash; + if (blockIndexOrHash.Length < UInt256.Length) + hash = GetBlockHash(engine.SnapshotCache, (uint)new BigInteger(blockIndexOrHash)); + else if (blockIndexOrHash.Length == UInt256.Length) + hash = new UInt256(blockIndexOrHash); + else + throw new ArgumentException($"Invalid blockIndexOrHash length: {blockIndexOrHash.Length}", nameof(blockIndexOrHash)); + if (hash is null) return null; + TrimmedBlock? block = GetTrimmedBlock(engine.SnapshotCache, hash); + if (block is null || !IsTraceableBlock(engine, block.Index)) return null; + if (txIndex < 0 || txIndex >= block.Hashes.Length) + throw new ArgumentOutOfRangeException(nameof(txIndex)); + return GetTransaction(engine.SnapshotCache, block.Hashes[txIndex]); + } +} diff --git a/src/Neo/SmartContract/Native/NFTState.cs b/src/Neo/SmartContract/Native/NFTState.cs new file mode 100644 index 0000000000..2de564a5c5 --- /dev/null +++ b/src/Neo/SmartContract/Native/NFTState.cs @@ -0,0 +1,60 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NFTState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.SmartContract.Native; + +/// +/// Represents the state of a non-fungible token (NFT), including its asset identifier, owner, and associated properties. +/// Implements to allow conversion to/from VM . +/// +public class NFTState : IInteroperable +{ + /// + /// The asset id (collection) this NFT belongs to. + /// + public required UInt160 AssetId; + + /// + /// The account (owner) that currently owns this NFT. + /// + public required UInt160 Owner; + + /// + /// Arbitrary properties associated with this NFT. Keys are ByteString and values are ByteString or Buffer. + /// + public required Map Properties; + + /// + /// Populates this instance from a VM representation. + /// + /// A expected to be a with fields in the order: AssetId, Owner, Properties. + public void FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + AssetId = new UInt160(@struct[0].GetSpan()); + Owner = new UInt160(@struct[1].GetSpan()); + Properties = (Map)@struct[2]; + } + + /// + /// Convert current NFTState to a VM (Struct). + /// + /// Optional reference counter used by the VM. + /// A representing the NFTState. + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { AssetId.ToArray(), Owner.ToArray(), Properties }; + } +} diff --git a/src/Neo/SmartContract/Native/NamedCurveHash.cs b/src/Neo/SmartContract/Native/NamedCurveHash.cs new file mode 100644 index 0000000000..cc28bc1ca3 --- /dev/null +++ b/src/Neo/SmartContract/Native/NamedCurveHash.cs @@ -0,0 +1,38 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NamedCurveHash.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.SmartContract.Native; + +/// +/// Represents a pair of the named curve used in ECDSA and a hash algorithm used to hash message. +/// +public enum NamedCurveHash : byte +{ + /// + /// The secp256k1 curve and SHA256 hash algorithm. + /// + secp256k1SHA256 = 22, + + /// + /// The secp256r1 curve, which known as prime256v1 or nistP-256, and SHA256 hash algorithm. + /// + secp256r1SHA256 = 23, + + /// + /// The secp256k1 curve and Keccak256 hash algorithm. + /// + secp256k1Keccak256 = 122, + + /// + /// The secp256r1 curve, which known as prime256v1 or nistP-256, and Keccak256 hash algorithm. + /// + secp256r1Keccak256 = 123 +} diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs new file mode 100644 index 0000000000..d03aee56ed --- /dev/null +++ b/src/Neo/SmartContract/Native/NativeContract.cs @@ -0,0 +1,490 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NativeContract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Manifest; +using Neo.VM; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Neo.SmartContract.Native; + +/// +/// The base class of all native contracts. +/// +public abstract class NativeContract +{ + private class NativeContractsCache + { + public record CacheEntry(Dictionary Methods, byte[] Script); + + internal Dictionary NativeContracts { get; set; } = new(); + + public CacheEntry GetAllowedMethods(NativeContract native, ApplicationEngine engine) + { + if (NativeContracts.TryGetValue(native.Id, out var value)) return value; + + uint index = engine.PersistingBlock is null ? Ledger.CurrentIndex(engine.SnapshotCache) : engine.PersistingBlock.Index; + CacheEntry methods = native.GetAllowedMethods(engine.ProtocolSettings.IsHardforkEnabled, index); + NativeContracts[native.Id] = methods; + return methods; + } + } + + public delegate bool IsHardforkEnabledDelegate(Hardfork hf, uint blockHeight); + private static readonly List s_contractsList = []; + private static readonly Dictionary s_contractsDictionary = new(); + private readonly ImmutableHashSet _usedHardforks; + private readonly ReadOnlyCollection _methodDescriptors; + private readonly ReadOnlyCollection _eventsDescriptors; + + #region Named Native Contracts + + /// + /// Gets the instance of the class. + /// + public static ContractManagement ContractManagement { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static StdLib StdLib { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static CryptoLib CryptoLib { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static LedgerContract Ledger { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static NeoToken NEO { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static GasToken GAS { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static PolicyContract Policy { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static RoleManagement RoleManagement { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static OracleContract Oracle { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static Notary Notary { get; } = new(); + + /// + /// Gets the instance of the class. + /// + public static Treasury Treasury { get; } = new(); + + public static TokenManagement TokenManagement { get; } = new(); + + #endregion + + /// + /// Gets all native contracts. + /// + public static IReadOnlyCollection Contracts { get; } = s_contractsList; + + /// + /// The name of the native contract. + /// + public string Name => GetType().Name; + + /// + /// Since Hardfork has to start having access to the native contract. + /// + public virtual Hardfork? ActiveIn { get; } = null; + + /// + /// The hash of the native contract. + /// + public UInt160 Hash { get; } + + /// + /// The id of the native contract. + /// + public int Id { get; } + + /// + /// Initializes a new instance of the class. + /// + protected NativeContract(int id) + { + Id = id; + Hash = Helper.GetContractHash(UInt160.Zero, 0, Name); + + // Reflection to get the methods + + List listMethods = []; + foreach (var member in GetType().GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) + { + foreach (var attribute in member.GetCustomAttributes()) + { + listMethods.Add(new ContractMethodMetadata(member, attribute)); + } + } + _methodDescriptors = listMethods.OrderBy(p => p.Name, StringComparer.Ordinal).ThenBy(p => p.Parameters.Length).ToList().AsReadOnly(); + + // Reflection to get the events + _eventsDescriptors = GetType() + .GetCustomAttributes(true) + .OrderBy(p => p.Order) + .ToList() + .AsReadOnly(); + + // Calculate the initializations forks + _usedHardforks = + _methodDescriptors.Select(u => u.ActiveIn) + .Concat(_methodDescriptors.Select(u => u.DeprecatedIn)) + .Concat(_eventsDescriptors.Select(u => u.DeprecatedIn)) + .Concat(_eventsDescriptors.Select(u => u.ActiveIn)) + .Concat([ActiveIn]) + .Where(u => u.HasValue) + .Select(u => u!.Value) + .OrderBy(u => (byte)u) + .Cast().ToImmutableHashSet(); + s_contractsList.Add(this); + s_contractsDictionary.Add(Hash, this); + } + + /// + /// The allowed methods and his offsets. + /// + /// Hardfork checker + /// Block height. Used to check the hardforks and active methods. + /// The . + private NativeContractsCache.CacheEntry GetAllowedMethods(IsHardforkEnabledDelegate hfChecker, uint blockHeight) + { + Dictionary methods = new(); + + // Reflection to get the ContractMethods + byte[] script; + using (ScriptBuilder sb = new()) + { + foreach (ContractMethodMetadata method in _methodDescriptors.Where(u => IsActive(u, hfChecker, blockHeight))) + { + method.Descriptor.Offset = sb.Length; + sb.EmitPush(0); //version + methods.Add(sb.Length, method); + sb.EmitSysCall(ApplicationEngine.System_Contract_CallNative); + sb.Emit(OpCode.RET); + } + script = sb.ToArray(); + } + + return new NativeContractsCache.CacheEntry(methods, script); + } + + /// + /// The of the native contract. + /// + /// The where the HardForks are configured. + /// Block index + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ContractState GetContractState(ProtocolSettings settings, uint blockHeight) => GetContractState(settings.IsHardforkEnabled, blockHeight); + + internal static bool IsActive(IHardforkActivable u, IsHardforkEnabledDelegate hfChecker, uint blockHeight) + { + return // 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, blockHeight) == false || + // active method hardfork is involved + u.ActiveIn is not null && hfChecker(u.ActiveIn.Value, blockHeight); + } + + /// + /// The of the native contract. + /// + /// Hardfork checker + /// Block height. Used to check hardforks and active methods. + /// The . + public ContractState GetContractState(IsHardforkEnabledDelegate hfChecker, uint blockHeight) + { + // Get allowed methods and nef script + var allowedMethods = GetAllowedMethods(hfChecker, blockHeight); + + // Compose nef file + var nef = new NefFile() + { + Compiler = "neo-core-v3.0", + Source = string.Empty, + Tokens = [], + Script = allowedMethods.Script + }; + nef.CheckSum = NefFile.ComputeChecksum(nef); + + // Compose manifest + var manifest = new ContractManifest() + { + Name = Name, + Groups = [], + SupportedStandards = [], + Abi = new ContractAbi + { + Events = _eventsDescriptors + .Where(u => IsActive(u, hfChecker, blockHeight)) + .Select(p => p.Descriptor).ToArray(), + Methods = allowedMethods.Methods.Values + .Select(p => p.Descriptor).ToArray() + }, + Permissions = [ContractPermission.DefaultPermission], + Trusts = WildcardContainer.Create(), + Extra = null + }; + + OnManifestCompose(hfChecker, blockHeight, manifest); + + // Return ContractState + return new ContractState + { + Id = Id, + Nef = nef, + Hash = Hash, + Manifest = manifest + }; + } + + protected virtual void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest) { } + + /// + /// It is the initialize block + /// + /// The where the HardForks are configured. + /// Block index + /// Active hardforks + /// True if the native contract must be initialized + internal bool IsInitializeBlock(ProtocolSettings settings, uint index, [NotNullWhen(true)] out Hardfork[]? hardforks) + { + var hfs = new List(); + + // If is in the hardfork height, add them to return array + foreach (var hf in _usedHardforks) + { + if (!settings.Hardforks.TryGetValue(hf, out var activeIn)) + { + // If hf is not set in the configuration (with EnsureOmmitedHardforks applied over it), it is treated as disabled. + continue; + } + + if (activeIn == index) + { + hfs.Add(hf); + } + } + + // Return all initialize hardforks + if (hfs.Count > 0) + { + hardforks = hfs.ToArray(); + return true; + } + + // If is not configured, the Genesis is an initialization block. + if (index == 0 && ActiveIn is null) + { + hardforks = hfs.ToArray(); + return true; + } + + // Initialized not required + hardforks = null; + return false; + } + + /// + /// Is the native contract active + /// + /// The where the HardForks are configured. + /// Block height + /// True if the native contract is active + public bool IsActive(ProtocolSettings settings, uint blockHeight) + { + if (ActiveIn is null) return true; + + if (!settings.Hardforks.TryGetValue(ActiveIn.Value, out var activeIn)) + { + // If is not set in the configuration is treated as enabled from the genesis + activeIn = 0; + } + + return activeIn <= blockHeight; + } + + /// + /// Checks whether the committee has witnessed the current transaction. + /// + /// The that is executing the contract. + /// if the committee has witnessed the current transaction; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static bool CheckCommittee(ApplicationEngine engine) + { + var committeeMultiSigAddr = NEO.GetCommitteeAddress(engine.SnapshotCache); + return engine.CheckWitnessInternal(committeeMultiSigAddr); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static void AssertCommittee(ApplicationEngine engine) + { + if (!CheckCommittee(engine)) + throw new InvalidOperationException("Invalid committee signature. It should be a multisig(len(committee) - (len(committee) - 1) / 2))."); + } + + protected void Notify(ApplicationEngine engine, string eventName, params object?[] args) + { + engine.SendNotification(Hash, eventName, new(engine.ReferenceCounter, args.Select(engine.Convert))); + } + + #region Storage keys + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix) => new KeyBuilder(Id, prefix); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, byte data) => new KeyBuilder(Id, prefix) { data }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, T bigEndianKey) where T : unmanaged => new KeyBuilder(Id, prefix) { bigEndianKey }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, ReadOnlySpan content) => new KeyBuilder(Id, prefix) { content }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, params IEnumerable serializables) + { + var builder = new KeyBuilder(Id, prefix); + foreach (var serializable in serializables) + builder.Add(serializable); + return builder; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash, int bigEndianKey) + => new KeyBuilder(Id, prefix) { hash, bigEndianKey }; + + #endregion + + /// + /// Gets the native contract with the specified hash. + /// + /// The hash of the native contract. + /// The native contract with the specified hash. + public static NativeContract? GetContract(UInt160 hash) + { + s_contractsDictionary.TryGetValue(hash, out var contract); + return contract; + } + + internal Dictionary GetContractMethods(ApplicationEngine engine) + { + var nativeContracts = engine.GetState(() => new NativeContractsCache()); + var currentAllowedMethods = nativeContracts.GetAllowedMethods(this, engine); + return currentAllowedMethods.Methods; + } + + internal async void Invoke(ApplicationEngine engine, byte version) + { + try + { + if (version != 0) + throw new InvalidOperationException($"The native contract of version {version} is not active."); + // Get native contracts invocation cache + var currentAllowedMethods = GetContractMethods(engine); + // Check if the method is allowed + var context = engine.CurrentContext!; + var method = currentAllowedMethods[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}."); + var state = context.GetState(); + if (!state.CallFlags.HasFlag(method.RequiredCallFlags)) + throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}."); + // Check native-whitelist + if (!Policy.IsWhitelistFeeContract(engine.SnapshotCache, Hash, method.Descriptor, out var fixedFee)) + { + // In the unit of datoshi, 1 datoshi = 1e-8 GAS + engine.AddFee(method.CpuFee * engine.ExecFeeFactor + method.StorageFee * engine.StoragePrice); + } + List parameters = new(); + if (method.NeedApplicationEngine) parameters.Add(engine); + if (method.NeedSnapshot) parameters.Add(engine.SnapshotCache); + for (int i = 0; i < method.Parameters.Length; i++) + parameters.Add(engine.Convert(context.EvaluationStack.Peek(i), method.Parameters[i])); + object? returnValue = method.Handler.Invoke(this, parameters.ToArray()); + if (returnValue is ContractTask task) + { + await task; + returnValue = task.GetResult(); + } + for (int i = 0; i < method.Parameters.Length; i++) + { + context.EvaluationStack.Pop(); + } + if (method.Handler.ReturnType != typeof(void) && method.Handler.ReturnType != typeof(ContractTask)) + { + context.EvaluationStack.Push(engine.Convert(returnValue)); + } + } + catch (Exception ex) + { + engine.Throw(ex); + } + } + + /// + /// Determine whether the specified contract is a native contract. + /// + /// The hash of the contract. + /// if the contract is native; otherwise, . + public static bool IsNative(UInt160 hash) + { + return s_contractsDictionary.ContainsKey(hash); + } + + internal virtual ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardFork) + { + return ContractTask.CompletedTask; + } + + internal virtual ContractTask OnPersistAsync(ApplicationEngine engine) + { + return ContractTask.CompletedTask; + } + + internal virtual ContractTask PostPersistAsync(ApplicationEngine engine) + { + return ContractTask.CompletedTask; + } +} diff --git a/src/Neo/SmartContract/Native/NeoToken.cs b/src/Neo/SmartContract/Native/NeoToken.cs new file mode 100644 index 0000000000..1d03eca4a3 --- /dev/null +++ b/src/Neo/SmartContract/Native/NeoToken.cs @@ -0,0 +1,714 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NeoToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable IDE0051 + +using Neo.Cryptography.ECC; +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Persistence; +using Neo.SmartContract.Iterators; +using Neo.SmartContract.Manifest; +using Neo.VM; +using Neo.VM.Types; +using System.Buffers.Binary; +using System.Numerics; +using Array = System.Array; + +namespace Neo.SmartContract.Native; + +/// +/// Represents the NEO token in the NEO system. +/// +[ContractEvent(1, name: "CandidateStateChanged", + "pubkey", ContractParameterType.PublicKey, + "registered", ContractParameterType.Boolean, + "votes", ContractParameterType.Integer)] +[ContractEvent(2, name: "Vote", + "account", ContractParameterType.Hash160, + "from", ContractParameterType.PublicKey, + "to", ContractParameterType.PublicKey, + "amount", ContractParameterType.Integer)] +[ContractEvent(3, name: "CommitteeChanged", + "old", ContractParameterType.Array, + "new", ContractParameterType.Array)] +public sealed class NeoToken : FungibleToken +{ + public override string Symbol => "NEO"; + public override byte Decimals => 0; + + /// + /// Indicates the total amount of NEO. + /// + public BigInteger TotalAmount { get; } + + /// + /// Indicates the effective voting turnout in NEO. The voted candidates will only be effective when the voting turnout exceeds this value. + /// + public const decimal EffectiveVoterTurnout = 0.2M; + private const long VoteFactor = 100000000L; + + private const byte Prefix_VotersCount = 1; + private const byte Prefix_Candidate = 33; + private const byte Prefix_Committee = 14; + private const byte Prefix_GasPerBlock = 29; + private const byte Prefix_RegisterPrice = 13; + private const byte Prefix_VoterRewardPerCommittee = 23; + + private const byte NeoHolderRewardRatio = 10; + private const byte CommitteeRewardRatio = 10; + private const byte VoterRewardRatio = 80; + + private readonly StorageKey _votersCount; + private readonly StorageKey _registerPrice; + + internal NeoToken() : base(-5) + { + TotalAmount = 100000000 * Factor; + _votersCount = CreateStorageKey(Prefix_VotersCount); + _registerPrice = CreateStorageKey(Prefix_RegisterPrice); + } + + public override BigInteger TotalSupply(IReadOnlyStore snapshot) + { + return TotalAmount; + } + + internal override void OnBalanceChanging(ApplicationEngine engine, UInt160 account, NeoAccountState state, BigInteger amount) + { + GasDistribution? distribution = DistributeGas(engine, account, state); + if (distribution is not null) + { + var list = engine.CurrentContext!.GetState>(); + list.Add(distribution); + } + if (amount.IsZero) return; + if (state.VoteTo is null) return; + engine.SnapshotCache.GetAndChange(_votersCount)!.Add(amount); + StorageKey key = CreateStorageKey(Prefix_Candidate, state.VoteTo); + CandidateState candidate = engine.SnapshotCache.GetAndChange(key)!.GetInteroperable(); + candidate.Votes += amount; + CheckCandidate(engine.SnapshotCache, state.VoteTo, candidate); + } + + private protected override async ContractTask PostTransferAsync(ApplicationEngine engine, UInt160? from, UInt160? to, BigInteger amount, StackItem data, bool callOnPayment) + { + await base.PostTransferAsync(engine, from, to, amount, data, callOnPayment); + var list = engine.CurrentContext!.GetState>(); + foreach (var distribution in list) + await GAS.Mint(engine, distribution.Account, distribution.Amount, callOnPayment); + } + + protected override void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest) + { + manifest.SupportedStandards = ["NEP-17", "NEP-27"]; + } + + private GasDistribution? DistributeGas(ApplicationEngine engine, UInt160 account, NeoAccountState state) + { + // PersistingBlock is null when running under the debugger + if (engine.PersistingBlock is null) return null; + + // In the unit of datoshi, 1 datoshi = 1e-8 GAS + BigInteger datoshi = CalculateBonus(engine.SnapshotCache, state, engine.PersistingBlock.Index); + state.BalanceHeight = engine.PersistingBlock.Index; + if (state.VoteTo is not null) + { + var keyLastest = CreateStorageKey(Prefix_VoterRewardPerCommittee, state.VoteTo); + var latestGasPerVote = engine.SnapshotCache.TryGet(keyLastest) ?? BigInteger.Zero; + state.LastGasPerVote = latestGasPerVote; + } + if (datoshi == 0) return null; + return new GasDistribution + { + Account = account, + Amount = datoshi + }; + } + + private BigInteger CalculateBonus(DataCache snapshot, NeoAccountState state, uint end) + { + if (state.Balance.IsZero) return BigInteger.Zero; + if (state.Balance.Sign < 0) throw new ArgumentOutOfRangeException(nameof(state), "Balance cannot be negative"); + + var expectEnd = Ledger.CurrentIndex(snapshot) + 1; + ArgumentOutOfRangeException.ThrowIfNotEqual(end, expectEnd); + if (state.BalanceHeight >= end) return BigInteger.Zero; + // In the unit of datoshi, 1 datoshi = 1e-8 GAS + (var neoHolderReward, var voteReward) = CalculateReward(snapshot, state, end); + + return neoHolderReward + voteReward; + } + + private (BigInteger neoHold, BigInteger voteReward) CalculateReward(DataCache snapshot, NeoAccountState state, uint end) + { + var start = state.BalanceHeight; + + // Compute Neo holder reward + + // In the unit of datoshi, 1 GAS = 10^8 datoshi + BigInteger sumGasPerBlock = 0; + foreach (var (index, gasPerBlock) in GetSortedGasRecords(snapshot, end - 1)) + { + if (index > start) + { + sumGasPerBlock += gasPerBlock * (end - index); + end = index; + } + else + { + sumGasPerBlock += gasPerBlock * (end - start); + break; + } + } + + // Compute vote reward + + var voteReward = BigInteger.Zero; + + if (state.VoteTo != null) + { + var keyLastest = CreateStorageKey(Prefix_VoterRewardPerCommittee, state.VoteTo); + var latestGasPerVote = snapshot.TryGet(keyLastest) ?? BigInteger.Zero; + voteReward = state.Balance * (latestGasPerVote - state.LastGasPerVote) / VoteFactor; + } + + return (state.Balance * sumGasPerBlock * NeoHolderRewardRatio / 100 / TotalAmount, voteReward); + } + + private void CheckCandidate(DataCache snapshot, ECPoint pubkey, CandidateState candidate) + { + if (!candidate.Registered && candidate.Votes.IsZero) + { + snapshot.Delete(CreateStorageKey(Prefix_VoterRewardPerCommittee, pubkey)); + snapshot.Delete(CreateStorageKey(Prefix_Candidate, pubkey)); + } + } + + /// + /// Determine whether the votes should be recounted at the specified height. + /// + /// The height to be checked. + /// The number of committee members in the system. + /// if the votes should be recounted; otherwise, . + public static bool ShouldRefreshCommittee(uint height, int committeeMembersCount) => height % committeeMembersCount == 0; + + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) + { + if (hardfork == ActiveIn) + { + var cachedCommittee = new CachedCommittee(engine.ProtocolSettings.StandbyCommittee.Select(p => (p, BigInteger.Zero))); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_Committee), new StorageItem(cachedCommittee)); + engine.SnapshotCache.Add(_votersCount, new StorageItem(Array.Empty())); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_GasPerBlock, 0u), new StorageItem(5 * GAS.Factor)); + engine.SnapshotCache.Add(_registerPrice, new StorageItem(1000 * GAS.Factor)); + return Mint(engine, Contract.GetBFTAddress(engine.ProtocolSettings.StandbyValidators), TotalAmount, false); + } + return ContractTask.CompletedTask; + } + + internal override ContractTask OnPersistAsync(ApplicationEngine engine) + { + // Set next committee + if (ShouldRefreshCommittee(engine.PersistingBlock!.Index, engine.ProtocolSettings.CommitteeMembersCount)) + { + var storageItem = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Committee))!; + var cachedCommittee = storageItem.GetInteroperable(); + + var prevCommittee = cachedCommittee.Select(u => u.PublicKey).ToArray(); + + cachedCommittee.Clear(); + cachedCommittee.AddRange(ComputeCommitteeMembers(engine.SnapshotCache, engine.ProtocolSettings)); + + var newCommittee = cachedCommittee.Select(u => u.PublicKey).ToArray(); + + if (!newCommittee.SequenceEqual(prevCommittee)) + { + Notify(engine, "CommitteeChanged", prevCommittee, newCommittee); + } + } + return ContractTask.CompletedTask; + } + + internal override async ContractTask PostPersistAsync(ApplicationEngine engine) + { + // Distribute GAS for committee + + int m = engine.ProtocolSettings.CommitteeMembersCount; + int n = engine.ProtocolSettings.ValidatorsCount; + int index = (int)(engine.PersistingBlock!.Index % (uint)m); + var gasPerBlock = GetGasPerBlock(engine.SnapshotCache); + var committee = GetCommitteeFromCache(engine.SnapshotCache); + var pubkey = committee[index].PublicKey; + var account = Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash(); + await GAS.Mint(engine, account, gasPerBlock * CommitteeRewardRatio / 100, false); + + // Record the cumulative reward of the voters of committee + + if (ShouldRefreshCommittee(engine.PersistingBlock.Index, m)) + { + BigInteger voterRewardOfEachCommittee = gasPerBlock * VoterRewardRatio * VoteFactor * m / (m + n) / 100; // Zoom in VoteFactor times, and the final calculation should be divided VoteFactor + for (index = 0; index < committee.Count; index++) + { + var (publicKey, votes) = committee[index]; + var factor = index < n ? 2 : 1; // The `voter` rewards of validator will double than other committee's + if (votes > 0) + { + BigInteger voterSumRewardPerNEO = factor * voterRewardOfEachCommittee / votes; + StorageKey voterRewardKey = CreateStorageKey(Prefix_VoterRewardPerCommittee, publicKey); + StorageItem lastRewardPerNeo = engine.SnapshotCache.GetAndChange(voterRewardKey, () => new StorageItem(BigInteger.Zero)); + lastRewardPerNeo.Add(voterSumRewardPerNEO); + } + } + } + } + + /// + /// Sets the amount of GAS generated in each block. Only committee members can call this method. + /// + /// The engine used to check committee witness and read data. + /// The amount of GAS generated in each block. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetGasPerBlock(ApplicationEngine engine, BigInteger gasPerBlock) + { + if (gasPerBlock < 0 || gasPerBlock > 10 * GAS.Factor) + throw new ArgumentOutOfRangeException(nameof(gasPerBlock), $"GasPerBlock must be between [0, {10 * GAS.Factor}]"); + AssertCommittee(engine); + + var index = engine.PersistingBlock!.Index + 1; + var entry = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_GasPerBlock, index), () => new StorageItem(gasPerBlock)); + entry.Set(gasPerBlock); + } + + /// + /// Gets the amount of GAS generated in each block. + /// + /// The snapshot used to read data. + /// The amount of GAS generated. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public BigInteger GetGasPerBlock(DataCache snapshot) + { + return GetSortedGasRecords(snapshot, Ledger.CurrentIndex(snapshot) + 1).First().GasPerBlock; + } + + /// + /// Sets the fees to be paid to register as a candidate. Only committee members can call this method. + /// + /// The engine used to check committee witness and read data. + /// The fees to be paid to register as a candidate. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetRegisterPrice(ApplicationEngine engine, long registerPrice) + { + if (registerPrice <= 0) + throw new ArgumentOutOfRangeException(nameof(registerPrice), "RegisterPrice must be positive"); + AssertCommittee(engine); + + engine.SnapshotCache.GetAndChange(_registerPrice)!.Set(registerPrice); + } + + /// + /// Gets the fees to be paid to register as a candidate. + /// + /// The snapshot used to read data. + /// The amount of the fees. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public long GetRegisterPrice(IReadOnlyStore snapshot) + { + // In the unit of datoshi, 1 datoshi = 1e-8 GAS + return (long)(BigInteger)snapshot[_registerPrice]; + } + + private IEnumerable<(uint Index, BigInteger GasPerBlock)> GetSortedGasRecords(DataCache snapshot, uint end) + { + var key = CreateStorageKey(Prefix_GasPerBlock, end).ToArray(); + var boundary = CreateStorageKey(Prefix_GasPerBlock).ToArray(); + return snapshot.FindRange(key, boundary, SeekDirection.Backward) + .Select(u => (BinaryPrimitives.ReadUInt32BigEndian(u.Key.Key.Span[^sizeof(uint)..]), (BigInteger)u.Value)); + } + + /// + /// Get the amount of unclaimed GAS in the specified account. + /// + /// The snapshot used to read data. + /// The account to check. + /// The block index used when calculating GAS. + /// The amount of unclaimed GAS. + [ContractMethod(CpuFee = 1 << 17, RequiredCallFlags = CallFlags.ReadStates)] + public BigInteger UnclaimedGas(DataCache snapshot, UInt160 account, uint end) + { + StorageItem? storage = snapshot.TryGet(CreateStorageKey(Prefix_Account, account)); + if (storage is null) return BigInteger.Zero; + NeoAccountState state = storage.GetInteroperable(); + return CalculateBonus(snapshot, state, end); + } + + /// + /// Handles the payment of GAS. + /// + /// The engine used to check witness and read data. + /// The account that is paying the GAS. + /// The amount of GAS being paid. + /// The data of the payment. + [ContractMethod(RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private async ContractTask OnNEP17Payment(ApplicationEngine engine, UInt160 from, BigInteger amount, StackItem data) + { + if (engine.CallingScriptHash != GAS.Hash) + throw new InvalidOperationException("Only GAS contract can call this method"); + + if ((long)amount != GetRegisterPrice(engine.SnapshotCache)) + throw new ArgumentException($"Incorrect GAS amount. Expected {GetRegisterPrice(engine.SnapshotCache)} GAS, but received {amount} GAS."); + + var pubkey = ECPoint.DecodePoint(data.GetSpan(), ECCurve.Secp256r1); + + if (!RegisterInternal(engine, pubkey)) + throw new InvalidOperationException("Failed to register candidate"); + + await GAS.Burn(engine, Hash, amount); + } + + /// + /// Registers a candidate. + /// + /// The engine used to check witness and read data. + /// The public key of the candidate. + /// if the candidate is registered; otherwise, . + [ContractMethod(RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private bool RegisterCandidate(ApplicationEngine engine, ECPoint pubkey) + { + // In the unit of datoshi, 1 datoshi = 1e-8 GAS + engine.AddFee(GetRegisterPrice(engine.SnapshotCache)); + return RegisterInternal(engine, pubkey); + } + + private bool RegisterInternal(ApplicationEngine engine, ECPoint pubkey) + { + if (!engine.CheckWitnessInternal(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash())) + return false; + StorageKey key = CreateStorageKey(Prefix_Candidate, pubkey); + StorageItem item = engine.SnapshotCache.GetAndChange(key, () => new StorageItem(new CandidateState())); + CandidateState state = item.GetInteroperable(); + if (state.Registered) return true; + state.Registered = true; + Notify(engine, "CandidateStateChanged", pubkey, true, state.Votes); + return true; + } + + /// + /// Unregisters a candidate. + /// + /// The engine used to check witness and read data. + /// The public key of the candidate. + /// if the candidate is unregistered; otherwise, . + [ContractMethod(CpuFee = 1 << 16, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private bool UnregisterCandidate(ApplicationEngine engine, ECPoint pubkey) + { + if (!engine.CheckWitnessInternal(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash())) + return false; + StorageKey key = CreateStorageKey(Prefix_Candidate, pubkey); + if (engine.SnapshotCache.TryGet(key) is null) return true; + StorageItem item = engine.SnapshotCache.GetAndChange(key)!; + CandidateState state = item.GetInteroperable(); + if (!state.Registered) return true; + state.Registered = false; + CheckCandidate(engine.SnapshotCache, pubkey, state); + Notify(engine, "CandidateStateChanged", pubkey, false, state.Votes); + return true; + } + + /// + /// Votes for a candidate. + /// + /// The engine used to check witness and read data. + /// The account that is voting. + /// The candidate to vote for. + /// if the vote is successful; otherwise, . + [ContractMethod(CpuFee = 1 << 16, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private async ContractTask Vote(ApplicationEngine engine, UInt160 account, ECPoint? voteTo) + { + if (!engine.CheckWitnessInternal(account)) return false; + return await VoteInternal(engine, account, voteTo); + } + + internal async ContractTask VoteInternal(ApplicationEngine engine, UInt160 account, ECPoint? voteTo) + { + NeoAccountState? stateAccount = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Account, account))?.GetInteroperable(); + if (stateAccount is null) return false; + if (stateAccount.Balance == 0) return false; + + CandidateState? validatorNew = null; + if (voteTo != null) + { + validatorNew = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Candidate, voteTo))?.GetInteroperable(); + if (validatorNew is null) return false; + if (!validatorNew.Registered) return false; + } + if (stateAccount.VoteTo is null ^ voteTo is null) + { + StorageItem item = engine.SnapshotCache.GetAndChange(_votersCount)!; + if (stateAccount.VoteTo is null) + item.Add(stateAccount.Balance); + else + item.Add(-stateAccount.Balance); + } + GasDistribution? gasDistribution = DistributeGas(engine, account, stateAccount); + if (stateAccount.VoteTo != null) + { + StorageKey key = CreateStorageKey(Prefix_Candidate, stateAccount.VoteTo); + StorageItem storageValidator = engine.SnapshotCache.GetAndChange(key)!; + CandidateState stateValidator = storageValidator.GetInteroperable(); + stateValidator.Votes -= stateAccount.Balance; + CheckCandidate(engine.SnapshotCache, stateAccount.VoteTo, stateValidator); + } + if (voteTo != null && voteTo != stateAccount.VoteTo) + { + StorageKey voterRewardKey = CreateStorageKey(Prefix_VoterRewardPerCommittee, voteTo); + var latestGasPerVote = engine.SnapshotCache.TryGet(voterRewardKey) ?? BigInteger.Zero; + stateAccount.LastGasPerVote = latestGasPerVote; + } + ECPoint? from = stateAccount.VoteTo; + stateAccount.VoteTo = voteTo; + + if (validatorNew != null) + { + validatorNew.Votes += stateAccount.Balance; + } + else + { + stateAccount.LastGasPerVote = 0; + } + Notify(engine, "Vote", account, from, voteTo, stateAccount.Balance); + if (gasDistribution is not null) + await GAS.Mint(engine, gasDistribution.Account, gasDistribution.Amount, true); + return true; + } + + /// + /// Gets the first 256 registered candidates. + /// + /// The snapshot used to read data. + /// All the registered candidates. + [ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)] + internal (ECPoint PublicKey, BigInteger Votes)[] GetCandidates(DataCache snapshot) + { + return GetCandidatesInternal(snapshot) + .Select(p => (p.PublicKey, p.State.Votes)) + .Take(256) + .ToArray(); + } + + /// + /// Gets the registered candidates iterator. + /// + /// The snapshot used to read data. + /// All the registered candidates. + [ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)] + private StorageIterator GetAllCandidates(IReadOnlyStore snapshot) + { + const FindOptions options = FindOptions.RemovePrefix | FindOptions.DeserializeValues | FindOptions.PickField1; + var enumerator = GetCandidatesInternal(snapshot) + .Select(p => (p.Key, p.Value)) + .GetEnumerator(); + return new StorageIterator(enumerator, 1, options); + } + + internal IEnumerable<(StorageKey Key, StorageItem Value, ECPoint PublicKey, CandidateState State)> GetCandidatesInternal(IReadOnlyStore snapshot) + { + var prefixKey = CreateStorageKey(Prefix_Candidate); + return snapshot.Find(prefixKey) + .Select(p => (p.Key, p.Value, PublicKey: p.Key.Key[1..].AsSerializable(), State: p.Value.GetInteroperable())) + .Where(p => p.State.Registered) + .Where(p => !Policy.IsBlocked(snapshot, Contract.CreateSignatureRedeemScript(p.PublicKey).ToScriptHash())); + } + + /// + /// Gets votes from specific candidate. + /// + /// The snapshot used to read data. + /// Specific public key + /// Votes or -1 if it was not found. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public BigInteger GetCandidateVote(IReadOnlyStore snapshot, ECPoint pubKey) + { + var key = CreateStorageKey(Prefix_Candidate, pubKey); + var state = snapshot.TryGet(key, out var item) ? item.GetInteroperable() : null; + return state?.Registered == true ? state.Votes : -1; + } + + /// + /// Gets all the members of the committee. + /// + /// The snapshot used to read data. + /// The public keys of the members. + [ContractMethod(CpuFee = 1 << 16, RequiredCallFlags = CallFlags.ReadStates)] + public ECPoint[] GetCommittee(IReadOnlyStore snapshot) + { + return GetCommitteeFromCache(snapshot).Select(p => p.PublicKey).OrderBy(p => p).ToArray(); + } + + /// + /// Get account state. + /// + /// The snapshot used to read data. + /// account + /// The state of the account. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public NeoAccountState? GetAccountState(IReadOnlyStore snapshot, UInt160 account) + { + var key = CreateStorageKey(Prefix_Account, account); + return snapshot.TryGet(key, out var item) ? item.GetInteroperableClone() : null; + } + + /// + /// Gets the address of the committee. + /// + /// The snapshot used to read data. + /// The address of the committee. + [ContractMethod(CpuFee = 1 << 16, RequiredCallFlags = CallFlags.ReadStates)] + public UInt160 GetCommitteeAddress(IReadOnlyStore snapshot) + { + ECPoint[] committees = GetCommittee(snapshot); + return Contract.CreateMultiSigRedeemScript(committees.Length - (committees.Length - 1) / 2, committees).ToScriptHash(); + } + + private CachedCommittee GetCommitteeFromCache(IReadOnlyStore snapshot) + { + return snapshot[CreateStorageKey(Prefix_Committee)].GetInteroperable(); + } + + /// + /// Computes the validators of the next block. + /// + /// The snapshot used to read data. + /// The used during computing. + /// The public keys of the validators. + public ECPoint[] ComputeNextBlockValidators(DataCache snapshot, ProtocolSettings settings) + { + return ComputeCommitteeMembers(snapshot, settings).Select(p => p.PublicKey).Take(settings.ValidatorsCount).OrderBy(p => p).ToArray(); + } + + private IEnumerable<(ECPoint PublicKey, BigInteger Votes)> ComputeCommitteeMembers(DataCache snapshot, ProtocolSettings settings) + { + decimal votersCount = (decimal)(BigInteger)snapshot[_votersCount]; + decimal voterTurnout = votersCount / (decimal)TotalAmount; + var candidates = GetCandidatesInternal(snapshot) + .Select(p => (p.PublicKey, p.State.Votes)) + .ToArray(); + if (voterTurnout < EffectiveVoterTurnout || candidates.Length < settings.CommitteeMembersCount) + return settings.StandbyCommittee.Select(p => (p, candidates.FirstOrDefault(k => k.PublicKey.Equals(p)).Votes)); + return candidates + .OrderByDescending(p => p.Votes) + .ThenBy(p => p.PublicKey) + .Take(settings.CommitteeMembersCount); + } + + /// + /// Gets the validators of the next block. + /// + /// The engine used to read data. + /// The public keys of the validators. + [ContractMethod(CpuFee = 1 << 16, RequiredCallFlags = CallFlags.ReadStates)] + private ECPoint[] GetNextBlockValidators(ApplicationEngine engine) + { + return GetNextBlockValidators(engine.SnapshotCache, engine.ProtocolSettings.ValidatorsCount); + } + + /// + /// Gets the validators of the next block. + /// + /// The snapshot used to read data. + /// The number of validators in the system. + /// The public keys of the validators. + public ECPoint[] GetNextBlockValidators(IReadOnlyStore snapshot, int validatorsCount) + { + return GetCommitteeFromCache(snapshot) + .Take(validatorsCount) + .Select(p => p.PublicKey) + .OrderBy(p => p) + .ToArray(); + } + + /// + /// Represents the account state of . + /// + public class NeoAccountState : AccountState + { + /// + /// The height of the block where the balance changed last time. + /// + public uint BalanceHeight; + + /// + /// The voting target of the account. This field can be . + /// + public ECPoint? VoteTo; + + public BigInteger LastGasPerVote; + + public override void FromStackItem(StackItem stackItem) + { + base.FromStackItem(stackItem); + Struct @struct = (Struct)stackItem; + BalanceHeight = (uint)@struct[1].GetInteger(); + VoteTo = @struct[2].IsNull ? null : ECPoint.DecodePoint(@struct[2].GetSpan(), ECCurve.Secp256r1); + LastGasPerVote = @struct[3].GetInteger(); + } + + public override StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + Struct @struct = (Struct)base.ToStackItem(referenceCounter); + @struct.Add(BalanceHeight); + @struct.Add(VoteTo?.ToArray() ?? StackItem.Null); + @struct.Add(LastGasPerVote); + return @struct; + } + } + + internal class CandidateState : IInteroperable + { + public bool Registered; + public BigInteger Votes; + + public void FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + Registered = @struct[0].GetBoolean(); + Votes = @struct[1].GetInteger(); + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { Registered, Votes }; + } + } + + internal class CachedCommittee : InteroperableList<(ECPoint PublicKey, BigInteger Votes)> + { + public CachedCommittee() { } + public CachedCommittee(IEnumerable<(ECPoint, BigInteger)> collection) => AddRange(collection); + + protected override (ECPoint, BigInteger) ElementFromStackItem(StackItem item) + { + Struct @struct = (Struct)item; + return (ECPoint.DecodePoint(@struct[0].GetSpan(), ECCurve.Secp256r1), @struct[1].GetInteger()); + } + + protected override StackItem ElementToStackItem((ECPoint PublicKey, BigInteger Votes) element, IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { element.PublicKey.ToArray(), element.Votes }; + } + } + + private record GasDistribution + { + public required UInt160 Account { get; init; } + public BigInteger Amount { get; init; } + } +} diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs new file mode 100644 index 0000000000..a4e5996d1d --- /dev/null +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -0,0 +1,338 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Notary.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable IDE0051 + +using Neo.Cryptography; +using Neo.Cryptography.ECC; +using Neo.Extensions; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Manifest; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Native; + +/// +/// The Notary native contract used for multisignature transactions forming assistance. +/// +public sealed class Notary : NativeContract +{ + /// + /// A default value for maximum allowed NotValidBeforeDelta. It is set to be + /// 20 rounds for 7 validators, a little more than half an hour for 15-seconds blocks. + /// + private const int DefaultMaxNotValidBeforeDelta = 140; + /// + /// A default value for deposit lock period. + /// + private const int DefaultDepositDeltaTill = 5760; + private const byte Prefix_Deposit = 1; + private const byte Prefix_MaxNotValidBeforeDelta = 10; + + internal Notary() : base(-10) { } + + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) + { + if (hardfork == ActiveIn) + { + engine.SnapshotCache.Add(CreateStorageKey(Prefix_MaxNotValidBeforeDelta), new StorageItem(DefaultMaxNotValidBeforeDelta)); + } + return ContractTask.CompletedTask; + } + + internal override async ContractTask OnPersistAsync(ApplicationEngine engine) + { + long nFees = 0; + ECPoint[]? notaries = null; + foreach (var tx in engine.PersistingBlock!.Transactions) + { + var attr = tx.GetAttribute(); + if (attr is not null) + { + notaries ??= GetNotaryNodes(engine.SnapshotCache); + var nKeys = attr.NKeys; + nFees += (long)nKeys + 1; + if (tx.Sender == Hash) + { + var payer = tx.Signers[1]; + // Don't need to seal because Deposit is a fixed-sized interoperable, hence always can be serialized. + var balance = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Deposit, payer.Account))?.GetInteroperable(); + if (balance != null) + { + balance.Amount -= tx.SystemFee + tx.NetworkFee; + if (balance.Amount.Sign == 0) RemoveDepositFor(engine.SnapshotCache, payer.Account); + } + } + } + } + if (nFees == 0) return; + if (notaries == null) return; + var singleReward = CalculateNotaryReward(engine.SnapshotCache, nFees, notaries.Length); + foreach (var notary in notaries) await GAS.Mint(engine, Contract.CreateSignatureRedeemScript(notary).ToScriptHash(), singleReward, false); + } + + protected override void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest) + { + manifest.SupportedStandards = ["NEP-27"]; + } + + /// + /// Verify checks whether the transaction is signed by one of the notaries and + /// ensures whether deposited amount of GAS is enough to pay the actual sender's fee. + /// + /// ApplicationEngine + /// Signature + /// Whether transaction is valid. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + private bool Verify(ApplicationEngine engine, byte[] signature) + { + if (signature is null || signature.Length != 64) return false; + var tx = engine.ScriptContainer as Transaction; + if (tx?.GetAttribute() is null) return false; + foreach (var signer in tx.Signers) + { + if (signer.Account == Hash) + { + if (signer.Scopes != WitnessScope.None) return false; + break; + } + } + if (tx.Sender == Hash) + { + if (tx.Signers.Length != 2) return false; + var payer = tx.Signers[1].Account; + var balance = GetDepositFor(engine.SnapshotCache, payer); + if (balance is null || balance.Amount.CompareTo(tx.NetworkFee + tx.SystemFee) < 0) return false; + } + var notaries = GetNotaryNodes(engine.SnapshotCache); + var hash = tx.GetSignData(engine.GetNetwork()); + return notaries.Any(n => Crypto.VerifySignature(hash, signature, n)); + } + + /// + /// OnNEP17Payment is a callback that accepts GAS transfer as Notary deposit. + /// It also sets the deposit's lock height after which deposit can be withdrawn. + /// + /// ApplicationEngine + /// GAS sender + /// The amount of GAS sent + /// Deposit-related data: optional To value (treated as deposit owner if set) and Till height after which deposit can be withdrawn + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void OnNEP17Payment(ApplicationEngine engine, UInt160 from, BigInteger amount, StackItem data) + { + if (engine.CallingScriptHash != GAS.Hash) throw new InvalidOperationException(string.Format("only GAS can be accepted for deposit, got {0}", engine.CallingScriptHash!.ToString())); + if (data is not Array additionalParams || additionalParams.Count != 2) throw new FormatException("`data` parameter should be an array of 2 elements"); + var to = from; + if (!additionalParams[0].Equals(StackItem.Null)) to = additionalParams[0].GetSpan().ToArray().AsSerializable(); + var till = (uint)additionalParams[1].GetInteger(); + var tx = (Transaction)engine.ScriptContainer!; + var allowedChangeTill = tx.Sender == to; + var currentHeight = Ledger.CurrentIndex(engine.SnapshotCache); + if (till < currentHeight + 2) throw new ArgumentOutOfRangeException(string.Format("`till` shouldn't be less than the chain's height {0} + 1", currentHeight + 2)); + // Don't need to seal because Deposit is a fixed-sized interoperable, hence always can be serialized. + var deposit = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Deposit, to))?.GetInteroperable(); + if (deposit != null && till < deposit.Till) throw new ArgumentOutOfRangeException(string.Format("`till` shouldn't be less than the previous value {0}", deposit.Till)); + if (deposit is null) + { + var feePerKey = Policy.GetAttributeFee(engine.SnapshotCache, (byte)TransactionAttributeType.NotaryAssisted); + if ((long)amount < 2 * feePerKey) throw new ArgumentOutOfRangeException(string.Format("first deposit can not be less than {0}, got {1}", 2 * feePerKey, amount)); + deposit = new Deposit() { Amount = 0, Till = 0 }; + if (!allowedChangeTill) till = currentHeight + DefaultDepositDeltaTill; + } + else if (!allowedChangeTill) till = deposit.Till; + + deposit.Amount += amount; + deposit.Till = till; + PutDepositFor(engine, to, deposit); + } + + /// + /// Lock asset until the specified height is unlocked. + /// + /// ApplicationEngine + /// Account + /// specified height + /// Whether deposit lock height was successfully updated. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + public bool LockDepositUntil(ApplicationEngine engine, UInt160 account, uint till) + { + if (!engine.CheckWitnessInternal(account)) return false; + if (till < Ledger.CurrentIndex(engine.SnapshotCache) + 2) return false; // deposit must be valid at least until the next block after persisting block. + var deposit = GetDepositFor(engine.SnapshotCache, account); + if (deposit is null || till < deposit.Till) return false; + deposit.Till = till; + + PutDepositFor(engine, account, deposit); + return true; + } + + /// + /// ExpirationOf returns deposit lock height for specified address. + /// + /// DataCache + /// Account + /// Deposit lock height of the specified address. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public uint ExpirationOf(DataCache snapshot, UInt160 account) + { + var deposit = GetDepositFor(snapshot, account); + if (deposit is null) return 0; + return deposit.Till; + } + + /// + /// BalanceOf returns deposited GAS amount for specified address. + /// + /// DataCache + /// Account + /// Deposit balance of the specified account. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public BigInteger BalanceOf(DataCache snapshot, UInt160 account) + { + var deposit = GetDepositFor(snapshot, account); + if (deposit is null) return 0; + return deposit.Amount; + } + + /// + /// Withdraw sends all deposited GAS for "from" address to "to" address. If "to" + /// address is not specified, then "from" will be used as a sender. + /// + /// ApplicationEngine + /// From Account + /// To Account + /// Whether withdrawal was successfull. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.All)] + private async ContractTask Withdraw(ApplicationEngine engine, UInt160 from, UInt160? to) + { + if (!engine.CheckWitnessInternal(from)) return false; + var receive = to is null ? from : to; + var deposit = GetDepositFor(engine.SnapshotCache, from); + if (deposit is null) return false; + if (Ledger.CurrentIndex(engine.SnapshotCache) < deposit.Till) return false; + RemoveDepositFor(engine.SnapshotCache, from); + if (!await engine.CallFromNativeContractAsync(Hash, GAS.Hash, "transfer", Hash, receive, deposit.Amount, null)) + { + throw new InvalidOperationException(string.Format("Transfer to {0} has failed", receive.ToString())); + } + return true; + } + + /// + /// GetMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta. + /// + /// DataCache + /// NotValidBefore + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public uint GetMaxNotValidBeforeDelta(IReadOnlyStore snapshot) + { + return (uint)(BigInteger)snapshot[CreateStorageKey(Prefix_MaxNotValidBeforeDelta)]; + } + + /// + /// SetMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. + /// + /// ApplicationEngine + /// Value + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetMaxNotValidBeforeDelta(ApplicationEngine engine, uint value) + { + var maxVUBIncrement = engine.ProtocolSettings.MaxValidUntilBlockIncrement; + if (value > maxVUBIncrement / 2 || value < ProtocolSettings.Default.ValidatorsCount) + { + throw new FormatException(string.Format("MaxNotValidBeforeDelta cannot be more than {0} or less than {1}", + maxVUBIncrement / 2, ProtocolSettings.Default.ValidatorsCount)); + } + AssertCommittee(engine); + + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_MaxNotValidBeforeDelta))!.Set(value); + } + + /// + /// GetNotaryNodes returns public keys of notary nodes. + /// + /// DataCache + /// Public keys of notary nodes. + private static ECPoint[] GetNotaryNodes(DataCache snapshot) + { + return RoleManagement.GetDesignatedByRole(snapshot, Role.P2PNotary, Ledger.CurrentIndex(snapshot) + 1); + } + + /// + /// GetDepositFor returns Deposit for the specified account or nil in case if deposit + /// is not found in storage. + /// + /// + /// + /// Deposit for the specified account. + private Deposit? GetDepositFor(DataCache snapshot, UInt160 acc) + { + return snapshot.TryGet(CreateStorageKey(Prefix_Deposit, acc))?.GetInteroperable(); + } + + /// + /// PutDepositFor puts deposit on the balance of the specified account in the storage. + /// + /// ApplicationEngine + /// Account + /// deposit + private void PutDepositFor(ApplicationEngine engine, UInt160 acc, Deposit deposit) + { + // Don't need to seal because Deposit is a fixed-sized interoperable, hence always can be serialized. + var indeposit = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Deposit, acc), () => new StorageItem(deposit)); + indeposit!.Value = new StorageItem(deposit).Value; + } + + /// + /// RemoveDepositFor removes deposit from the storage. + /// + /// DataCache + /// Account + private void RemoveDepositFor(DataCache snapshot, UInt160 acc) + { + snapshot.Delete(CreateStorageKey(Prefix_Deposit, acc)); + } + + /// + /// CalculateNotaryReward calculates the reward for a single notary node based on FEE's count and Notary nodes count. + /// + /// DataCache + /// + /// + /// result + private static long CalculateNotaryReward(IReadOnlyStore snapshot, long nFees, int notariesCount) + { + return nFees * Policy.GetAttributeFee(snapshot, (byte)TransactionAttributeType.NotaryAssisted) / notariesCount; + } + + public class Deposit : IInteroperable + { + public BigInteger Amount { get; set; } + public uint Till { get; set; } + + public void FromStackItem(StackItem stackItem) + { + var @struct = (Struct)stackItem; + Amount = @struct[0].GetInteger(); + Till = (uint)@struct[1].GetInteger(); + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { Amount, Till }; + } + } +} diff --git a/src/Neo/SmartContract/Native/OracleContract.cs b/src/Neo/SmartContract/Native/OracleContract.cs new file mode 100644 index 0000000000..7738ac489c --- /dev/null +++ b/src/Neo/SmartContract/Native/OracleContract.cs @@ -0,0 +1,285 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// OracleContract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable IDE0051 + +using Neo.Cryptography; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.VM; +using Neo.VM.Types; +using System.Buffers.Binary; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// The native Oracle service for NEO system. +/// +[ContractEvent(0, name: "OracleRequest", + "Id", ContractParameterType.Integer, + "RequestContract", ContractParameterType.Hash160, + "Url", ContractParameterType.String, + "Filter", ContractParameterType.String)] +[ContractEvent(1, name: "OracleResponse", + "Id", ContractParameterType.Integer, + "OriginalTx", ContractParameterType.Hash256)] +public sealed class OracleContract : NativeContract +{ + private const int MaxUrlLength = 256; + private const int MaxFilterLength = 128; + private const int MaxCallbackLength = 32; + private const int MaxUserDataLength = 512; + + private const byte Prefix_Price = 5; + private const byte Prefix_RequestId = 9; + private const byte Prefix_Request = 7; + private const byte Prefix_IdList = 6; + + internal OracleContract() : base(-9) { } + + /// + /// Sets the price for an Oracle request. Only committee members can call this method. + /// + /// The engine used to check witness and read data. + /// The price for an Oracle request. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetPrice(ApplicationEngine engine, long price) + { + if (price <= 0) throw new ArgumentOutOfRangeException(nameof(price), "Price must be positive"); + AssertCommittee(engine); + + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Price))!.Set(price); + } + + /// + /// Gets the price for an Oracle request. + /// + /// The snapshot used to read data. + /// The price for an Oracle request, in the unit of datoshi, 1 datoshi = 1e-8 GAS. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public long GetPrice(IReadOnlyStore snapshot) + { + return (long)(BigInteger)snapshot[CreateStorageKey(Prefix_Price)]; + } + + /// + /// Finishes an Oracle response. + /// + /// The engine used to check witness and read data. + /// if the response is finished; otherwise, . + [ContractMethod(RequiredCallFlags = CallFlags.States | CallFlags.AllowCall | CallFlags.AllowNotify)] + private ContractTask Finish(ApplicationEngine engine) + { + if (engine.InvocationStack.Count != 2) throw new InvalidOperationException(); + if (engine.GetInvocationCounter() != 1) throw new InvalidOperationException(); + Transaction tx = (Transaction)engine.ScriptContainer!; + OracleResponse response = tx.GetAttribute() + ?? throw new ArgumentException("Oracle response not found"); + OracleRequest request = GetRequest(engine.SnapshotCache, response.Id) + ?? throw new ArgumentException("Oracle request not found"); + Notify(engine, "OracleResponse", response.Id, request.OriginalTxid); + StackItem userData = BinarySerializer.Deserialize(request.UserData, engine.Limits, engine.ReferenceCounter); + return engine.CallFromNativeContractAsync(Hash, request.CallbackContract, request.CallbackMethod, request.Url, userData, response.Code, response.Result); + } + + private UInt256 GetOriginalTxid(ApplicationEngine engine) + { + Transaction tx = (Transaction)engine.ScriptContainer!; + OracleResponse? response = tx.GetAttribute(); + if (response is null) return tx.Hash; + OracleRequest request = GetRequest(engine.SnapshotCache, response.Id)!; + return request.OriginalTxid; + } + + /// + /// Gets a pending request with the specified id. + /// + /// The snapshot used to read data. + /// The id of the request. + /// The pending request. Or if no request with the specified id is found. + public OracleRequest? GetRequest(IReadOnlyStore snapshot, ulong id) + { + var key = CreateStorageKey(Prefix_Request, id); + return snapshot.TryGet(key, out var item) ? item.GetInteroperableClone() : null; + } + + /// + /// Gets all the pending requests. + /// + /// The snapshot used to read data. + /// All the pending requests. + public IEnumerable<(ulong, OracleRequest)> GetRequests(IReadOnlyStore snapshot) + { + var key = CreateStorageKey(Prefix_Request); + return snapshot.Find(key) + .Select(p => (BinaryPrimitives.ReadUInt64BigEndian(p.Key.Key.Span[1..]), p.Value.GetInteroperableClone())); + } + + /// + /// Gets the requests with the specified url. + /// + /// The snapshot used to read data. + /// The url of the requests. + /// All the requests with the specified url. + public IEnumerable<(ulong, OracleRequest)> GetRequestsByUrl(IReadOnlyStore snapshot, string url) + { + var listKey = CreateStorageKey(Prefix_IdList, GetUrlHash(url)); + IdList? list = snapshot.TryGet(listKey, out var item) ? item.GetInteroperable() : null; + if (list is null) yield break; + foreach (ulong id in list) + { + var key = CreateStorageKey(Prefix_Request, id); + yield return (id, snapshot[key].GetInteroperableClone()); + } + } + + private static ReadOnlySpan GetUrlHash(string url) + { + return Crypto.Hash160(url.ToStrictUtf8Bytes()); + } + + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) + { + if (hardfork == ActiveIn) + { + engine.SnapshotCache.Add(CreateStorageKey(Prefix_RequestId), new StorageItem(BigInteger.Zero)); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_Price), new StorageItem(0_50000000)); + } + return ContractTask.CompletedTask; + } + + internal override async ContractTask PostPersistAsync(ApplicationEngine engine) + { + (UInt160 Account, BigInteger GAS)[]? nodes = null; + foreach (Transaction tx in engine.PersistingBlock!.Transactions) + { + //Filter the response transactions + OracleResponse? response = tx.GetAttribute(); + if (response is null) continue; + + //Remove the request from storage + StorageKey key = CreateStorageKey(Prefix_Request, response.Id); + // Don't need to seal because it's read-only + var request = engine.SnapshotCache.TryGet(key)?.GetInteroperable(); + if (request == null) continue; + engine.SnapshotCache.Delete(key); + + //Remove the id from IdList + key = CreateStorageKey(Prefix_IdList, GetUrlHash(request.Url)); + using (var sealInterop = engine.SnapshotCache.GetAndChange(key)!.GetInteroperable(out IdList list)) + { + if (!list.Remove(response.Id)) throw new InvalidOperationException(); + if (list.Count == 0) engine.SnapshotCache.Delete(key); + } + + //Mint GAS for oracle nodes + nodes ??= RoleManagement.GetDesignatedByRole(engine.SnapshotCache, Role.Oracle, engine.PersistingBlock.Index) + .Select(p => (Contract.CreateSignatureRedeemScript(p).ToScriptHash(), BigInteger.Zero)) + .ToArray(); + if (nodes.Length > 0) + { + int index = (int)(response.Id % (ulong)nodes.Length); + nodes[index].GAS += GetPrice(engine.SnapshotCache); + } + } + if (nodes != null) + { + foreach (var (account, gas) in nodes) + { + if (gas.Sign > 0) + await GAS.Mint(engine, account, gas, false); + } + } + } + + [ContractMethod(RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private async ContractTask Request(ApplicationEngine engine, string url, string? filter, string callback, + StackItem userData, long gasForResponse /* In the unit of datoshi, 1 datoshi = 1e-8 GAS */) + { + var urlSize = url.GetStrictUtf8ByteCount(); + if (urlSize > MaxUrlLength) + throw new ArgumentException($"URL size {urlSize} bytes exceeds maximum allowed size of {MaxUrlLength} bytes."); + + var filterSize = filter is null ? 0 : filter.GetStrictUtf8ByteCount(); + if (filterSize > MaxFilterLength) + throw new ArgumentException($"Filter size {filterSize} bytes exceeds maximum allowed size of {MaxFilterLength} bytes."); + + var callbackSize = callback.GetStrictUtf8ByteCount(); + if (callbackSize > MaxCallbackLength) + throw new ArgumentException($"Callback size {callbackSize} bytes exceeds maximum allowed size of {MaxCallbackLength} bytes."); + + if (callback.StartsWith('_')) + throw new ArgumentException("Callback cannot start with underscore."); + + if (gasForResponse < 0_10000000) + throw new ArgumentException($"gasForResponse {gasForResponse} must be at least 0.1 datoshi."); + + engine.AddFee(GetPrice(engine.SnapshotCache)); + + //Mint gas for the response + engine.AddFee(gasForResponse); + await GAS.Mint(engine, Hash, gasForResponse, false); + + //Increase the request id + var itemId = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_RequestId))!; + var id = (ulong)(BigInteger)itemId; + itemId.Add(1); + + //Put the request to storage + if (!ContractManagement.IsContract(engine.SnapshotCache, engine.CallingScriptHash!)) + throw new InvalidOperationException(); + var request = new OracleRequest + { + OriginalTxid = GetOriginalTxid(engine), + GasForResponse = gasForResponse, + Url = url, + Filter = filter, + CallbackContract = engine.CallingScriptHash!, + CallbackMethod = callback, + UserData = BinarySerializer.Serialize(userData, MaxUserDataLength, engine.Limits.MaxStackSize) + }; + engine.SnapshotCache.Add(CreateStorageKey(Prefix_Request, id), StorageItem.CreateSealed(request)); + + //Add the id to the IdList + using (var sealInterop = engine.SnapshotCache.GetAndChange + (CreateStorageKey(Prefix_IdList, GetUrlHash(url)), () => new StorageItem(new IdList())) + .GetInteroperable(out IdList list)) + { + if (list.Count >= 256) + throw new InvalidOperationException("There are too many pending responses for this url"); + list.Add(id); + } + + Notify(engine, "OracleRequest", id, engine.CallingScriptHash, url, filter); + } + + [ContractMethod(CpuFee = 1 << 15)] + private static bool Verify(ApplicationEngine engine) + { + Transaction? tx = (Transaction?)engine.ScriptContainer; + return tx?.GetAttribute() != null; + } + + private class IdList : InteroperableList + { + protected override ulong ElementFromStackItem(StackItem item) + { + return (ulong)item.GetInteger(); + } + + protected override StackItem ElementToStackItem(ulong element, IReferenceCounter? referenceCounter) + { + return element; + } + } +} diff --git a/src/Neo/SmartContract/Native/OracleRequest.cs b/src/Neo/SmartContract/Native/OracleRequest.cs new file mode 100644 index 0000000000..0e0efbbe99 --- /dev/null +++ b/src/Neo/SmartContract/Native/OracleRequest.cs @@ -0,0 +1,84 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// OracleRequest.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.VM; +using Neo.VM.Types; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Native; + +/// +/// Represents an Oracle request in smart contracts. +/// +public class OracleRequest : IInteroperable +{ + /// + /// The original transaction that sent the related request. + /// + public required UInt256 OriginalTxid; + + /// + /// The maximum amount of GAS that can be used when executing response callback. + /// + public long GasForResponse; + + /// + /// The url of the request. + /// + public required string Url; + + /// + /// The filter for the response. + /// + public string? Filter; + + /// + /// The hash of the callback contract. + /// + public required UInt160 CallbackContract; + + /// + /// The name of the callback method. + /// + public required string CallbackMethod; + + /// + /// The user-defined object that will be passed to the callback. + /// + public required byte[] UserData; + + public void FromStackItem(StackItem stackItem) + { + Array array = (Array)stackItem; + OriginalTxid = new UInt256(array[0].GetSpan()); + GasForResponse = (long)array[1].GetInteger(); + Url = array[2].GetString()!; + Filter = array[3].GetString(); + CallbackContract = new UInt160(array[4].GetSpan()); + CallbackMethod = array[5].GetString()!; + UserData = array[6].GetSpan().ToArray(); + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Array(referenceCounter) + { + OriginalTxid.ToArray(), + GasForResponse, + Url, + Filter ?? StackItem.Null, + CallbackContract.ToArray(), + CallbackMethod, + UserData + }; + } +} diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs new file mode 100644 index 0000000000..4a86bd439a --- /dev/null +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -0,0 +1,386 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// PolicyContract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable IDE0051 + +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Iterators; +using Neo.SmartContract.Manifest; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// A native contract that manages the system policies. +/// +[ContractEvent(0, name: WhitelistChangedEventName, + "contract", ContractParameterType.Hash160, + "method", ContractParameterType.String, + "argCount", ContractParameterType.Integer, + "fee", ContractParameterType.Any +)] +public sealed class PolicyContract : NativeContract +{ + /// + /// The default execution fee factor. + /// + public const uint DefaultExecFeeFactor = 30; + + /// + /// The default storage price. + /// + public const uint DefaultStoragePrice = 100000; + + /// + /// The default network fee per byte of transactions. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS + /// + public const uint DefaultFeePerByte = 1000; + + /// + /// The default fee for attribute. + /// + public const uint DefaultAttributeFee = 0; + + /// + /// The default fee for NotaryAssisted attribute. + /// + public const uint DefaultNotaryAssistedAttributeFee = 1000_0000; + + /// + /// The maximum execution fee factor that the committee can set. + /// + public const uint MaxExecFeeFactor = 100; + + /// + /// The maximum fee for attribute that the committee can set. + /// + public const uint MaxAttributeFee = 10_0000_0000; + + /// + /// The maximum storage price that the committee can set. + /// + public const uint MaxStoragePrice = 10000000; + + private const byte Prefix_FeePerByte = 10; + private const byte Prefix_BlockedAccount = 15; + private const byte Prefix_WhitelistedFeeContracts = 16; + private const byte Prefix_ExecFeeFactor = 18; + private const byte Prefix_StoragePrice = 19; + private const byte Prefix_AttributeFee = 20; + + private readonly StorageKey _feePerByte; + private readonly StorageKey _execFeeFactor; + private readonly StorageKey _storagePrice; + + private const string WhitelistChangedEventName = "WhitelistFeeChanged"; + + internal PolicyContract() : base(-7) + { + _feePerByte = CreateStorageKey(Prefix_FeePerByte); + _execFeeFactor = CreateStorageKey(Prefix_ExecFeeFactor); + _storagePrice = CreateStorageKey(Prefix_StoragePrice); + } + + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) + { + if (hardfork == ActiveIn) + { + engine.SnapshotCache.Add(_feePerByte, new StorageItem(DefaultFeePerByte)); + engine.SnapshotCache.Add(_execFeeFactor, new StorageItem(DefaultExecFeeFactor)); + engine.SnapshotCache.Add(_storagePrice, new StorageItem(DefaultStoragePrice)); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_AttributeFee, (byte)TransactionAttributeType.NotaryAssisted), new StorageItem(DefaultNotaryAssistedAttributeFee)); + } + return ContractTask.CompletedTask; + } + + /// + /// Gets the network fee per transaction byte. + /// + /// The snapshot used to read data. + /// The network fee per transaction byte. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public long GetFeePerByte(IReadOnlyStore snapshot) + { + return (long)(BigInteger)snapshot[_feePerByte]; + } + + /// + /// Gets the execution fee factor. This is a multiplier that can be adjusted by the committee to adjust the system fees for transactions. + /// + /// The snapshot used to read data. + /// The execution fee factor. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public uint GetExecFeeFactor(IReadOnlyStore snapshot) + { + return (uint)(BigInteger)snapshot[_execFeeFactor]; + } + + /// + /// Gets the storage price. + /// + /// The snapshot used to read data. + /// The storage price. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public uint GetStoragePrice(IReadOnlyStore snapshot) + { + return (uint)(BigInteger)snapshot[_storagePrice]; + } + + /// + /// Gets the fee for attribute. + /// + /// The snapshot used to read data. + /// Attribute type + /// The fee for attribute. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public uint GetAttributeFee(IReadOnlyStore snapshot, byte attributeType) + { + if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType)) + throw new InvalidOperationException($"Attribute type {attributeType} is not supported."); + var key = CreateStorageKey(Prefix_AttributeFee, attributeType); + return snapshot.TryGet(key, out var item) ? (uint)(BigInteger)item : DefaultAttributeFee; + } + + /// + /// Determines whether the specified account is blocked. + /// + /// The snapshot used to read data. + /// The account to be checked. + /// if the account is blocked; otherwise, . + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public bool IsBlocked(IReadOnlyStore snapshot, UInt160 account) + { + return snapshot.Contains(CreateStorageKey(Prefix_BlockedAccount, account)); + } + + /// + /// Sets the fee for attribute. + /// + /// The engine used to check committee witness and read data. + /// Attribute type + /// Attribute fee value + /// The fee for attribute. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetAttributeFee(ApplicationEngine engine, byte attributeType, uint value) + { + if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType)) + throw new InvalidOperationException($"Attribute type {attributeType} is not supported."); + + if (value > MaxAttributeFee) + throw new ArgumentOutOfRangeException(nameof(value), $"AttributeFee must be less than {MaxAttributeFee}"); + + AssertCommittee(engine); + + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_AttributeFee, attributeType), () => new StorageItem(DefaultAttributeFee)).Set(value); + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetFeePerByte(ApplicationEngine engine, long value) + { + if (value < 0 || value > 1_00000000) + throw new ArgumentOutOfRangeException(nameof(value), $"FeePerByte must be between [0, 100000000], got {value}"); + AssertCommittee(engine); + + engine.SnapshotCache.GetAndChange(_feePerByte)!.Set(value); + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetExecFeeFactor(ApplicationEngine engine, uint value) + { + if (value == 0 || value > MaxExecFeeFactor) + throw new ArgumentOutOfRangeException(nameof(value), $"ExecFeeFactor must be between [1, {MaxExecFeeFactor}], got {value}"); + AssertCommittee(engine); + + engine.SnapshotCache.GetAndChange(_execFeeFactor)!.Set(value); + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetStoragePrice(ApplicationEngine engine, uint value) + { + if (value == 0 || value > MaxStoragePrice) + throw new ArgumentOutOfRangeException(nameof(value), $"StoragePrice must be between [1, {MaxStoragePrice}], got {value}"); + AssertCommittee(engine); + + engine.SnapshotCache.GetAndChange(_storagePrice)!.Set(value); + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private async ContractTask BlockAccount(ApplicationEngine engine, UInt160 account) + { + AssertCommittee(engine); + return await BlockAccountInternal(engine, account); + } + + internal async ContractTask BlockAccountInternal(ApplicationEngine engine, UInt160 account) + { + if (IsNative(account)) throw new InvalidOperationException("Cannot block a native contract."); + + var key = CreateStorageKey(Prefix_BlockedAccount, account); + if (engine.SnapshotCache.Contains(key)) return false; + + await NEO.VoteInternal(engine, account, null); + + engine.SnapshotCache.Add(key, new StorageItem(Array.Empty())); + return true; + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private bool UnblockAccount(ApplicationEngine engine, UInt160 account) + { + AssertCommittee(engine); + + + var key = CreateStorageKey(Prefix_BlockedAccount, account); + if (!engine.SnapshotCache.Contains(key)) return false; + + engine.SnapshotCache.Delete(key); + return true; + } + + internal bool IsWhitelistFeeContract(DataCache snapshot, UInt160 contractHash, ContractMethodDescriptor method, [NotNullWhen(true)] out long? fixedFee) + { + // Check contract existence + + var currentContract = ContractManagement.GetContract(snapshot, contractHash); + + if (currentContract != null) + { + // Check state existence + + var item = snapshot.TryGet(CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method.Offset)); + + if (item != null) + { + fixedFee = (long)(BigInteger)item; + return true; + } + } + + fixedFee = null; + return false; + } + + /// + /// Remove whitelisted Fee contracts + /// + /// The execution engine. + /// The contract to set the whitelist + /// Method + /// Argument count + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private void RemoveWhitelistFeeContract(ApplicationEngine engine, UInt160 contractHash, string method, int argCount) + { + if (!CheckCommittee(engine)) throw new InvalidOperationException("Invalid committee signature"); + + // Validate methods + var contract = ContractManagement.GetContract(engine.SnapshotCache, contractHash) + ?? throw new InvalidOperationException("Is not a valid contract"); + + // If exists multiple instance a exception is throwed + var methodDescriptor = contract.Manifest.Abi.Methods.SingleOrDefault(u => u.Name == method && u.Parameters.Length == argCount) ?? + throw new InvalidOperationException($"Method {method} with {argCount} args was not found in {contractHash}"); + var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, methodDescriptor.Offset); + + if (!engine.SnapshotCache.Contains(key)) throw new InvalidOperationException("Whitelist not found"); + + engine.SnapshotCache.Delete(key); + + // Emit event + Notify(engine, WhitelistChangedEventName, contractHash, method, argCount, null); + } + + internal int CleanWhitelist(ApplicationEngine engine, ContractState contract) + { + var count = 0; + var searchKey = CreateStorageKey(Prefix_WhitelistedFeeContracts, contract.Hash); + + foreach ((var key, _) in engine.SnapshotCache.Find(searchKey, SeekDirection.Forward)) + { + engine.SnapshotCache.Delete(key); + count++; + + // Emit event recovering the values from the Key + + var keyData = key.ToArray().AsSpan(); + var methodOffset = BinaryPrimitives.ReadInt32BigEndian(keyData.Slice(sizeof(int) + sizeof(byte) + UInt160.Length, sizeof(int))); + + // Get method for event + var method = contract.Manifest.Abi.Methods.FirstOrDefault(m => m.Offset == methodOffset); + + Notify(engine, WhitelistChangedEventName, contract.Hash, method?.Name, method?.Parameters.Length, null); + } + + return count; + } + + /// + /// Set whitelisted Fee contracts + /// + /// The execution engine. + /// The contract to set the whitelist + /// Method + /// Argument count + /// Fixed execution fee + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + internal void SetWhitelistFeeContract(ApplicationEngine engine, UInt160 contractHash, string method, int argCount, long fixedFee) + { + ArgumentOutOfRangeException.ThrowIfNegative(fixedFee, nameof(fixedFee)); + + if (!CheckCommittee(engine)) throw new InvalidOperationException("Invalid committee signature"); + + // Validate methods + var contract = ContractManagement.GetContract(engine.SnapshotCache, contractHash) + ?? throw new InvalidOperationException("Is not a valid contract"); + + if (contract.Manifest.Abi.GetMethod(method, argCount) is null) + throw new InvalidOperationException($"{method} with {argCount} args is not a valid method of {contractHash}"); + + // If exists multiple instance a exception is throwed + var methodDescriptor = contract.Manifest.Abi.Methods.SingleOrDefault(u => u.Name == method && u.Parameters.Length == argCount) ?? + throw new InvalidOperationException($"Method {method} with {argCount} args was not found in {contractHash}"); + var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, methodDescriptor.Offset); + + // Set + var entry = engine.SnapshotCache + .GetAndChange(key, () => new StorageItem(fixedFee)); + + entry.Set(fixedFee); + + // Emit event + + Notify(engine, WhitelistChangedEventName, contractHash, method, argCount, fixedFee); + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + internal StorageIterator GetWhitelistFeeContracts(DataCache snapshot) + { + const FindOptions options = FindOptions.RemovePrefix | FindOptions.KeysOnly; + var enumerator = snapshot + .Find(CreateStorageKey(Prefix_WhitelistedFeeContracts), SeekDirection.Forward) + .GetEnumerator(); + + return new StorageIterator(enumerator, 1, options); + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + private StorageIterator GetBlockedAccounts(DataCache snapshot) + { + const FindOptions options = FindOptions.RemovePrefix | FindOptions.KeysOnly; + var enumerator = snapshot + .Find(CreateStorageKey(Prefix_BlockedAccount), SeekDirection.Forward) + .GetEnumerator(); + return new StorageIterator(enumerator, 1, options); + } +} diff --git a/src/Neo/SmartContract/Native/Role.cs b/src/Neo/SmartContract/Native/Role.cs new file mode 100644 index 0000000000..09a00f9fce --- /dev/null +++ b/src/Neo/SmartContract/Native/Role.cs @@ -0,0 +1,38 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Role.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.SmartContract.Native; + +/// +/// Represents the roles in the NEO system. +/// +public enum Role : byte +{ + /// + /// The validators of state. Used to generate and sign the state root. + /// + StateValidator = 4, + + /// + /// The nodes used to process Oracle requests. + /// + Oracle = 8, + + /// + /// NeoFS Alphabet nodes. + /// + NeoFSAlphabetNode = 16, + + /// + /// P2P Notary nodes used to process P2P notary requests. + /// + P2PNotary = 32 +} diff --git a/src/Neo/SmartContract/Native/RoleManagement.cs b/src/Neo/SmartContract/Native/RoleManagement.cs new file mode 100644 index 0000000000..f88d4d1a18 --- /dev/null +++ b/src/Neo/SmartContract/Native/RoleManagement.cs @@ -0,0 +1,92 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// RoleManagement.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.Persistence; +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.SmartContract.Native; + +/// +/// A native contract for managing roles in NEO system. +/// +[ContractEvent(0, name: "Designation", + "Role", ContractParameterType.Integer, + "BlockIndex", ContractParameterType.Integer, + "Old", ContractParameterType.Array, + "New", ContractParameterType.Array + )] +public sealed class RoleManagement : NativeContract +{ + internal RoleManagement() : base(-8) { } + + /// + /// Gets the list of nodes for the specified role. + /// + /// The snapshot used to read data. + /// The type of the role. + /// The index of the block to be queried. + /// The public keys of the nodes. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public ECPoint[] GetDesignatedByRole(DataCache snapshot, Role role, uint index) + { + if (!Enum.IsDefined(role)) + throw new ArgumentOutOfRangeException(nameof(role), $"Role {role} is not valid"); + + var currentIndex = Ledger.CurrentIndex(snapshot); + if (currentIndex + 1 < index) + throw new ArgumentOutOfRangeException(nameof(index), $"Index {index} exceeds current index + 1 ({currentIndex + 1})"); + var key = CreateStorageKey((byte)role, index).ToArray(); + var boundary = CreateStorageKey((byte)role).ToArray(); + return snapshot.FindRange(key, boundary, SeekDirection.Backward) + .Select(u => u.Value.GetInteroperable().ToArray()) + .FirstOrDefault() ?? []; + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private void DesignateAsRole(ApplicationEngine engine, Role role, ECPoint[] nodes) + { + if (nodes.Length == 0 || nodes.Length > 32) + throw new ArgumentException($"Nodes count {nodes.Length} must be between 1 and 32", nameof(nodes)); + if (!Enum.IsDefined(role)) + throw new ArgumentOutOfRangeException(nameof(role), $"Role {role} is not valid"); + AssertCommittee(engine); + + if (engine.PersistingBlock is null) + throw new InvalidOperationException("Persisting block is null"); + var index = engine.PersistingBlock.Index + 1; + var key = CreateStorageKey((byte)role, index); + if (engine.SnapshotCache.Contains(key)) + throw new InvalidOperationException("Role already designated"); + + NodeList list = new(); + list.AddRange(nodes); + list.Sort(); + engine.SnapshotCache.Add(key, new StorageItem(list)); + var oldNodes = GetDesignatedByRole(engine.SnapshotCache, role, index - 1); + Notify(engine, "Designation", role, engine.PersistingBlock.Index, oldNodes, nodes); + } + + private class NodeList : InteroperableList + { + protected override ECPoint ElementFromStackItem(StackItem item) + { + return ECPoint.DecodePoint(item.GetSpan(), ECCurve.Secp256r1); + } + + protected override StackItem ElementToStackItem(ECPoint element, IReferenceCounter? referenceCounter) + { + return element.ToArray(); + } + } +} diff --git a/src/Neo/SmartContract/Native/StdLib.cs b/src/Neo/SmartContract/Native/StdLib.cs new file mode 100644 index 0000000000..2c2fa00171 --- /dev/null +++ b/src/Neo/SmartContract/Native/StdLib.cs @@ -0,0 +1,276 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StdLib.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable IDE0051 + +using Microsoft.IdentityModel.Tokens; +using Neo.Cryptography; +using Neo.Json; +using Neo.VM.Types; +using System.Globalization; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// A native contract library that provides useful functions. +/// +public sealed class StdLib : NativeContract +{ + private const int MaxInputLength = 1024; + + internal StdLib() : base(-2) { } + + [ContractMethod(CpuFee = 1 << 12)] + private static byte[] Serialize(ApplicationEngine engine, StackItem item) + { + return BinarySerializer.Serialize(item, engine.Limits); + } + + [ContractMethod(CpuFee = 1 << 14)] + private static StackItem Deserialize(ApplicationEngine engine, byte[] data) + { + return BinarySerializer.Deserialize(data, engine.Limits, engine.ReferenceCounter); + } + + [ContractMethod(CpuFee = 1 << 12)] + private static byte[] JsonSerialize(ApplicationEngine engine, StackItem item) + { + return JsonSerializer.SerializeToByteArray(item, engine.Limits.MaxItemSize); + } + + [ContractMethod(CpuFee = 1 << 14)] + private static StackItem JsonDeserialize(ApplicationEngine engine, byte[] json) + { + JToken? token = JToken.Parse(json, 10); + if (token is null) return StackItem.Null; + return JsonSerializer.Deserialize(engine, token, engine.Limits, engine.ReferenceCounter); + } + + /// + /// Converts an integer to a . + /// + /// The integer to convert. + /// The converted . + [ContractMethod(CpuFee = 1 << 12)] + public static string Itoa(BigInteger value) + { + return Itoa(value, 10); + } + + /// + /// Converts an integer to a . + /// + /// The integer to convert. + /// The base of the integer. Only support 10 and 16. + /// The converted . + [ContractMethod(CpuFee = 1 << 12)] + public static string Itoa(BigInteger value, int @base) + { + return @base switch + { + 10 => value.ToString(), + 16 => value.ToString("x"), + _ => throw new ArgumentOutOfRangeException(nameof(@base), $"Invalid base: {@base}") + }; + } + + /// + /// Converts a to an integer. + /// + /// The to convert. + /// The converted integer. + [ContractMethod(CpuFee = 1 << 6)] + public static BigInteger Atoi([Length(MaxInputLength)] string value) + { + return Atoi(value, 10); + } + + /// + /// Converts a to an integer. + /// + /// The to convert. + /// The base of the integer. Only support 10 and 16. + /// The converted integer. + [ContractMethod(CpuFee = 1 << 6)] + public static BigInteger Atoi([Length(MaxInputLength)] string value, int @base) + { + return @base switch + { + 10 => BigInteger.Parse(value, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture), + 16 => BigInteger.Parse(value, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture), + _ => throw new ArgumentOutOfRangeException(nameof(@base), $"Invalid base: {@base}") + }; + } + + /// + /// Encodes a byte array into a base64 . + /// + /// The byte array to be encoded. + /// The encoded . + [ContractMethod(CpuFee = 1 << 5)] + public static string Base64Encode([Length(MaxInputLength)] byte[] data) + { + return Convert.ToBase64String(data); + } + + /// + /// Decodes a byte array from a base64 . + /// + /// The base64 . + /// The decoded byte array. + [ContractMethod(CpuFee = 1 << 5)] + public static byte[] Base64Decode([Length(MaxInputLength)] string s) + { + return Convert.FromBase64String(s); + } + + /// + /// Encodes a byte array into a base64Url string. + /// + /// The base64Url to be encoded. + /// The encoded base64Url string. + [ContractMethod(CpuFee = 1 << 5)] + public static string Base64UrlEncode([Length(MaxInputLength)] string data) + { + return Base64UrlEncoder.Encode(data); + } + + /// + /// Decodes a byte array from a base64Url string. + /// + /// The base64Url string. + /// The decoded base64Url string. + [ContractMethod(CpuFee = 1 << 5)] + public static string Base64UrlDecode([Length(MaxInputLength)] string s) + { + return Base64UrlEncoder.Decode(s); + } + + /// + /// Encodes a byte array into a base58 . + /// + /// The byte array to be encoded. + /// The encoded . + [ContractMethod(CpuFee = 1 << 13)] + public static string Base58Encode([Length(MaxInputLength)] byte[] data) + { + return Base58.Encode(data); + } + + /// + /// Decodes a byte array from a base58 . + /// + /// The base58 . + /// The decoded byte array. + [ContractMethod(CpuFee = 1 << 10)] + public static byte[] Base58Decode([Length(MaxInputLength)] string s) + { + return Base58.Decode(s); + } + + /// + /// Converts a byte array to its equivalent representation that is encoded with base-58 digits. The encoded contains the checksum of the binary data. + /// + /// The byte array to be encoded. + /// The encoded . + [ContractMethod(CpuFee = 1 << 16)] + public static string Base58CheckEncode([Length(MaxInputLength)] byte[] data) + { + return Base58.Base58CheckEncode(data); + } + + /// + /// Converts the specified , which encodes binary data as base-58 digits, to an equivalent byte array. The encoded contains the checksum of the binary data. + /// + /// The base58 . + /// The decoded byte array. + [ContractMethod(CpuFee = 1 << 16)] + public static byte[] Base58CheckDecode([Length(MaxInputLength)] string s) + { + return Base58.Base58CheckDecode(s); + } + + [ContractMethod(CpuFee = 1 << 5)] + private static string HexEncode([Length(MaxInputLength)] byte[] bytes) + { + return bytes.ToHexString(); + } + + [ContractMethod(CpuFee = 1 << 5)] + private static byte[] HexDecode([Length(MaxInputLength)] string str) + { + return str.HexToBytes(); + } + + [ContractMethod(CpuFee = 1 << 5)] + private static int MemoryCompare([Length(MaxInputLength)] byte[] str1, [Length(MaxInputLength)] byte[] str2) + { + return Math.Sign(str1.AsSpan().SequenceCompareTo(str2)); + } + + [ContractMethod(CpuFee = 1 << 6)] + private static int MemorySearch([Length(MaxInputLength)] byte[] mem, byte[] value) + { + return MemorySearch(mem, value, 0, false); + } + + [ContractMethod(CpuFee = 1 << 6)] + private static int MemorySearch([Length(MaxInputLength)] byte[] mem, byte[] value, int start) + { + return MemorySearch(mem, value, start, false); + } + + [ContractMethod(CpuFee = 1 << 6)] + private static int MemorySearch([Length(MaxInputLength)] byte[] mem, byte[] value, int start, bool backward) + { + if (backward) + { + return mem.AsSpan(0, start).LastIndexOf(value); + } + else + { + int index = mem.AsSpan(start).IndexOf(value); + if (index < 0) return -1; + return index + start; + } + } + + [ContractMethod(CpuFee = 1 << 8)] + private static string[] StringSplit([Length(MaxInputLength)] string str, string separator) + { + return str.Split(separator); + } + + [ContractMethod(CpuFee = 1 << 8)] + private static string[] StringSplit([Length(MaxInputLength)] string str, string separator, bool removeEmptyEntries) + { + StringSplitOptions options = removeEmptyEntries ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None; + return str.Split(separator, options); + } + + [ContractMethod(CpuFee = 1 << 8)] + private static int StrLen([Length(MaxInputLength)] string str) + { + // return the length of the string in elements + // it should return 1 for both "🦆" and "ã" + + TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(str); + int count = 0; + + while (enumerator.MoveNext()) + { + count++; + } + + return count; + } +} diff --git a/src/Neo/SmartContract/Native/TokenManagement.Fungible.cs b/src/Neo/SmartContract/Native/TokenManagement.Fungible.cs new file mode 100644 index 0000000000..cb3e736f2b --- /dev/null +++ b/src/Neo/SmartContract/Native/TokenManagement.Fungible.cs @@ -0,0 +1,153 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TokenManagement.Fungible.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM.Types; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +[ContractEvent(1, "Transfer", "assetId", ContractParameterType.Hash160, "from", ContractParameterType.Hash160, "to", ContractParameterType.Hash160, "amount", ContractParameterType.Integer)] +partial class TokenManagement +{ + static readonly BigInteger MaxMintAmount = BigInteger.Pow(2, 128); + + /// + /// Creates a new token with an unlimited maximum supply. + /// + /// The current instance. + /// The token name (1-32 characters). + /// The token symbol (2-6 characters). + /// The number of decimals (0-18). + /// The asset identifier generated for the new token. + /// If parameter constraints are violated. + /// If a token with the same id already exists. + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + internal UInt160 Create(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, [Range(0, 18)] byte decimals) + { + return Create(engine, name, symbol, decimals, BigInteger.MinusOne); + } + + /// + /// Creates a new token with a specified maximum supply. + /// + /// The current instance. + /// The token name (1-32 characters). + /// The token symbol (2-6 characters). + /// The number of decimals (0-18). + /// Maximum total supply, or -1 for unlimited. + /// The asset identifier generated for the new token. + /// If is less than -1. + /// If a token with the same id already exists. + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + internal UInt160 Create(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, [Range(0, 18)] byte decimals, BigInteger maxSupply) + { + ArgumentOutOfRangeException.ThrowIfLessThan(maxSupply, BigInteger.MinusOne); + UInt160 owner = engine.CallingScriptHash!; + UInt160 tokenid = GetAssetId(owner, name); + StorageKey key = CreateStorageKey(Prefix_TokenState, tokenid); + if (engine.SnapshotCache.Contains(key)) + throw new InvalidOperationException($"{name} already exists."); + var state = new TokenState + { + Type = TokenType.Fungible, + Owner = owner, + Name = name, + Symbol = symbol, + Decimals = decimals, + TotalSupply = BigInteger.Zero, + MaxSupply = maxSupply + }; + engine.SnapshotCache.Add(key, new(state)); + Notify(engine, "Created", tokenid, TokenType.Fungible); + return tokenid; + } + + /// + /// Mints new tokens to an account. Only the token owner contract may call this method. + /// + /// The current instance. + /// The asset identifier. + /// The recipient account . + /// The amount to mint (must be > 0 and <= ). + /// A representing the asynchronous operation. + /// If is invalid. + /// If the asset id does not exist or caller is not the owner or max supply would be exceeded. + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)] + internal async Task Mint(ApplicationEngine engine, UInt160 assetId, UInt160 account, BigInteger amount) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount); + ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, MaxMintAmount); + AddTotalSupply(engine, TokenType.Fungible, assetId, amount, assertOwner: true); + AddBalance(engine.SnapshotCache, assetId, account, amount); + await PostTransferAsync(engine, assetId, null, account, amount, StackItem.Null, callOnPayment: true); + } + + /// + /// Burns tokens from an account, decreasing the total supply. Only the token owner contract may call this method. + /// + /// The current instance. + /// The asset identifier. + /// The account from which tokens will be burned. + /// The amount to burn (must be > 0 and <= ). + /// A representing the asynchronous operation. + /// If is invalid. + /// If the asset id does not exist, caller is not the owner, or account has insufficient balance. + [ContractMethod(CpuFee = 1 << 17, RequiredCallFlags = CallFlags.All)] + internal async Task Burn(ApplicationEngine engine, UInt160 assetId, UInt160 account, BigInteger amount) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount); + ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, MaxMintAmount); + AddTotalSupply(engine, TokenType.Fungible, assetId, -amount, assertOwner: true); + if (!AddBalance(engine.SnapshotCache, assetId, account, -amount)) + throw new InvalidOperationException("Insufficient balance to burn."); + await PostTransferAsync(engine, assetId, account, null, amount, StackItem.Null, callOnPayment: false); + } + + /// + /// Transfers tokens between accounts. + /// + /// The current instance. + /// The asset identifier. + /// The sender account . + /// The recipient account . + /// The amount to transfer (must be >= 0). + /// Arbitrary data passed to onPayment or onTransfer callbacks. + /// true if the transfer succeeded; otherwise false. + /// If is negative. + /// If the asset id does not exist. + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)] + internal async Task Transfer(ApplicationEngine engine, UInt160 assetId, UInt160 from, UInt160 to, BigInteger amount, StackItem data) + { + ArgumentOutOfRangeException.ThrowIfNegative(amount); + StorageKey key = CreateStorageKey(Prefix_TokenState, assetId); + TokenState token = engine.SnapshotCache.TryGet(key)?.GetInteroperable() + ?? throw new InvalidOperationException("The asset id does not exist."); + if (token.Type != TokenType.Fungible) + throw new InvalidOperationException("The asset id and the token type do not match."); + if (!engine.CheckWitnessInternal(from)) return false; + if (!amount.IsZero && from != to) + { + if (!AddBalance(engine.SnapshotCache, assetId, from, -amount)) + return false; + AddBalance(engine.SnapshotCache, assetId, to, amount); + } + await PostTransferAsync(engine, assetId, from, to, amount, data, callOnPayment: true); + await engine.CallFromNativeContractAsync(Hash, token.Owner, "_onTransfer", assetId, from, to, amount, data); + return true; + } + + async ContractTask PostTransferAsync(ApplicationEngine engine, UInt160 assetId, UInt160? from, UInt160? to, BigInteger amount, StackItem data, bool callOnPayment) + { + Notify(engine, "Transfer", assetId, from, to, amount); + if (!callOnPayment || to is null || !ContractManagement.IsContract(engine.SnapshotCache, to)) return; + await engine.CallFromNativeContractAsync(Hash, to, "_onPayment", assetId, from, amount, data); + } +} diff --git a/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs b/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs new file mode 100644 index 0000000000..3ed55cc077 --- /dev/null +++ b/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs @@ -0,0 +1,289 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TokenManagement.NonFungible.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.SmartContract.Iterators; +using Neo.VM.Types; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +[ContractEvent(2, "NFTTransfer", "uniqueId", ContractParameterType.Hash160, "from", ContractParameterType.Hash160, "to", ContractParameterType.Hash160)] +partial class TokenManagement +{ + const byte Prefix_NFTUniqueIdSeed = 15; + const byte Prefix_NFTState = 8; + const byte Prefix_NFTOwnerUniqueIdIndex = 21; + const byte Prefix_NFTAssetIdUniqueIdIndex = 23; + + partial void Initialize_NonFungible(ApplicationEngine engine, Hardfork? hardfork) + { + if (hardfork == ActiveIn) + { + engine.SnapshotCache.Add(CreateStorageKey(Prefix_NFTUniqueIdSeed), BigInteger.Zero); + } + } + + /// + /// Creates a new NFT collection with an unlimited maximum supply. + /// + /// The current instance. + /// The NFT collection name (1-32 characters). + /// The NFT collection symbol (2-6 characters). + /// The asset identifier generated for the new NFT collection. + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + internal UInt160 CreateNonFungible(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol) + { + return CreateNonFungible(engine, name, symbol, BigInteger.MinusOne); + } + + /// + /// Creates a new NFT collection with a specified maximum supply. + /// + /// The current instance. + /// The NFT collection name (1-32 characters). + /// The NFT collection symbol (2-6 characters). + /// Maximum total supply for NFTs in this collection, or -1 for unlimited. + /// The asset identifier generated for the new NFT collection. + /// If is less than -1. + /// If a collection with the same id already exists. + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + internal UInt160 CreateNonFungible(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, BigInteger maxSupply) + { + ArgumentOutOfRangeException.ThrowIfLessThan(maxSupply, BigInteger.MinusOne); + UInt160 owner = engine.CallingScriptHash!; + UInt160 tokenid = GetAssetId(owner, name); + StorageKey key = CreateStorageKey(Prefix_TokenState, tokenid); + if (engine.SnapshotCache.Contains(key)) + throw new InvalidOperationException($"{name} already exists."); + var state = new TokenState + { + Type = TokenType.NonFungible, + Owner = owner, + Name = name, + Symbol = symbol, + Decimals = 0, + TotalSupply = BigInteger.Zero, + MaxSupply = maxSupply + }; + engine.SnapshotCache.Add(key, new(state)); + Notify(engine, "Created", tokenid, TokenType.NonFungible); + return tokenid; + } + + /// + /// Mints a new NFT for the given collection to the specified account using empty properties. + /// + /// The current instance. + /// The NFT collection asset identifier. + /// The recipient account . + /// The unique id () of the newly minted NFT. + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)] + internal async Task MintNFT(ApplicationEngine engine, UInt160 assetId, UInt160 account) + { + return await MintNFT(engine, assetId, account, new Map(engine.ReferenceCounter)); + } + + /// + /// Mints a new NFT for the given collection to the specified account with provided properties. + /// + /// The current instance. + /// The NFT collection asset identifier. + /// The recipient account . + /// A of properties for the NFT (keys: ByteString, values: ByteString or Buffer). + /// The unique id () of the newly minted NFT. + /// If properties are invalid (too many, invalid key/value types or lengths). + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 10, RequiredCallFlags = CallFlags.All)] + internal async Task MintNFT(ApplicationEngine engine, UInt160 assetId, UInt160 account, Map properties) + { + if (properties.Count > 8) + throw new ArgumentException("Too many properties.", nameof(properties)); + foreach (var (k, v) in properties) + { + if (k is not ByteString) + throw new ArgumentException("The key of a property should be a ByteString.", nameof(properties)); + if (k.Size < 1 || k.Size > 16) + throw new ArgumentException("The key length of a property should be between 1 and 16.", nameof(properties)); + k.GetString(); // Ensure to invoke `ToStrictUtf8String()` + switch (v) + { + case ByteString bs: + if (bs.Size < 1 || bs.Size > 128) + throw new ArgumentException("The value length of a property should be between 1 and 128.", nameof(properties)); + break; + case VM.Types.Buffer buffer: + if (buffer.Size < 1 || buffer.Size > 128) + throw new ArgumentException("The value length of a property should be between 1 and 128.", nameof(properties)); + break; + default: + throw new ArgumentException("The value of a property should be a ByteString or Buffer.", nameof(properties)); + } + v.GetString(); // Ensure to invoke `ToStrictUtf8String()` + } + AddTotalSupply(engine, TokenType.NonFungible, assetId, 1, assertOwner: true); + AddBalance(engine.SnapshotCache, assetId, account, 1); + UInt160 uniqueId = GetNextNFTUniqueId(engine); + StorageKey key = CreateStorageKey(Prefix_NFTAssetIdUniqueIdIndex, assetId, uniqueId); + engine.SnapshotCache.Add(key, new()); + key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, account, uniqueId); + engine.SnapshotCache.Add(key, new()); + key = CreateStorageKey(Prefix_NFTState, uniqueId); + engine.SnapshotCache.Add(key, new(new NFTState + { + AssetId = assetId, + Owner = account, + Properties = (Map)properties.DeepCopy(asImmutable: true) + })); + await PostNFTTransferAsync(engine, uniqueId, null, account, StackItem.Null, callOnPayment: true); + return uniqueId; + } + + /// + /// Burns an NFT identified by . Only the owner contract may call this method. + /// + /// The current instance. + /// The unique id of the NFT to burn. + /// A representing the asynchronous operation. + /// If the unique id does not exist or owner has insufficient balance or caller is not owner contract. + [ContractMethod(CpuFee = 1 << 17, RequiredCallFlags = CallFlags.All)] + internal async Task BurnNFT(ApplicationEngine engine, UInt160 uniqueId) + { + StorageKey key = CreateStorageKey(Prefix_NFTState, uniqueId); + NFTState nft = engine.SnapshotCache.TryGet(key)?.GetInteroperable() + ?? throw new InvalidOperationException("The unique id does not exist."); + AddTotalSupply(engine, TokenType.NonFungible, nft.AssetId, BigInteger.MinusOne, assertOwner: true); + if (!AddBalance(engine.SnapshotCache, nft.AssetId, nft.Owner, BigInteger.MinusOne)) + throw new InvalidOperationException("Insufficient balance to burn."); + engine.SnapshotCache.Delete(key); + key = CreateStorageKey(Prefix_NFTAssetIdUniqueIdIndex, nft.AssetId, uniqueId); + engine.SnapshotCache.Delete(key); + key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, nft.Owner, uniqueId); + engine.SnapshotCache.Delete(key); + await PostNFTTransferAsync(engine, uniqueId, nft.Owner, null, StackItem.Null, callOnPayment: false); + } + + /// + /// Transfers an NFT between owners. + /// + /// The current instance. + /// The unique id of the NFT. + /// The current owner account . + /// The recipient account . + /// Arbitrary data passed to onNFTPayment or onNFTTransfer callbacks. + /// true if the transfer succeeded; otherwise false. + /// If the unique id does not exist. + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)] + internal async Task TransferNFT(ApplicationEngine engine, UInt160 uniqueId, UInt160 from, UInt160 to, StackItem data) + { + StorageKey key_nft = CreateStorageKey(Prefix_NFTState, uniqueId); + NFTState nft = engine.SnapshotCache.TryGet(key_nft)?.GetInteroperable() + ?? throw new InvalidOperationException("The unique id does not exist."); + if (nft.Owner != from) return false; + if (!engine.CheckWitnessInternal(from)) return false; + StorageKey key = CreateStorageKey(Prefix_TokenState, nft.AssetId); + TokenState token = engine.SnapshotCache.TryGet(key)!.GetInteroperable(); + if (from != to) + { + if (!AddBalance(engine.SnapshotCache, nft.AssetId, from, BigInteger.MinusOne)) + return false; + AddBalance(engine.SnapshotCache, nft.AssetId, to, BigInteger.One); + key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, from, uniqueId); + engine.SnapshotCache.Delete(key); + key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, to, uniqueId); + engine.SnapshotCache.Add(key, new()); + nft = engine.SnapshotCache.GetAndChange(key_nft)!.GetInteroperable(); + nft.Owner = to; + } + await PostNFTTransferAsync(engine, uniqueId, from, to, data, callOnPayment: true); + await engine.CallFromNativeContractAsync(Hash, token.Owner, "_onNFTTransfer", uniqueId, from, to, data); + return true; + } + + /// + /// Gets NFT metadata for a unique id. + /// + /// A readonly view of the storage. + /// The unique id of the NFT. + /// The if found; otherwise null. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public NFTState? GetNFTInfo(IReadOnlyStore snapshot, UInt160 uniqueId) + { + StorageKey key = CreateStorageKey(Prefix_NFTState, uniqueId); + return snapshot.TryGet(key)?.GetInteroperable(); + } + + /// + /// Returns an iterator over the unique ids of NFTs for the specified asset (collection). + /// The iterator yields the stored unique id keys (UInt160) indexed under the NFT asset id. + /// + /// A readonly view of the storage. + /// The asset (collection) identifier whose NFTs are requested. + /// + /// An that enumerates the NFT unique ids belonging to the given collection. + /// The iterator is configured to return keys only and to remove the storage prefix. + /// + /// Thrown when the specified asset id does not exist. + /// + /// The returned iterator is backed by the storage layer and uses the NFT asset-to-unique-id index. + /// Consumers should dispose the iterator when finished if they hold unmanaged resources from it. + /// + [ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)] + public IIterator GetNFTs(IReadOnlyStore snapshot, UInt160 assetId) + { + StorageKey key = CreateStorageKey(Prefix_TokenState, assetId); + if (!snapshot.Contains(key)) + throw new InvalidOperationException("The asset id does not exist."); + const FindOptions options = FindOptions.KeysOnly | FindOptions.RemovePrefix; + var prefixKey = CreateStorageKey(Prefix_NFTAssetIdUniqueIdIndex, assetId); + var enumerator = snapshot.Find(prefixKey).GetEnumerator(); + return new StorageIterator(enumerator, 21, options); + } + + /// + /// Returns an iterator over the unique ids of NFTs owned by the specified account. + /// The iterator yields the stored unique id keys () indexed under the NFT owner index. + /// + /// A readonly view of the storage. + /// The account whose NFTs are requested. + /// + /// An that enumerates the NFT unique ids owned by the given account. + /// The iterator is configured to return keys only and to remove the storage prefix. + /// + /// + /// The returned iterator is backed by the storage layer and uses the NFT owner-to-unique-id index. + /// Consumers should dispose the iterator when finished if they hold unmanaged resources from it. + /// + [ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)] + public IIterator GetNFTsOfOwner(IReadOnlyStore snapshot, UInt160 account) + { + const FindOptions options = FindOptions.KeysOnly | FindOptions.RemovePrefix; + var prefixKey = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, account); + var enumerator = snapshot.Find(prefixKey).GetEnumerator(); + return new StorageIterator(enumerator, 21, options); + } + + UInt160 GetNextNFTUniqueId(ApplicationEngine engine) + { + StorageKey key = CreateStorageKey(Prefix_NFTUniqueIdSeed); + BigInteger seed = engine.SnapshotCache.GetAndChange(key)!.Add(BigInteger.One); + using MemoryStream ms = new(); + ms.Write(engine.PersistingBlock!.Hash.GetSpan()); + ms.Write(seed.ToByteArrayStandard()); + return ms.ToArray().ToScriptHash(); + } + + async ContractTask PostNFTTransferAsync(ApplicationEngine engine, UInt160 uniqueId, UInt160? from, UInt160? to, StackItem data, bool callOnPayment) + { + Notify(engine, "NFTTransfer", uniqueId, from, to); + if (!callOnPayment || to is null || !ContractManagement.IsContract(engine.SnapshotCache, to)) return; + await engine.CallFromNativeContractAsync(Hash, to, "_onNFTPayment", uniqueId, from, data); + } +} diff --git a/src/Neo/SmartContract/Native/TokenManagement.cs b/src/Neo/SmartContract/Native/TokenManagement.cs new file mode 100644 index 0000000000..1f75815176 --- /dev/null +++ b/src/Neo/SmartContract/Native/TokenManagement.cs @@ -0,0 +1,129 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TokenManagement.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// Provides core functionality for creating, managing, and transferring tokens within a native contract environment. +/// +[ContractEvent(0, "Created", "assetId", ContractParameterType.Hash160, "type", ContractParameterType.Integer)] +public sealed partial class TokenManagement : NativeContract +{ + const byte Prefix_TokenState = 10; + const byte Prefix_AccountState = 12; + + internal TokenManagement() : base(-12) { } + + partial void Initialize_Fungible(ApplicationEngine engine, Hardfork? hardfork); + partial void Initialize_NonFungible(ApplicationEngine engine, Hardfork? hardfork); + + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) + { + Initialize_Fungible(engine, hardfork); + Initialize_NonFungible(engine, hardfork); + return ContractTask.CompletedTask; + } + + /// + /// Retrieves the token metadata for the given asset id. + /// + /// A readonly view of the storage. + /// The asset identifier. + /// The if found; otherwise null. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public TokenState? GetTokenInfo(IReadOnlyStore snapshot, UInt160 assetId) + { + StorageKey key = CreateStorageKey(Prefix_TokenState, assetId); + return snapshot.TryGet(key)?.GetInteroperable(); + } + + /// + /// Returns the balance of for the specified . + /// + /// A readonly view of the storage. + /// The asset identifier. + /// The account whose balance is requested. + /// The account balance as a . + /// If the asset id does not exist. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public BigInteger BalanceOf(IReadOnlyStore snapshot, UInt160 assetId, UInt160 account) + { + StorageKey key = CreateStorageKey(Prefix_TokenState, assetId); + if (!snapshot.Contains(key)) + throw new InvalidOperationException("The asset id does not exist."); + key = CreateStorageKey(Prefix_AccountState, account, assetId); + AccountState? accountState = snapshot.TryGet(key)?.GetInteroperable(); + if (accountState is null) return BigInteger.Zero; + return accountState.Balance; + } + + /// + /// Computes a unique asset id from the token owner's script hash and the token name. + /// + /// Owner contract hash. + /// Token name. + /// The asset id for the token. + public static UInt160 GetAssetId(UInt160 owner, string name) + { + byte[] nameBytes = name.ToStrictUtf8Bytes(); + byte[] buffer = new byte[UInt160.Length + nameBytes.Length]; + owner.Serialize(buffer); + nameBytes.CopyTo(buffer.AsSpan()[UInt160.Length..]); + return buffer.ToScriptHash(); + } + + void AddTotalSupply(ApplicationEngine engine, TokenType type, UInt160 assetId, BigInteger amount, bool assertOwner) + { + StorageKey key = CreateStorageKey(Prefix_TokenState, assetId); + TokenState token = engine.SnapshotCache.GetAndChange(key)?.GetInteroperable() + ?? throw new InvalidOperationException("The asset id does not exist."); + if (token.Type != type) + throw new InvalidOperationException("The asset id and the token type do not match."); + if (assertOwner && token.Owner != engine.CallingScriptHash) + throw new InvalidOperationException("This method can be called by the owner contract only."); + token.TotalSupply += amount; + if (token.TotalSupply < 0) + throw new InvalidOperationException("Insufficient balance to burn."); + if (token.MaxSupply >= 0 && token.TotalSupply > token.MaxSupply) + throw new InvalidOperationException("The total supply exceeds the maximum supply."); + } + + bool AddBalance(DataCache snapshot, UInt160 assetId, UInt160 account, BigInteger amount) + { + if (amount.IsZero) return true; + StorageKey key = CreateStorageKey(Prefix_AccountState, account, assetId); + AccountState? accountState = snapshot.GetAndChange(key)?.GetInteroperable(); + if (amount > 0) + { + if (accountState is null) + { + accountState = new AccountState { Balance = amount }; + snapshot.Add(key, new(accountState)); + } + else + { + accountState.Balance += amount; + } + } + else + { + if (accountState is null) return false; + if (accountState.Balance < -amount) return false; + accountState.Balance += amount; + if (accountState.Balance.IsZero) + snapshot.Delete(key); + } + return true; + } +} diff --git a/src/Neo/SmartContract/Native/TokenState.cs b/src/Neo/SmartContract/Native/TokenState.cs new file mode 100644 index 0000000000..de6d21946e --- /dev/null +++ b/src/Neo/SmartContract/Native/TokenState.cs @@ -0,0 +1,85 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TokenState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// Represents the persisted metadata for a token. +/// Implements to allow conversion to/from VM . +/// +public class TokenState : IInteroperable +{ + /// + /// Specifies the type of token represented by this instance. + /// + public required TokenType Type; + + /// + /// The owner contract script hash that can manage this token (mint/burn, onTransfer callback target). + /// + public required UInt160 Owner; + + /// + /// The token's human-readable name. + /// + public required string Name; + + /// + /// The token's symbol (short string). + /// + public required string Symbol; + + /// + /// Number of decimal places the token supports. + /// + public required byte Decimals; + + /// + /// Current total supply of the token. + /// + public BigInteger TotalSupply; + + /// + /// Maximum total supply allowed; -1 indicates no limit. + /// + public BigInteger MaxSupply; + + /// + /// Populates this instance from a VM representation. + /// + /// A expected to be a with the token fields in order. + public void FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + Type = (TokenType)(byte)@struct[0].GetInteger(); + Owner = new UInt160(@struct[1].GetSpan()); + Name = @struct[2].GetString()!; + Symbol = @struct[3].GetString()!; + Decimals = (byte)@struct[4].GetInteger(); + TotalSupply = @struct[5].GetInteger(); + MaxSupply = @struct[6].GetInteger(); + } + + /// + /// Converts this instance to a VM representation. + /// + /// Optional reference counter used by the VM. + /// A containing the token fields in order. + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { (byte)Type, Owner.ToArray(), Name, Symbol, Decimals, TotalSupply, MaxSupply }; + } +} diff --git a/src/Neo/SmartContract/Native/TokenType.cs b/src/Neo/SmartContract/Native/TokenType.cs new file mode 100644 index 0000000000..5dc13167a2 --- /dev/null +++ b/src/Neo/SmartContract/Native/TokenType.cs @@ -0,0 +1,27 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TokenType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.SmartContract.Native; + +/// +/// Specifies the type of token, indicating whether it is fungible or non-fungible. +/// +public enum TokenType : byte +{ + /// + /// Fungible token type. + /// + Fungible = 1, + /// + /// Non-fungible token (NFT) type. + /// + NonFungible = 2 +} diff --git a/src/Neo/SmartContract/Native/TransactionState.cs b/src/Neo/SmartContract/Native/TransactionState.cs new file mode 100644 index 0000000000..13aa9ce780 --- /dev/null +++ b/src/Neo/SmartContract/Native/TransactionState.cs @@ -0,0 +1,68 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.SmartContract.Native; + +/// +/// Represents a transaction that has been included in a block. +/// +public class TransactionState : IInteroperable +{ + /// + /// The block containing this transaction. + /// + public uint BlockIndex { get; set; } + + /// + /// The transaction, if the transaction is trimmed this value will be null + /// + public Transaction? Transaction { get; set; } + + /// + /// The execution state + /// + public VMState State { get; set; } + + void IInteroperable.FromReplica(IInteroperable replica) + { + var from = (TransactionState)replica; + BlockIndex = from.BlockIndex; + Transaction = from.Transaction; + State = from.State; + } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + var @struct = (Struct)stackItem; + BlockIndex = (uint)@struct[0].GetInteger(); + + // Conflict record. + if (@struct.Count == 1) return; + + // Fully-qualified transaction. + var rawTransaction = ((ByteString)@struct[1]).Memory; + Transaction = rawTransaction.AsSerializable(); + State = (VMState)(byte)@struct[2].GetInteger(); + } + + StackItem IInteroperable.ToStackItem(IReferenceCounter? referenceCounter) + { + if (Transaction is null) + return new Struct(referenceCounter) { BlockIndex }; + return new Struct(referenceCounter) { BlockIndex, Transaction.ToArray(), (byte)State }; + } +} diff --git a/src/Neo/SmartContract/Native/Treasury.cs b/src/Neo/SmartContract/Native/Treasury.cs new file mode 100644 index 0000000000..fe373f16ba --- /dev/null +++ b/src/Neo/SmartContract/Native/Treasury.cs @@ -0,0 +1,58 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Treasury.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.SmartContract.Manifest; +using Neo.VM.Types; +using System.Numerics; + +namespace Neo.SmartContract.Native; + +/// +/// The Treasury native contract used for manage the treasury funds. +/// +public sealed class Treasury : NativeContract +{ + internal Treasury() : base(-11) { } + + protected override void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest) + { + manifest.SupportedStandards = ["NEP-26", "NEP-27"]; + } + + /// + /// Verify checks whether the transaction is signed by the committee. + /// + /// ApplicationEngine + /// Whether transaction is valid. + [ContractMethod(CpuFee = 1 << 5, RequiredCallFlags = CallFlags.ReadStates)] + private bool Verify(ApplicationEngine engine) => CheckCommittee(engine); + + /// + /// OnNEP17Payment callback. + /// + /// ApplicationEngine + /// GAS sender + /// The amount of GAS sent + /// Optional data + [ContractMethod(CpuFee = 1 << 5, RequiredCallFlags = CallFlags.None)] + private void OnNEP17Payment(ApplicationEngine engine, UInt160 from, BigInteger amount, StackItem data) { } + + /// + /// OnNEP11Payment callback. + /// + /// ApplicationEngine + /// GAS sender + /// The amount of GAS sent + /// Nep11 token Id + /// Optional data + [ContractMethod(CpuFee = 1 << 5, RequiredCallFlags = CallFlags.None)] + private void OnNEP11Payment(ApplicationEngine engine, UInt160 from, BigInteger amount, byte[] tokenId, StackItem data) { } +} diff --git a/src/Neo/SmartContract/Native/TrimmedBlock.cs b/src/Neo/SmartContract/Native/TrimmedBlock.cs new file mode 100644 index 0000000000..94b54ca076 --- /dev/null +++ b/src/Neo/SmartContract/Native/TrimmedBlock.cs @@ -0,0 +1,131 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TrimmedBlock.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.VM; +using Neo.VM.Types; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Native; + +/// +/// Represents a block which the transactions are trimmed. +/// +public class TrimmedBlock : IInteroperable, ISerializable +{ + /// + /// The header of the block. + /// + public required Header Header; + + /// + /// The hashes of the transactions of the block. + /// + public required UInt256[] Hashes; + + /// + /// The hash of the block. + /// + public UInt256 Hash => Header.Hash; + + /// + /// The index of the block. + /// + public uint Index => Header.Index; + + public int Size => Header.Size + Hashes.GetVarSize(); + + /// + /// Create Trimmed block + /// + /// Block + /// + public static TrimmedBlock Create(Block block) + { + return Create(block.Header, block.Transactions.Select(p => p.Hash).ToArray()); + } + + /// + /// Create Trimmed block + /// + /// Block header + /// Transaction hashes + /// + public static TrimmedBlock Create(Header header, UInt256[] txHashes) + { + return new TrimmedBlock + { + Header = header, + Hashes = txHashes + }; + } + + public void Deserialize(ref MemoryReader reader) + { + Header = reader.ReadSerializable
(); + Hashes = reader.ReadSerializableArray(ushort.MaxValue); + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(Header); + writer.Write(Hashes); + } + + IInteroperable IInteroperable.Clone() + { + // FromStackItem is not supported so we need to do the copy + + return new TrimmedBlock + { + Header = Header.Clone(), + Hashes = [.. Hashes] + }; + } + + void IInteroperable.FromReplica(IInteroperable replica) + { + var from = (TrimmedBlock)replica; + Header = from.Header; + Hashes = from.Hashes; + } + + void IInteroperable.FromStackItem(StackItem stackItem) + { + throw new NotSupportedException(); + } + + StackItem IInteroperable.ToStackItem(IReferenceCounter? referenceCounter) + { + return new Array(referenceCounter, + [ + // Computed properties + Header.Hash.ToArray(), + + // BlockBase properties + Header.Version, + Header.PrevHash.ToArray(), + Header.MerkleRoot.ToArray(), + Header.Timestamp, + Header.Nonce, + Header.Index, + Header.PrimaryIndex, + Header.NextConsensus.ToArray(), + + // Block properties + Hashes.Length + ]); + } +} diff --git a/src/Neo/SmartContract/NefFile.cs b/src/Neo/SmartContract/NefFile.cs new file mode 100644 index 0000000000..462631ec67 --- /dev/null +++ b/src/Neo/SmartContract/NefFile.cs @@ -0,0 +1,169 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NefFile.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Cryptography; +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.VM; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace Neo.SmartContract; + +/* +┌───────────────────────────────────────────────────────────────────────┐ +│ NEO Executable Format 3 (NEF3) │ +├──────────┬───────────────┬────────────────────────────────────────────┤ +│ Field │ Type │ Comment │ +├──────────┼───────────────┼────────────────────────────────────────────┤ +│ Magic │ uint32 │ Magic header │ +│ Compiler │ byte[64] │ Compiler name and version │ +├──────────┼───────────────┼────────────────────────────────────────────┤ +│ Source │ byte[] │ The url of the source files │ +│ Reserve │ byte │ Reserved for future extensions. Must be 0. │ +│ Tokens │ MethodToken[] │ Method tokens. │ +│ Reserve │ byte[2] │ Reserved for future extensions. Must be 0. │ +│ Script │ byte[] │ Var bytes for the payload │ +├──────────┼───────────────┼────────────────────────────────────────────┤ +│ Checksum │ uint32 │ First four bytes of double SHA256 hash │ +└──────────┴───────────────┴────────────────────────────────────────────┘ +*/ +/// +/// Represents the structure of NEO Executable Format. +/// +public class NefFile : ISerializable +{ + /// + /// NEO Executable Format 3 (NEF3) + /// + private const uint Magic = 0x3346454E; + + /// + /// The name and version of the compiler that generated this nef file. + /// + public required string Compiler { get; set; } + + /// + /// The url of the source files. + /// + public required string Source { get; set; } + + /// + /// The methods that to be called statically. + /// + public required MethodToken[] Tokens { get; set; } + + /// + /// The script of the contract. + /// + public ReadOnlyMemory Script { get; set; } + + /// + /// The checksum of the nef file. + /// + public uint CheckSum { get; set; } + + private const int HeaderSize = + sizeof(uint) + // Magic + 64; // Compiler + + public int Size => + HeaderSize + // Header + Source.GetVarSize() + // Source + 1 + // Reserve + Tokens.GetVarSize() + // Tokens + 2 + // Reserve + Script.GetVarSize() + // Script + sizeof(uint); // Checksum + + /// + /// Parse NefFile from memory + /// + /// Memory + /// Do checksum and MaxItemSize checks + /// NefFile + public static NefFile Parse(ReadOnlyMemory memory, bool verify = true) + { + var reader = new MemoryReader(memory); + var nef = (NefFile)RuntimeHelpers.GetUninitializedObject(typeof(NefFile)); + nef.Deserialize(ref reader, verify); + return nef; + } + + public void Serialize(BinaryWriter writer) + { + SerializeHeader(writer); + writer.WriteVarString(Source); + writer.Write((byte)0); + writer.Write(Tokens); + writer.Write((short)0); + writer.WriteVarBytes(Script.Span); + writer.Write(CheckSum); + } + + private void SerializeHeader(BinaryWriter writer) + { + writer.Write(Magic); + writer.WriteFixedString(Compiler, 64); + } + + public void Deserialize(ref MemoryReader reader) => Deserialize(ref reader, true); + + public void Deserialize(ref MemoryReader reader, bool verify = true) + { + long startPosition = reader.Position; + if (reader.ReadUInt32() != Magic) throw new FormatException("Wrong magic"); + Compiler = reader.ReadFixedString(64); + Source = reader.ReadVarString(256); + if (reader.ReadByte() != 0) throw new FormatException("Reserved bytes must be 0"); + Tokens = reader.ReadSerializableArray(128); + if (reader.ReadUInt16() != 0) throw new FormatException("Reserved bytes must be 0"); + Script = reader.ReadVarMemory((int)ExecutionEngineLimits.Default.MaxItemSize); + if (Script.Length == 0) throw new ArgumentException("Script cannot be empty."); + CheckSum = reader.ReadUInt32(); + if (verify) + { + if (CheckSum != ComputeChecksum(this)) throw new FormatException("CRC verification fail"); + if (reader.Position - startPosition > ExecutionEngineLimits.Default.MaxItemSize) throw new FormatException("Max vm item size exceed"); + } + } + + /// + /// Computes the checksum for the specified nef file. + /// + /// The specified nef file. + /// The checksum of the nef file. + public static uint ComputeChecksum(NefFile file) + { + return BinaryPrimitives.ReadUInt32LittleEndian(Crypto.Hash256(file.ToArray().AsSpan(..^sizeof(uint)))); + } + + /// + /// Converts the nef file to a JSON object. + /// + /// The nef file represented by a JSON object. + public JObject ToJson() + { + return new JObject + { + ["magic"] = Magic, + ["compiler"] = Compiler, + ["source"] = Source, + ["tokens"] = new JArray(Tokens.Select(p => p.ToJson())), + ["script"] = Convert.ToBase64String(Script.Span), + ["checksum"] = CheckSum + }; + } +} diff --git a/src/Neo/SmartContract/NotifyEventArgs.cs b/src/Neo/SmartContract/NotifyEventArgs.cs new file mode 100644 index 0000000000..4543ca63ec --- /dev/null +++ b/src/Neo/SmartContract/NotifyEventArgs.cs @@ -0,0 +1,74 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NotifyEventArgs.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.VM; +using Neo.VM.Types; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract; + +/// +/// The of . +/// +public class NotifyEventArgs : EventArgs, IInteroperable +{ + /// + /// The container that containing the executed script. + /// + public IVerifiable? ScriptContainer { get; } + + /// + /// The script hash of the contract that sends the log. + /// + public UInt160 ScriptHash { get; } + + /// + /// The name of the event. + /// + public string EventName { get; } + + /// + /// The arguments of the event. + /// + public Array State { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The container that containing the executed script. + /// The script hash of the contract that sends the log. + /// The name of the event. + /// The arguments of the event. + public NotifyEventArgs(IVerifiable? container, UInt160 scriptHash, string eventName, Array state) + { + ScriptContainer = container; + ScriptHash = scriptHash; + EventName = eventName; + State = state; + } + + public void FromStackItem(StackItem stackItem) + { + throw new NotSupportedException(); + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Array(referenceCounter) + { + ScriptHash.ToArray(), + EventName, + State.DeepCopy(true) + }; + } +} diff --git a/src/Neo/SmartContract/RangeAttribute.cs b/src/Neo/SmartContract/RangeAttribute.cs new file mode 100644 index 0000000000..232ed26ebe --- /dev/null +++ b/src/Neo/SmartContract/RangeAttribute.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// RangeAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM.Types; + +namespace Neo.SmartContract; + +class RangeAttribute : ValidatorAttribute +{ + public long MinValue { get; } + public long MaxValue { get; } + + public RangeAttribute(long min, long max) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(min, max); + MinValue = min; + MaxValue = max; + } + + public override void Validate(StackItem item) + { + if (item is not Integer) + throw new InvalidOperationException("The input data is not an integer."); + var value = item.GetInteger(); + if (value < MinValue || value > MaxValue) + throw new InvalidOperationException("The value of the input data is out of range."); + } +} diff --git a/src/Neo/SmartContract/StorageContext.cs b/src/Neo/SmartContract/StorageContext.cs new file mode 100644 index 0000000000..3aba0d46a3 --- /dev/null +++ b/src/Neo/SmartContract/StorageContext.cs @@ -0,0 +1,28 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StorageContext.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.SmartContract; + +/// +/// The storage context used to read and write data in smart contracts. +/// +public class StorageContext +{ + /// + /// The id of the contract that owns the context. + /// + public int Id; + + /// + /// Indicates whether the context is read-only. + /// + public bool IsReadOnly; +} diff --git a/src/Neo/SmartContract/StorageItem.cs b/src/Neo/SmartContract/StorageItem.cs new file mode 100644 index 0000000000..5d88d4d32d --- /dev/null +++ b/src/Neo/SmartContract/StorageItem.cs @@ -0,0 +1,309 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StorageItem.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.IO; +using Neo.VM; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Neo.SmartContract; + +/// +/// Represents the values in contract storage. +/// +[DebuggerDisplay("{ToString()}")] +public class StorageItem : ISerializable +{ + private class SealInteroperable(StorageItem item) : IDisposable + { + public readonly StorageItem Item = item; + + public void Dispose() + { + Item.Seal(); + } + } + + private ReadOnlyMemory _value; + private object? _cache; + + public int Size => Value.GetVarSize(); + + /// + /// The byte array value of the . + /// + public ReadOnlyMemory Value + { + get + { + return !_value.IsEmpty ? _value : _value = _cache switch + { + BigInteger bi => bi.ToByteArrayStandard(), + IInteroperable interoperable => BinarySerializer.Serialize(interoperable.ToStackItem(null), ExecutionEngineLimits.Default), + null => ReadOnlyMemory.Empty, + _ => throw new InvalidCastException() + }; + } + set + { + _value = value; + _cache = null; + } + } + + /// + /// Initializes a new instance of the class. + /// + public StorageItem() { } + + /// + /// Initializes a new instance of the class. + /// + /// The byte array value of the . + public StorageItem(byte[] value) + { + _value = value; + } + + /// + /// Initializes a new instance of the class. + /// + /// The integer value of the . + public StorageItem(BigInteger value) + { + _cache = value; + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of the . + public StorageItem(IInteroperable interoperable) + { + _cache = interoperable; + } + + /// + /// Create a new instance from an sealed class. + /// + /// The value of the . + /// class + public static StorageItem CreateSealed(IInteroperable interoperable) + { + var item = new StorageItem(interoperable); + item.Seal(); + return item; + } + + /// + /// Returns true if the class is serializable + /// + /// The value of the . + /// True if serializable + public static bool IsSerializable(IInteroperable interoperable) + { + try + { + _ = CreateSealed(interoperable); + } + catch + { + return false; + } + return true; + } + + /// + /// Ensure that is Serializable and cache the value + /// + public void Seal() + { + // Assert is Serializable and cached + _ = Value; + } + + /// + /// Increases the integer value in the store by the specified value. + /// + /// The integer to add. + public BigInteger Add(BigInteger integer) + { + BigInteger result = this + integer; + Set(result); + return result; + } + + /// + /// Creates a new instance of with the same value as this instance. + /// + /// The created . + public StorageItem Clone() + { + return new() + { + _value = _value, + _cache = _cache is IInteroperable interoperable ? interoperable.Clone() : _cache + }; + } + + public void Deserialize(ref MemoryReader reader) + { + Value = reader.ReadToEnd(); + } + + /// + /// Copies the value of another instance to this instance. + /// + /// The instance to be copied. + public void FromReplica(StorageItem replica) + { + _value = replica._value; + if (replica._cache is IInteroperable interoperable) + { + if (_cache?.GetType() == interoperable.GetType()) + ((IInteroperable)_cache).FromReplica(interoperable); + else + _cache = interoperable.Clone(); + } + else + { + _cache = replica._cache; + } + } + + /// + /// Gets an from the storage. + /// + /// The type of the . + /// The in the storage. + public T GetInteroperable() where T : IInteroperable + { + _cache ??= GetInteroperableClone(); + _value = null; + return (T)_cache; + } + + /// + /// Gets an from the storage. + /// + /// Verify deserialization + /// The type of the . + /// The in the storage. + public T GetInteroperable(bool verify = true) where T : IInteroperableVerifiable + { + _cache ??= GetInteroperableClone(verify); + _value = null; + return (T)_cache; + } + + /// + /// Gets an from the storage not related to this . + /// + /// The type of the . + /// The in the storage. + public T GetInteroperableClone() where T : IInteroperable + { + // If it's interoperable and not sealed + if (_value.IsEmpty && _cache is T interoperable) + { + // Refresh data without change _value + return (T)interoperable.Clone(); + } + + interoperable = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); + interoperable.FromStackItem(BinarySerializer.Deserialize(_value, ExecutionEngineLimits.Default)); + return interoperable; + } + + /// + /// Gets an from the storage not related to this . + /// + /// Verify deserialization + /// The type of the . + /// The in the storage. + public T GetInteroperableClone(bool verify = true) where T : IInteroperableVerifiable + { + // If it's interoperable and not sealed + if (_value.IsEmpty && _cache is T interoperable) + { + return (T)interoperable.Clone(); + } + + interoperable = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); + interoperable.FromStackItem(BinarySerializer.Deserialize(_value, ExecutionEngineLimits.Default), verify); + return interoperable; + } + + /// + /// Gets an from the storage. + /// + /// The in the storage. + /// The type of the . + /// The that seal the item when disposed. + public IDisposable GetInteroperable(out T interop) where T : IInteroperable, new() + { + interop = GetInteroperable(); + return new SealInteroperable(this); + } + + /// + /// Gets an from the storage. + /// + /// The in the storage. + /// Verify deserialization + /// The type of the . + /// The that seal the item when disposed. + public IDisposable GetInteroperable(out T interop, bool verify = true) where T : IInteroperableVerifiable + { + interop = GetInteroperable(verify); + return new SealInteroperable(this); + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(Value.Span); + } + + /// + /// Sets the integer value of the storage. + /// + /// The integer value to set. + public void Set(BigInteger integer) + { + _cache = integer; + _value = null; + } + + public static implicit operator BigInteger(StorageItem item) + { + item._cache ??= new BigInteger(item._value.Span); + return (BigInteger)item._cache; + } + + public static implicit operator StorageItem(BigInteger value) + { + return new StorageItem(value); + } + + public static implicit operator StorageItem(byte[] value) + { + return new StorageItem(value); + } + + public override string ToString() + { + var valueArray = _value.ToArray(); + return $"Value = {{ {string.Join(", ", valueArray.Select(static s => $"0x{s:x02}"))} }}"; + } +} diff --git a/src/Neo/SmartContract/StorageKey.cs b/src/Neo/SmartContract/StorageKey.cs new file mode 100644 index 0000000000..13483d1b9d --- /dev/null +++ b/src/Neo/SmartContract/StorageKey.cs @@ -0,0 +1,144 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StorageKey.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Buffers.Binary; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Neo.SmartContract; + +/// +/// Represents the keys in contract storage. +/// +[DebuggerDisplay("{ToString()}")] +public sealed record StorageKey +{ + /// + /// The id of the contract. + /// + public int Id { get; init; } + + /// + /// The key of the storage entry. + /// + public ReadOnlyMemory Key + { + get => _key; + // The example below shows how you would of been + // able to overwrite keys in the pass + // Example: + // byte[] keyData = [0x00, 0x00, 0x00, 0x00, 0x12]; + // var keyMemory = new ReadOnlyMemory(keyData); + // var storageKey1 = new StorageKey { Id = 0, Key = keyMemory }; + // // Below will overwrite the key in "storageKey1.Key" + // keyData[0] = 0xff; + init => _key = value.ToArray(); // make new region of memory (a copy). + } + + /// + /// Get key length + /// + public int Length + { + get + { + if (_cache is { IsEmpty: true }) + { + _cache = Build(); + } + return _cache.Length; + } + } + + private ReadOnlyMemory _cache; + private readonly ReadOnlyMemory _key; + + // NOTE: StorageKey is readonly, so we can cache the hash code. + private int _hashCode = 0; + + /// + /// Creates a search prefix for a contract. + /// + /// The id of the contract. + /// The prefix of the keys to search. + /// The created search prefix. + public static byte[] CreateSearchPrefix(int id, ReadOnlySpan prefix) + { + var buffer = new byte[sizeof(int) + prefix.Length]; + BinaryPrimitives.WriteInt32LittleEndian(buffer, id); + prefix.CopyTo(buffer.AsSpan(sizeof(int)..)); + return buffer; + } + + public StorageKey() { } + + /// + /// Initializes a new instance of the class. + /// + /// The cached byte array. + internal StorageKey(ReadOnlySpan cache) : this(cache.ToArray(), false) { } + + internal StorageKey(ReadOnlyMemory cache, bool copy) + { + if (copy) cache = cache.ToArray(); + _cache = cache; + Id = BinaryPrimitives.ReadInt32LittleEndian(_cache.Span); + Key = _cache[sizeof(int)..]; // "Key" init makes a copy already. + } + + public bool Equals(StorageKey? other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + return Id == other.Id && Key.Span.SequenceEqual(other.Key.Span); + } + + public override int GetHashCode() + { + if (_hashCode == 0) + _hashCode = HashCode.Combine(Id, Key.Span.XxHash3_32()); + return _hashCode; + } + + public byte[] ToArray() + { + if (_cache is { IsEmpty: true }) + { + _cache = Build(); + } + return _cache.ToArray(); // Make a copy + } + + private byte[] Build() + { + var buffer = new byte[sizeof(int) + Key.Length]; + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(), Id); + Key.CopyTo(buffer.AsMemory(sizeof(int)..)); + return buffer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator StorageKey(byte[] value) => new(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator StorageKey(ReadOnlyMemory value) => new(value, true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator StorageKey(ReadOnlySpan value) => new(value); + + public override string ToString() + { + var keyArray = Key.ToArray(); + return $"Id = {Id}, Prefix = 0x{keyArray[0]:x02}, Key = {{ {string.Join(", ", keyArray[1..].Select(static s => $"0x{s:x02}"))} }}"; + } +} diff --git a/src/Neo/SmartContract/TriggerType.cs b/src/Neo/SmartContract/TriggerType.cs new file mode 100644 index 0000000000..7005e66628 --- /dev/null +++ b/src/Neo/SmartContract/TriggerType.cs @@ -0,0 +1,51 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TriggerType.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P.Payloads; + +namespace Neo.SmartContract; + +/// +/// Represents the triggers for running smart contracts. +/// +[Flags] +public enum TriggerType : byte +{ + /// + /// Indicate that the contract is triggered by the system to execute the OnPersist method of the native contracts. + /// + OnPersist = 0x01, + + /// + /// Indicate that the contract is triggered by the system to execute the PostPersist method of the native contracts. + /// + PostPersist = 0x02, + + /// + /// Indicates that the contract is triggered by the verification of a . + /// + Verification = 0x20, + + /// + /// Indicates that the contract is triggered by the execution of transactions. + /// + Application = 0x40, + + /// + /// The combination of all system triggers. + /// + System = OnPersist | PostPersist, + + /// + /// The combination of all triggers. + /// + All = OnPersist | PostPersist | Verification | Application +} diff --git a/src/Neo/SmartContract/ValidatorAttribute.cs b/src/Neo/SmartContract/ValidatorAttribute.cs new file mode 100644 index 0000000000..0b6c21aa87 --- /dev/null +++ b/src/Neo/SmartContract/ValidatorAttribute.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ValidatorAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM.Types; + +namespace Neo.SmartContract; + +[AttributeUsage(AttributeTargets.Parameter)] +abstract class ValidatorAttribute : Attribute +{ + public abstract void Validate(StackItem item); +} diff --git a/src/Neo/TimeProvider.cs b/src/Neo/TimeProvider.cs new file mode 100644 index 0000000000..d82b2a7c61 --- /dev/null +++ b/src/Neo/TimeProvider.cs @@ -0,0 +1,35 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TimeProvider.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +/// +/// The time provider for the NEO system. +/// +public class TimeProvider +{ + private static readonly TimeProvider Default = new(); + + /// + /// The currently used instance. + /// + public static TimeProvider Current { get; internal set; } = Default; + + /// + /// Gets the current time expressed as the Coordinated Universal Time (UTC). + /// + public virtual DateTime UtcNow => DateTime.UtcNow; + + internal static void ResetToDefault() + { + Current = Default; + } +} diff --git a/src/Neo/UInt160.cs b/src/Neo/UInt160.cs new file mode 100644 index 0000000000..b32810199e --- /dev/null +++ b/src/Neo/UInt160.cs @@ -0,0 +1,249 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UInt160.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Neo; + +/// +/// Represents a 160-bit unsigned integer. +/// +[StructLayout(LayoutKind.Explicit, Size = 20)] +public class UInt160 : IComparable, IComparable, IEquatable, ISerializable, ISerializableSpan +{ + /// + /// The length of values. + /// + public const int Length = 20; + + /// + /// Represents 0. + /// + public readonly static UInt160 Zero = new(); + + [FieldOffset(0)] private ulong _value1; + [FieldOffset(8)] private ulong _value2; + [FieldOffset(16)] private uint _value3; + + public int Size => Length; + + /// + /// Initializes a new instance of the class. + /// + public UInt160() { } + + /// + /// Initializes a new instance of the class. + /// + /// The value of the . + public UInt160(ReadOnlySpan value) + { + if (value.Length != Length) + throw new FormatException($"Invalid UInt160 length: expected {Length} bytes, but got {value.Length} bytes. UInt160 values must be exactly 20 bytes long."); + + var span = MemoryMarshal.CreateSpan(ref Unsafe.As(ref _value1), Length); + value.CopyTo(span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int CompareTo(object? obj) + { + if (ReferenceEquals(obj, this)) return 0; + return CompareTo(obj as UInt160); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int CompareTo(UInt160? other) + { + if (other is null) return 1; + var result = _value3.CompareTo(other._value3); + if (result != 0) return result; + result = _value2.CompareTo(other._value2); + if (result != 0) return result; + return _value1.CompareTo(other._value1); + } + + public void Deserialize(ref MemoryReader reader) + { + _value1 = reader.ReadUInt64(); + _value2 = reader.ReadUInt64(); + _value3 = reader.ReadUInt32(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (ReferenceEquals(obj, this)) return true; + return Equals(obj as UInt160); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(UInt160? other) + { + if (other == null) return false; + return _value1 == other._value1 && + _value2 == other._value2 && + _value3 == other._value3; + } + + public override int GetHashCode() + { + return HashCode.Combine(_value1, _value2, _value3); + } + + /// + /// Gets a ReadOnlySpan that represents the current value in little-endian. + /// + /// A ReadOnlySpan that represents the current value in little-endian. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan GetSpan() + { + if (BitConverter.IsLittleEndian) + return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref _value1), Length); + + return GetSpanLittleEndian(); + } + + internal Span GetSpanLittleEndian() + { + Span buffer = new byte[Length]; + SafeSerialize(buffer); + return buffer; // Keep the same output as Serialize when BigEndian + } + + /// + public void Serialize(Span destination) + { + if (BitConverter.IsLittleEndian) + { + var buffer = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref _value1), Length); + buffer.CopyTo(destination); + } + else + { + SafeSerialize(destination); + } + } + + // internal for testing, don't use it directly + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SafeSerialize(Span destination) + { + // Avoid partial write and keep the same Exception as before if the buffer is too small + if (destination.Length < Length) + throw new ArgumentException($"Destination buffer size ({destination.Length} bytes) is too small to serialize UInt160. Required size is {Length} bytes.", nameof(destination)); + + const int IxValue2 = sizeof(ulong); + const int IxValue3 = sizeof(ulong) * 2; + BinaryPrimitives.WriteUInt64LittleEndian(destination, _value1); + BinaryPrimitives.WriteUInt64LittleEndian(destination[IxValue2..], _value2); + BinaryPrimitives.WriteUInt32LittleEndian(destination[IxValue3..], _value3); + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(_value1); + writer.Write(_value2); + writer.Write(_value3); + } + + public override string ToString() + { + return "0x" + GetSpan().ToHexString(reverse: true); + } + + /// + /// Parses an from the specified . + /// + /// An represented by a . + /// The parsed . + /// + /// if an is successfully parsed; otherwise, . + /// + public static bool TryParse(string value, [NotNullWhen(true)] out UInt160? result) + { + result = null; + var data = value.AsSpan().TrimStartIgnoreCase("0x"); + if (data.Length != Length * 2) return false; + try + { + result = new UInt160(data.HexToBytesReversed()); + return true; + } + catch + { + return false; + } + } + + + /// + /// Parses an from the specified . + /// + /// An represented by a . + /// The parsed . + /// is not in the correct format. + public static UInt160 Parse(string value) + { + var data = value.AsSpan().TrimStartIgnoreCase("0x"); + if (data.Length != Length * 2) + throw new FormatException($"Invalid UInt160 string format: expected {Length * 2} hexadecimal characters, but got {data.Length}. UInt160 values must be represented as 40 hexadecimal characters (with or without '0x' prefix)."); + return new UInt160(data.HexToBytesReversed()); + } + + public static implicit operator UInt160(string s) + { + return Parse(s); + } + + public static implicit operator UInt160(byte[] b) + { + return new UInt160(b); + } + + public static bool operator ==(UInt160? left, UInt160? right) + { + if (left is null || right is null) + return Equals(left, right); + return left.Equals(right); + } + + public static bool operator !=(UInt160? left, UInt160? right) + { + if (left is null || right is null) + return !Equals(left, right); + return !left.Equals(right); + } + + public static bool operator >(UInt160 left, UInt160 right) + { + return left.CompareTo(right) > 0; + } + + public static bool operator >=(UInt160 left, UInt160 right) + { + return left.CompareTo(right) >= 0; + } + + public static bool operator <(UInt160 left, UInt160 right) + { + return left.CompareTo(right) < 0; + } + + public static bool operator <=(UInt160 left, UInt160 right) + { + return left.CompareTo(right) <= 0; + } +} diff --git a/src/Neo/UInt256.cs b/src/Neo/UInt256.cs new file mode 100644 index 0000000000..a96c2c8088 --- /dev/null +++ b/src/Neo/UInt256.cs @@ -0,0 +1,255 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UInt256.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Neo; + +/// +/// Represents a 256-bit unsigned integer. +/// +[StructLayout(LayoutKind.Explicit, Size = 32)] +public class UInt256 : IComparable, IComparable, IEquatable, ISerializable, ISerializableSpan +{ + /// + /// The length of values. + /// + public const int Length = 32; + + /// + /// Represents 0. + /// + public static readonly UInt256 Zero = new(); + + [FieldOffset(0)] private ulong _value1; + [FieldOffset(8)] private ulong _value2; + [FieldOffset(16)] private ulong _value3; + [FieldOffset(24)] private ulong _value4; + + public int Size => Length; + + /// + /// Initializes a new instance of the class. + /// + public UInt256() { } + + /// + /// Initializes a new instance of the class. + /// + /// The value of the . + public UInt256(ReadOnlySpan value) + { + if (value.Length != Length) + throw new FormatException($"Invalid UInt256 length: expected {Length} bytes, but got {value.Length} bytes. UInt256 values must be exactly 32 bytes long."); + + var span = MemoryMarshal.CreateSpan(ref Unsafe.As(ref _value1), Length); + value.CopyTo(span); + } + + public int CompareTo(object? obj) + { + if (ReferenceEquals(obj, this)) return 0; + return CompareTo(obj as UInt256); + } + + public int CompareTo(UInt256? other) + { + if (other is null) return 1; + var result = _value4.CompareTo(other._value4); + if (result != 0) return result; + result = _value3.CompareTo(other._value3); + if (result != 0) return result; + result = _value2.CompareTo(other._value2); + if (result != 0) return result; + return _value1.CompareTo(other._value1); + } + + public void Deserialize(ref MemoryReader reader) + { + _value1 = reader.ReadUInt64(); + _value2 = reader.ReadUInt64(); + _value3 = reader.ReadUInt64(); + _value4 = reader.ReadUInt64(); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(obj, this)) return true; + return Equals(obj as UInt256); + } + + public bool Equals(UInt256? other) + { + if (other is null) return false; + return _value1 == other._value1 + && _value2 == other._value2 + && _value3 == other._value3 + && _value4 == other._value4; + } + + public override int GetHashCode() + { + return (int)_value1; + } + + /// + /// Gets a ReadOnlySpan that represents the current value in little-endian. + /// + /// A ReadOnlySpan that represents the current value in little-endian. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan GetSpan() + { + if (BitConverter.IsLittleEndian) + return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref _value1), Length); + + return GetSpanLittleEndian(); + } + + /// + /// Get the output as Serialize when BigEndian + /// + /// A Span that represents the ourput as Serialize when BigEndian. + internal Span GetSpanLittleEndian() + { + Span buffer = new byte[Length]; + SafeSerialize(buffer); + return buffer; // Keep the same output as Serialize when BigEndian + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(_value1); + writer.Write(_value2); + writer.Write(_value3); + writer.Write(_value4); + } + + /// + public void Serialize(Span destination) + { + if (BitConverter.IsLittleEndian) + { + var buffer = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref _value1), Length); + buffer.CopyTo(destination); + } + else + { + SafeSerialize(destination); + } + } + + // internal for testing, don't use it directly + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SafeSerialize(Span destination) + { + // Avoid partial write and keep the same Exception as before if the buffer is too small + if (destination.Length < Length) + throw new ArgumentException($"Destination buffer size ({destination.Length} bytes) is too small to serialize UInt256. Required size is {Length} bytes.", nameof(destination)); + + const int IxValue2 = sizeof(ulong); + const int IxValue3 = sizeof(ulong) * 2; + const int IxValue4 = sizeof(ulong) * 3; + BinaryPrimitives.WriteUInt64LittleEndian(destination, _value1); + BinaryPrimitives.WriteUInt64LittleEndian(destination[IxValue2..], _value2); + BinaryPrimitives.WriteUInt64LittleEndian(destination[IxValue3..], _value3); + BinaryPrimitives.WriteUInt64LittleEndian(destination[IxValue4..], _value4); + } + + public override string ToString() + { + return "0x" + GetSpan().ToHexString(reverse: true); + } + + /// + /// Parses an from the specified . + /// + /// An represented by a . + /// The parsed . + /// + /// if an is successfully parsed; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string value, [NotNullWhen(true)] out UInt256? result) + { + result = null; + var data = value.AsSpan().TrimStartIgnoreCase("0x"); + if (data.Length != Length * 2) return false; + try + { + result = new UInt256(data.HexToBytesReversed()); + return true; + } + catch + { + return false; + } + } + + /// + /// Parses an from the specified . + /// + /// An represented by a . + /// The parsed . + /// is not in the correct format. + public static UInt256 Parse(string value) + { + var data = value.AsSpan().TrimStartIgnoreCase("0x"); + if (data.Length != Length * 2) + throw new FormatException($"Invalid UInt256 string format: expected {Length * 2} hexadecimal characters, but got {data.Length}. UInt256 values must be represented as 64 hexadecimal characters (with or without '0x' prefix)."); + return new UInt256(data.HexToBytesReversed()); + } + + public static implicit operator UInt256(string s) + { + return Parse(s); + } + + public static implicit operator UInt256(byte[] b) + { + return new UInt256(b); + } + + public static bool operator ==(UInt256? left, UInt256? right) + { + if (ReferenceEquals(left, right)) return true; + if (left is null || right is null) return false; + return left.Equals(right); + } + + public static bool operator !=(UInt256? left, UInt256? right) + { + return !(left == right); + } + + public static bool operator >(UInt256 left, UInt256 right) + { + return left.CompareTo(right) > 0; + } + + public static bool operator >=(UInt256 left, UInt256 right) + { + return left.CompareTo(right) >= 0; + } + + public static bool operator <(UInt256 left, UInt256 right) + { + return left.CompareTo(right) < 0; + } + + public static bool operator <=(UInt256 left, UInt256 right) + { + return left.CompareTo(right) <= 0; + } +} diff --git a/src/Neo/Wallets/AssetDescriptor.cs b/src/Neo/Wallets/AssetDescriptor.cs new file mode 100644 index 0000000000..05ab7feb07 --- /dev/null +++ b/src/Neo/Wallets/AssetDescriptor.cs @@ -0,0 +1,76 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// AssetDescriptor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.VM; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; + +namespace Neo.Wallets; + +/// +/// Represents the descriptor of an asset. +/// +public class AssetDescriptor +{ + /// + /// The id of the asset. + /// + public UInt160 AssetId { get; } + + /// + /// The name of the asset. + /// + public string AssetName { get; } + + /// + /// The symbol of the asset. + /// + public string Symbol { get; } + + /// + /// The number of decimal places of the token. + /// + public byte Decimals { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The snapshot used to read data. + /// The used by the . + /// The id of the asset. + public AssetDescriptor(DataCache snapshot, ProtocolSettings settings, UInt160 assetId) + { + var contract = NativeContract.ContractManagement.GetContract(snapshot, assetId) + ?? throw new ArgumentException($"No asset contract found for assetId {assetId}. Please ensure the assetId is correct and the asset is deployed on the blockchain.", nameof(assetId)); + + byte[] script; + using (ScriptBuilder sb = new()) + { + sb.EmitDynamicCall(assetId, "decimals", CallFlags.ReadOnly); + sb.EmitDynamicCall(assetId, "symbol", CallFlags.ReadOnly); + script = sb.ToArray(); + } + + using var engine = ApplicationEngine.Run(script, snapshot, settings: settings, gas: 0_30000000L); + if (engine.State != VMState.HALT) throw new ArgumentException($"Failed to execute 'decimals' or 'symbol' method for asset {assetId}. The contract execution did not complete successfully (VM state: {engine.State}).", nameof(assetId)); + AssetId = assetId; + AssetName = contract.Manifest.Name; + Symbol = engine.ResultStack.Pop().GetString()!; + Decimals = (byte)engine.ResultStack.Pop().GetInteger(); + } + + public override string ToString() + { + return AssetName; + } +} diff --git a/src/Neo/Wallets/Helper.cs b/src/Neo/Wallets/Helper.cs new file mode 100644 index 0000000000..231ea04499 --- /dev/null +++ b/src/Neo/Wallets/Helper.cs @@ -0,0 +1,230 @@ +// Copyright (C) 2015-2025 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; +using Neo.Cryptography; +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using static Neo.SmartContract.Helper; +using Array = System.Array; + +namespace Neo.Wallets; + +/// +/// A helper class related to wallets. +/// +public static class Helper +{ + /// + /// Signs an with the specified private key. + /// + /// The to sign. + /// The private key to be used. + /// The magic number of the NEO network. + /// The signature for the . + public static byte[] Sign(this IVerifiable verifiable, KeyPair key, uint network) + { + return Crypto.Sign(verifiable.GetSignData(network), key.PrivateKey); + } + + /// + /// Converts the specified script hash to an address. + /// + /// The script hash to convert. + /// The address version. + /// The converted address. + public static string ToAddress(this UInt160 scriptHash, byte version) + { + Span data = stackalloc byte[21]; + data[0] = version; + scriptHash.Serialize(data[1..]); + return Base58.Base58CheckEncode(data); + } + + /// + /// Converts the specified address to a script hash. + /// + /// The address to convert. + /// The address version. + /// The converted script hash. + public static UInt160 ToScriptHash(this string address, byte version) + { + var data = address.Base58CheckDecode(); + if (data.Length != 21) + throw new FormatException($"Invalid address format: expected 21 bytes after Base58Check decoding, but got {data.Length} bytes. The address may be corrupted or in an invalid format."); + if (data[0] != version) + throw new FormatException($"Invalid address version: expected version {version}, but got {data[0]}. The address may be for a different network."); + return new UInt160(data.AsSpan(1)); + } + + internal static byte[] XOR(byte[] x, byte[] y) + { + if (x.Length != y.Length) + throw new ArgumentException($"The x.Length({x.Length}) and y.Length({y.Length}) must be equal."); + var r = new byte[x.Length]; + for (var i = 0; i < r.Length; i++) + r[i] = (byte)(x[i] ^ y[i]); + return r; + } + + /// + /// Calculates the network fee for the specified transaction. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS + /// + /// The transaction to calculate. + /// The snapshot used to read data. + /// Thr protocol settings to use. + /// User wallet. + /// The maximum cost that can be spent when a contract is executed. + /// The network fee of the transaction. + public static long CalculateNetworkFee(this Transaction tx, DataCache snapshot, ProtocolSettings settings, Wallet? wallet = null, long maxExecutionCost = ApplicationEngine.TestModeGas) + { + Func? accountScript = wallet != null ? (scriptHash) => wallet.GetAccount(scriptHash)?.Contract?.Script : null; + return CalculateNetworkFee(tx, snapshot, settings, accountScript, maxExecutionCost); + } + + /// + /// Calculates the network fee for the specified transaction. + /// In the unit of datoshi, 1 datoshi = 1e-8 GAS + /// + /// The transaction to calculate. + /// The snapshot used to read data. + /// Thr protocol settings to use. + /// Function to retrive the script's account from a hash. + /// The maximum cost that can be spent when a contract is executed. + /// The network fee of the transaction. + public static long CalculateNetworkFee(this Transaction tx, DataCache snapshot, ProtocolSettings settings, + Func? accountScript, long maxExecutionCost = ApplicationEngine.TestModeGas) + { + var hashes = tx.GetScriptHashesForVerifying(snapshot); + + // base size for transaction: includes const_header + signers + attributes + script + hashes + int size = Transaction.HeaderSize + tx.Signers.GetVarSize() + tx.Attributes.GetVarSize() + + tx.Script.GetVarSize() + hashes.Length.GetVarSize(); + int index = -1; + var execFeeFactor = NativeContract.Policy.GetExecFeeFactor(snapshot); + long networkFee = 0; + foreach (var hash in hashes) + { + index++; + var witnessScript = accountScript != null ? accountScript(hash) : null; + byte[]? invocationScript = null; + + if (tx.Witnesses != null && witnessScript is null) + { + // Try to find the script in the witnesses + var witness = tx.Witnesses[index]; + witnessScript = witness?.VerificationScript.ToArray(); + + if (witnessScript is null || witnessScript.Length == 0) + { + // Then it's a contract-based witness, so try to get the corresponding invocation script for it + invocationScript = witness?.InvocationScript.ToArray(); + } + } + + if (witnessScript is null || witnessScript.Length == 0) + { + // Contract-based verification + var contract = NativeContract.ContractManagement.GetContract(snapshot, hash) + ?? throw new ArgumentException($"The smart contract or address {hash} ({hash.ToAddress(settings.AddressVersion)}) is not found. " + + $"If this is your wallet address and you want to sign a transaction with it, make sure you have opened this wallet."); + var md = contract.Manifest.Abi.GetMethod(ContractBasicMethod.Verify, ContractBasicMethod.VerifyPCount) + ?? throw new ArgumentException($"The smart contract {contract.Hash} haven't got verify method"); + if (md.ReturnType != ContractParameterType.Boolean) + throw new ArgumentException("The verify method doesn't return boolean value."); + if (md.Parameters.Length > 0 && invocationScript is null) + { + var script = new ScriptBuilder(); + foreach (var par in md.Parameters) + { + switch (par.Type) + { + case ContractParameterType.Any: + case ContractParameterType.Signature: + case ContractParameterType.String: + case ContractParameterType.ByteArray: + script.EmitPush(new byte[64]); + break; + case ContractParameterType.Boolean: + script.EmitPush(true); + break; + case ContractParameterType.Integer: + script.Emit(OpCode.PUSHINT256, new byte[Integer.MaxSize]); + break; + case ContractParameterType.Hash160: + script.EmitPush(new byte[UInt160.Length]); + break; + case ContractParameterType.Hash256: + script.EmitPush(new byte[UInt256.Length]); + break; + case ContractParameterType.PublicKey: + script.EmitPush(new byte[33]); + break; + case ContractParameterType.Array: + script.Emit(OpCode.NEWARRAY0); + break; + } + } + invocationScript = script.ToArray(); + } + + // Empty verification and non-empty invocation scripts + var invSize = invocationScript?.GetVarSize() ?? Array.Empty().GetVarSize(); + size += Array.Empty().GetVarSize() + invSize; + + // Check verify cost + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, + snapshot.CloneCache(), settings: settings, gas: maxExecutionCost); + + engine.LoadContract(contract, md, CallFlags.ReadOnly); + if (invocationScript != null) engine.LoadScript(invocationScript, configureState: p => p.CallFlags = CallFlags.None); + if (engine.Execute() == VMState.HALT) + { + // https://github.com/neo-project/neo/issues/2805 + if (engine.ResultStack.Count != 1) throw new ArgumentException($"Smart contract {contract.Hash} verification fault."); + _ = engine.ResultStack.Pop().GetBoolean(); // Ensure that the result is boolean + } + maxExecutionCost -= engine.FeeConsumed; + if (maxExecutionCost <= 0) throw new InvalidOperationException("Insufficient GAS."); + networkFee += engine.FeeConsumed; + } + else + { + // Regular signature verification. + if (IsSignatureContract(witnessScript)) + { + size += 67 + witnessScript.GetVarSize(); + networkFee += execFeeFactor * SignatureContractCost(); + } + else if (IsMultiSigContract(witnessScript, out int m, out int n)) + { + var sizeInv = 66 * m; + size += sizeInv.GetVarSize() + sizeInv + witnessScript.GetVarSize(); + networkFee += execFeeFactor * MultiSignatureContractCost(m, n); + } + } + } + networkFee += size * NativeContract.Policy.GetFeePerByte(snapshot); + foreach (var attr in tx.Attributes) + { + networkFee += attr.CalculateNetworkFee(snapshot, tx); + } + return networkFee; + } +} diff --git a/src/Neo/Wallets/IWalletFactory.cs b/src/Neo/Wallets/IWalletFactory.cs new file mode 100644 index 0000000000..9882485527 --- /dev/null +++ b/src/Neo/Wallets/IWalletFactory.cs @@ -0,0 +1,41 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IWalletFactory.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +public interface IWalletFactory +{ + /// + /// Determines whether the factory can handle the specified path. + /// + /// The path of the wallet file. + /// + /// if the factory can handle the specified path; otherwise, . + /// + public bool Handle(string path); + + /// + /// Creates a new wallet. + /// + /// The name of the wallet. + /// The path of the wallet file. + /// The password of the wallet. + /// The settings of the wallet. + public Wallet CreateWallet(string? name, string path, string password, ProtocolSettings settings); + + /// + /// Opens a wallet. + /// + /// The path of the wallet file. + /// The password of the wallet. + /// The settings of the wallet. + public Wallet OpenWallet(string path, string password, ProtocolSettings settings); +} diff --git a/src/Neo/Wallets/IWalletProvider.cs b/src/Neo/Wallets/IWalletProvider.cs new file mode 100644 index 0000000000..6bfee9fb2b --- /dev/null +++ b/src/Neo/Wallets/IWalletProvider.cs @@ -0,0 +1,29 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IWalletProvider.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +/// +/// A provider for obtaining wallet instance. +/// +public interface IWalletProvider +{ + /// + /// Triggered when a wallet is opened or closed. + /// + event EventHandler WalletChanged; + + /// + /// Get the currently opened instance. + /// + /// The opened wallet. Or if no wallet is opened. + Wallet? GetWallet(); +} diff --git a/src/Neo/Wallets/KeyPair.cs b/src/Neo/Wallets/KeyPair.cs new file mode 100644 index 0000000000..c5882edc4f --- /dev/null +++ b/src/Neo/Wallets/KeyPair.cs @@ -0,0 +1,158 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// KeyPair.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract; +using Neo.Wallets.NEP6; +using Org.BouncyCastle.Crypto.Generators; +using System.Security.Cryptography; +using System.Text; +using static Neo.Wallets.Helper; +using ECCurve = Neo.Cryptography.ECC.ECCurve; +using ECPoint = Neo.Cryptography.ECC.ECPoint; + +namespace Neo.Wallets; + +/// +/// Represents a private/public key pair in wallets. +/// +public class KeyPair : IEquatable +{ + /// + /// The private key. + /// + public readonly byte[] PrivateKey; + + /// + /// The public key. + /// + public readonly ECPoint PublicKey; + + /// + /// The hash of the public key. + /// + public UInt160 PublicKeyHash => PublicKey.EncodePoint(true).ToScriptHash(); + + /// + /// Initializes a new instance of the class. + /// + /// The private key in the . + public KeyPair(byte[] privateKey) + { + if (privateKey.Length != 32 && privateKey.Length != 96 && privateKey.Length != 104) + throw new ArgumentException($"Invalid private key length: {privateKey.Length}", nameof(privateKey)); + PrivateKey = privateKey[^32..]; + if (privateKey.Length == 32) + { + PublicKey = ECCurve.Secp256r1.G * privateKey; + } + else + { + PublicKey = ECPoint.FromBytes(privateKey, ECCurve.Secp256r1); + } + } + + public bool Equals(KeyPair? other) + { + if (ReferenceEquals(this, other)) return true; + if (other is null) return false; + return PublicKey.Equals(other.PublicKey); + } + + public override bool Equals(object? obj) + { + return Equals(obj as KeyPair); + } + + /// + /// Exports the private key in WIF format. + /// + /// The private key in WIF format. + public string Export() + { + Span data = stackalloc byte[34]; + data[0] = 0x80; + PrivateKey.CopyTo(data[1..]); + data[33] = 0x01; + string wif = Base58.Base58CheckEncode(data); + data.Clear(); + return wif; + } + + /// + /// Exports the private key in NEP-2 format. + /// + /// The passphrase of the private key. + /// The address version. + /// The N field of the to be used. + /// The R field of the to be used. + /// The P field of the to be used. + /// The private key in NEP-2 format. + public string Export(string passphrase, byte version, int N = 16384, int r = 8, int p = 8) + { + byte[] passphrasedata = Encoding.UTF8.GetBytes(passphrase); + try + { + return Export(passphrasedata, version, N, r, p); + } + finally + { + passphrasedata.AsSpan().Clear(); + } + } + + /// + /// Exports the private key in NEP-2 format. + /// + /// The passphrase of the private key. + /// The address version. + /// The N field of the to be used. + /// The R field of the to be used. + /// The P field of the to be used. + /// The private key in NEP-2 format. + public string Export(byte[] passphrase, byte version, int N = 16384, int r = 8, int p = 8) + { + UInt160 script_hash = Contract.CreateSignatureRedeemScript(PublicKey).ToScriptHash(); + string address = script_hash.ToAddress(version); + byte[] addresshash = Encoding.ASCII.GetBytes(address).Sha256().Sha256()[..4]; + byte[] derivedkey = SCrypt.Generate(passphrase, addresshash, N, r, p, 64); + byte[] derivedhalf1 = derivedkey[..32]; + byte[] derivedhalf2 = derivedkey[32..]; + byte[] encryptedkey = Encrypt(XOR(PrivateKey, derivedhalf1), derivedhalf2); + Span buffer = stackalloc byte[39]; + buffer[0] = 0x01; + buffer[1] = 0x42; + buffer[2] = 0xe0; + addresshash.CopyTo(buffer[3..]); + encryptedkey.CopyTo(buffer[7..]); + return Base58.Base58CheckEncode(buffer); + } + + private static byte[] Encrypt(byte[] data, byte[] key) + { + using Aes aes = Aes.Create(); + aes.Key = key; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + using ICryptoTransform encryptor = aes.CreateEncryptor(); + return encryptor.TransformFinalBlock(data, 0, data.Length); + } + + public override int GetHashCode() + { + return PublicKey.GetHashCode(); + } + + public override string ToString() + { + return PublicKey.ToString(); + } +} diff --git a/src/Neo/Wallets/NEP6/NEP6Account.cs b/src/Neo/Wallets/NEP6/NEP6Account.cs new file mode 100644 index 0000000000..a8cc944ba3 --- /dev/null +++ b/src/Neo/Wallets/NEP6/NEP6Account.cs @@ -0,0 +1,133 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NEP6Account.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Wallets.NEP6; + +sealed class NEP6Account : WalletAccount +{ + private readonly NEP6Wallet wallet; + private string? nep2key; + private string? nep2KeyNew = null; + private KeyPair? key; + public JToken? Extra; + + public bool Decrypted => nep2key == null || key != null; + public override bool HasKey => nep2key != null; + + public NEP6Account(NEP6Wallet wallet, UInt160 scriptHash, string? nep2key = null) + : base(scriptHash, wallet.ProtocolSettings) + { + this.wallet = wallet; + this.nep2key = nep2key; + } + + public NEP6Account(NEP6Wallet wallet, UInt160 scriptHash, KeyPair key, string password) + : this(wallet, scriptHash, key.Export(password, wallet.ProtocolSettings.AddressVersion, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P)) + { + this.key = key; + } + + public static NEP6Account FromJson(JObject json, NEP6Wallet wallet) + { + return new NEP6Account(wallet, json["address"]!.GetString().ToScriptHash(wallet.ProtocolSettings.AddressVersion), json["key"]?.GetString()) + { + Label = json["label"]?.GetString(), + IsDefault = json["isDefault"]!.GetBoolean(), + Lock = json["lock"]!.GetBoolean(), + Contract = NEP6Contract.FromJson((JObject)json["contract"]!), + Extra = json["extra"] + }; + } + + public override KeyPair? GetKey() + { + if (nep2key == null) return null; + key ??= wallet.DecryptKey(nep2key); + return key; + } + + public KeyPair? GetKey(string password) + { + if (nep2key == null) return null; + key ??= new KeyPair(Wallet.GetPrivateKeyFromNEP2(nep2key, password, ProtocolSettings.AddressVersion, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P)); + return key; + } + + public JObject ToJson() + { + JObject account = new(); + account["address"] = ScriptHash.ToAddress(ProtocolSettings.AddressVersion); + account["label"] = Label; + account["isDefault"] = IsDefault; + account["lock"] = Lock; + account["key"] = nep2key; + account["contract"] = ((NEP6Contract?)Contract)?.ToJson(); + account["extra"] = Extra; + return account; + } + + public bool VerifyPassword(string password) + { + try + { + Wallet.GetPrivateKeyFromNEP2(nep2key!, password, ProtocolSettings.AddressVersion, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P); + return true; + } + catch (FormatException) + { + return false; + } + } + + /// + /// Cache draft nep2key during wallet password changing process. Should not be called alone for a single account + /// + internal bool ChangePasswordPrepare(string password_old, string password_new) + { + if (WatchOnly) return true; + KeyPair? keyTemplate = key; + if (nep2key == null) + { + if (keyTemplate == null) + { + return true; + } + } + else + { + try + { + keyTemplate = new KeyPair(Wallet.GetPrivateKeyFromNEP2(nep2key, password_old, ProtocolSettings.AddressVersion, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P)); + } + catch + { + return false; + } + } + nep2KeyNew = keyTemplate.Export(password_new, ProtocolSettings.AddressVersion, wallet.Scrypt.N, wallet.Scrypt.R, wallet.Scrypt.P); + return true; + } + + internal void ChangePasswordCommit() + { + if (nep2KeyNew != null) + { + nep2key = Interlocked.Exchange(ref nep2KeyNew, null); + } + } + + internal void ChangePasswordRollback() + { + nep2KeyNew = null; + } +} diff --git a/src/Neo/Wallets/NEP6/NEP6Contract.cs b/src/Neo/Wallets/NEP6/NEP6Contract.cs new file mode 100644 index 0000000000..0a108c0037 --- /dev/null +++ b/src/Neo/Wallets/NEP6/NEP6Contract.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NEP6Contract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.Wallets.NEP6; + +internal class NEP6Contract : Contract +{ + public required string[] ParameterNames; + public bool Deployed; + + public static NEP6Contract? FromJson(JObject? json) + { + if (json == null) return null; + return new NEP6Contract + { + Script = Convert.FromBase64String(json["script"]!.AsString()), + ParameterList = ((JArray)json["parameters"]!).Select(p => p!["type"]!.GetEnum()).ToArray(), + ParameterNames = ((JArray)json["parameters"]!).Select(p => p!["name"]!.AsString()).ToArray(), + Deployed = json["deployed"]!.AsBoolean() + }; + } + + public JObject ToJson() + { + JObject contract = new(); + contract["script"] = Convert.ToBase64String(Script); + contract["parameters"] = new JArray(ParameterList.Zip(ParameterNames, (type, name) => + { + JObject parameter = new(); + parameter["name"] = name; + parameter["type"] = type; + return parameter; + })); + contract["deployed"] = Deployed; + return contract; + } +} diff --git a/src/Neo/Wallets/NEP6/NEP6Wallet.cs b/src/Neo/Wallets/NEP6/NEP6Wallet.cs new file mode 100644 index 0000000000..799439666c --- /dev/null +++ b/src/Neo/Wallets/NEP6/NEP6Wallet.cs @@ -0,0 +1,378 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NEP6Wallet.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Neo.Wallets.NEP6; + +/// +/// An implementation of the NEP-6 wallet standard. +/// +/// https://github.com/neo-project/proposals/blob/master/nep-6.mediawiki +public class NEP6Wallet : Wallet +{ + private SecureString password; + private string? name; + private Version version; + private readonly Dictionary accounts; + private readonly JToken? extra; + + /// + /// The parameters of the SCrypt algorithm used for encrypting and decrypting the private keys in the wallet. + /// + public readonly ScryptParameters Scrypt; + + /// + /// The name of the wallet. + /// If the name is not set, it will be the file name without extension of the wallet file. + /// + public override string Name => + !string.IsNullOrEmpty(name) ? name : System.IO.Path.GetFileNameWithoutExtension(Path); + + /// + /// The version of the wallet standard. It is currently fixed at 1.0 and will be used for functional upgrades in the future. + /// + public override Version Version => version; + + /// + /// Loads or creates a wallet at the specified path. + /// + /// The path of the wallet file. + /// The password of the wallet. + /// The to be used by the wallet. + /// The name of the wallet. If the wallet is loaded from an existing file, this parameter is ignored. + public NEP6Wallet(string path, string password, ProtocolSettings settings, string? name = null) : base(path, settings) + { + this.password = password.ToSecureString(); + if (File.Exists(path)) + { + var wallet = (JObject)JToken.Parse(File.ReadAllBytes(path))!; + LoadFromJson(wallet, out Scrypt, out accounts, out extra); + } + else + { + this.name = name; + version = Version.Parse("1.0"); + Scrypt = ScryptParameters.Default; + accounts = new Dictionary(); + extra = JToken.Null; + } + } + + /// + /// Loads the wallet with the specified JSON string. + /// + /// The path of the wallet. + /// The password of the wallet. + /// The to be used by the wallet. + /// The JSON object representing the wallet. + public NEP6Wallet(string path, string password, ProtocolSettings settings, JObject json) : base(path, settings) + { + this.password = password.ToSecureString(); + LoadFromJson(json, out Scrypt, out accounts, out extra); + } + + [MemberNotNull(nameof(version))] + private void LoadFromJson(JObject wallet, out ScryptParameters scrypt, out Dictionary accounts, out JToken? extra) + { + version = Version.Parse(wallet["version"]!.AsString()); + name = wallet["name"]?.AsString(); + scrypt = ScryptParameters.FromJson((JObject)wallet["scrypt"]!); + accounts = ((JArray)wallet["accounts"]!).Select(p => NEP6Account.FromJson((JObject)p!, this)).ToDictionary(p => p.ScriptHash); + extra = wallet["extra"]; + if (!VerifyPasswordInternal(password.GetClearText())) + throw new InvalidOperationException("Incorrect password provided for NEP6 wallet. Please verify the password and try again."); + } + + private void AddAccount(NEP6Account account) + { + lock (accounts) + { + if (accounts.TryGetValue(account.ScriptHash, out var accountOld)) + { + account.Label = accountOld.Label; + account.IsDefault = accountOld.IsDefault; + account.Lock = accountOld.Lock; + if (account.Contract == null) + { + account.Contract = accountOld.Contract; + } + else + { + var contractOld = (NEP6Contract?)accountOld.Contract; + if (contractOld != null) + { + NEP6Contract contract = (NEP6Contract)account.Contract; + contract.ParameterNames = contractOld.ParameterNames; + contract.Deployed = contractOld.Deployed; + } + } + account.Extra = accountOld.Extra; + } + accounts[account.ScriptHash] = account; + } + } + + public override bool Contains(UInt160 scriptHash) + { + lock (accounts) + { + return accounts.ContainsKey(scriptHash); + } + } + + public override WalletAccount CreateAccount(byte[] privateKey) + { + ArgumentNullException.ThrowIfNull(privateKey); + KeyPair key = new(privateKey); + if (key.PublicKey.IsInfinity) throw new ArgumentException("Invalid private key provided. The private key does not correspond to a valid public key on the elliptic curve.", nameof(privateKey)); + NEP6Contract contract = new() + { + Script = Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = new[] { ContractParameterType.Signature }, + ParameterNames = new[] { "signature" }, + Deployed = false + }; + NEP6Account account = new(this, contract.ScriptHash, key, password.GetClearText()) + { + Contract = contract + }; + AddAccount(account); + return account; + } + + public override WalletAccount CreateAccount(Contract contract, KeyPair? key = null) + { + if (contract is not NEP6Contract nep6contract) + { + nep6contract = new NEP6Contract + { + Script = contract.Script, + ParameterList = contract.ParameterList, + ParameterNames = contract.ParameterList.Select((p, i) => $"parameter{i}").ToArray(), + Deployed = false + }; + } + NEP6Account account; + if (key == null) + account = new NEP6Account(this, nep6contract.ScriptHash); + else + account = new NEP6Account(this, nep6contract.ScriptHash, key, password.GetClearText()); + account.Contract = nep6contract; + AddAccount(account); + return account; + } + + public override WalletAccount CreateAccount(UInt160 scriptHash) + { + NEP6Account account = new(this, scriptHash); + AddAccount(account); + return account; + } + + /// + /// Decrypts the specified NEP-2 string with the password of the wallet. + /// + /// The NEP-2 string to decrypt. + /// The decrypted private key. + internal KeyPair DecryptKey(string nep2key) + { + return new KeyPair(GetPrivateKeyFromNEP2(nep2key, password.GetClearText(), ProtocolSettings.AddressVersion, Scrypt.N, Scrypt.R, Scrypt.P)); + } + + public override void Delete() + { + if (File.Exists(Path)) File.Delete(Path); + } + + public override bool DeleteAccount(UInt160 scriptHash) + { + lock (accounts) + { + return accounts.Remove(scriptHash); + } + } + + public override WalletAccount? GetAccount(UInt160 scriptHash) + { + lock (accounts) + { + accounts.TryGetValue(scriptHash, out NEP6Account? account); + return account; + } + } + + public override IEnumerable GetAccounts() + { + lock (accounts) + { + foreach (NEP6Account account in accounts.Values) + yield return account; + } + } + + public override WalletAccount Import(X509Certificate2 cert) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + throw new PlatformNotSupportedException("Importing certificates is not supported on macOS."); + } + KeyPair key; + using (ECDsa ecdsa = cert.GetECDsaPrivateKey() ?? throw new ArgumentException("The certificate must contains a private key.", nameof(cert))) + { + key = new KeyPair(ecdsa.ExportParameters(true).D!); + } + NEP6Contract contract = new() + { + Script = Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = new[] { ContractParameterType.Signature }, + ParameterNames = new[] { "signature" }, + Deployed = false + }; + NEP6Account account = new(this, contract.ScriptHash, key, password.GetClearText()) + { + Contract = contract + }; + AddAccount(account); + return account; + } + + public override WalletAccount Import(string wif) + { + KeyPair key = new(GetPrivateKeyFromWIF(wif)); + NEP6Contract contract = new() + { + Script = Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = new[] { ContractParameterType.Signature }, + ParameterNames = new[] { "signature" }, + Deployed = false + }; + NEP6Account account = new(this, contract.ScriptHash, key, password.GetClearText()) + { + Contract = contract + }; + AddAccount(account); + return account; + } + + public override WalletAccount Import(string nep2, string passphrase, int N = 16384, int r = 8, int p = 8) + { + KeyPair key = new(GetPrivateKeyFromNEP2(nep2, passphrase, ProtocolSettings.AddressVersion, N, r, p)); + NEP6Contract contract = new() + { + Script = Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = new[] { ContractParameterType.Signature }, + ParameterNames = new[] { "signature" }, + Deployed = false + }; + NEP6Account account; + if (Scrypt.N == 16384 && Scrypt.R == 8 && Scrypt.P == 8) + account = new NEP6Account(this, contract.ScriptHash, nep2); + else + account = new NEP6Account(this, contract.ScriptHash, key, passphrase); + account.Contract = contract; + AddAccount(account); + return account; + } + + /// + /// Exports the wallet as JSON + /// + public JObject ToJson() + { + NEP6Account[] accountValues; + lock (accounts) + { + accountValues = accounts.Values.ToArray(); + } + + return new() + { + ["name"] = name, + ["version"] = version.ToString(), + ["scrypt"] = Scrypt.ToJson(), + ["accounts"] = accountValues.Select(p => p.ToJson()).ToArray(), + ["extra"] = extra + }; + } + + public override void Save() + { + File.WriteAllText(Path, ToJson().ToString()); + } + + public override bool VerifyPassword(string password) + { + return this.password.GetClearText() == password; + } + + private bool VerifyPasswordInternal(string password) + { + lock (accounts) + { + NEP6Account? account = accounts.Values.FirstOrDefault(p => !p.Decrypted); + account ??= accounts.Values.FirstOrDefault(p => p.HasKey); + if (account == null) return true; + if (account.Decrypted) + { + return account.VerifyPassword(password); + } + else + { + try + { + account.GetKey(password); + return true; + } + catch (FormatException) + { + return false; + } + } + } + } + + public override bool ChangePassword(string oldPassword, string newPassword) + { + bool succeed = true; + NEP6Account[] accountsValues; + lock (accounts) + { + accountsValues = accounts.Values.ToArray(); + } + Parallel.ForEach(accountsValues, (account, state) => + { + if (!account.ChangePasswordPrepare(oldPassword, newPassword)) + { + state.Stop(); + succeed = false; + } + }); + if (succeed) + { + foreach (NEP6Account account in accountsValues) + account.ChangePasswordCommit(); + password = newPassword.ToSecureString(); + } + else + { + foreach (NEP6Account account in accountsValues) + account.ChangePasswordRollback(); + } + return succeed; + } +} diff --git a/src/Neo/Wallets/NEP6/NEP6WalletFactory.cs b/src/Neo/Wallets/NEP6/NEP6WalletFactory.cs new file mode 100644 index 0000000000..536d3ba3b2 --- /dev/null +++ b/src/Neo/Wallets/NEP6/NEP6WalletFactory.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NEP6WalletFactory.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.NEP6; + +class NEP6WalletFactory : IWalletFactory +{ + public static readonly NEP6WalletFactory Instance = new(); + + public bool Handle(string path) + { + return Path.GetExtension(path).Equals(".json", StringComparison.InvariantCultureIgnoreCase); + } + + public Wallet CreateWallet(string? name, string path, string password, ProtocolSettings settings) + { + if (File.Exists(path)) + throw new InvalidOperationException("The wallet file already exists."); + var wallet = new NEP6Wallet(path, password, settings, name); + wallet.Save(); + return wallet; + } + + public Wallet OpenWallet(string path, string password, ProtocolSettings settings) + { + return new NEP6Wallet(path, password, settings); + } +} diff --git a/src/Neo/Wallets/NEP6/ScryptParameters.cs b/src/Neo/Wallets/NEP6/ScryptParameters.cs new file mode 100644 index 0000000000..6fa5f73e19 --- /dev/null +++ b/src/Neo/Wallets/NEP6/ScryptParameters.cs @@ -0,0 +1,76 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ScryptParameters.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Wallets.NEP6; + +/// +/// Represents the parameters of the SCrypt algorithm. +/// +public class ScryptParameters +{ + /// + /// The default parameters used by . + /// + public static ScryptParameters Default { get; } = new ScryptParameters(16384, 8, 8); + + /// + /// CPU/Memory cost parameter. Must be larger than 1, a power of 2 and less than 2^(128 * r / 8). + /// + public readonly int N; + + /// + /// The block size, must be >= 1. + /// + public readonly int R; + + /// + /// Parallelization parameter. Must be a positive integer less than or equal to Int32.MaxValue / (128 * r * 8). + /// + public readonly int P; + + /// + /// Initializes a new instance of the class. + /// + /// CPU/Memory cost parameter. + /// The block size. + /// Parallelization parameter. + public ScryptParameters(int n, int r, int p) + { + N = n; + R = r; + P = p; + } + + /// + /// Converts the parameters from a JSON object. + /// + /// The parameters represented by a JSON object. + /// The converted parameters. + public static ScryptParameters FromJson(JObject json) + { + return new ScryptParameters((int)json["n"]!.AsNumber(), (int)json["r"]!.AsNumber(), (int)json["p"]!.AsNumber()); + } + + /// + /// Converts the parameters to a JSON object. + /// + /// The parameters represented by a JSON object. + public JObject ToJson() + { + JObject json = new(); + json["n"] = N; + json["r"] = R; + json["p"] = P; + return json; + } +} diff --git a/src/Neo/Wallets/TransferOutput.cs b/src/Neo/Wallets/TransferOutput.cs new file mode 100644 index 0000000000..e6d223582a --- /dev/null +++ b/src/Neo/Wallets/TransferOutput.cs @@ -0,0 +1,38 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransferOutput.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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; + +/// +/// Represents an output of a transfer. +/// +public class TransferOutput +{ + /// + /// The id of the asset to transfer. + /// + public required UInt160 AssetId; + + /// + /// The amount of the asset to transfer. + /// + public BigDecimal Value; + + /// + /// The account to transfer to. + /// + public required UInt160 ScriptHash; + + /// + /// The object to be passed to the transfer method of NEP-17. + /// + public object? Data; +} diff --git a/src/Neo/Wallets/Wallet.cs b/src/Neo/Wallets/Wallet.cs new file mode 100644 index 0000000000..4b93c53d1c --- /dev/null +++ b/src/Neo/Wallets/Wallet.cs @@ -0,0 +1,849 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// 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 Neo.Cryptography; +using Neo.Extensions.VM; +using Neo.Factories; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.Sign; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets.NEP6; +using Org.BouncyCastle.Crypto.Generators; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using static Neo.SmartContract.Helper; +using static Neo.Wallets.Helper; +using ECCurve = Neo.Cryptography.ECC.ECCurve; +using ECPoint = Neo.Cryptography.ECC.ECPoint; + +namespace Neo.Wallets; + +/// +/// The base class of wallets. +/// +public abstract class Wallet : ISigner +{ + private static readonly List factories = new() { NEP6WalletFactory.Instance }; + + /// + /// The to be used by the wallet. + /// + public ProtocolSettings ProtocolSettings { get; } + + /// + /// The name of the wallet. + /// + public abstract string Name { get; } + + /// + /// The path of the wallet. + /// + public string Path { get; } + + /// + /// The version of the wallet. + /// + public abstract Version Version { get; } + + /// + /// Changes the password of the wallet. + /// + /// The old password of the wallet. + /// The new password to be used. + /// if the password is changed successfully; otherwise, . + public abstract bool ChangePassword(string oldPassword, string newPassword); + + /// + /// Determines whether the specified account is included in the wallet. + /// + /// The hash of the account. + /// if the account is included in the wallet; otherwise, . + public abstract bool Contains(UInt160 scriptHash); + + /// + /// Creates a standard account with the specified private key. + /// + /// The private key of the account. + /// The created account. + public abstract WalletAccount CreateAccount(byte[] privateKey); + + /// + /// Creates a contract account for the wallet. + /// + /// The contract of the account. + /// The private key of the account. + /// The created account. + public abstract WalletAccount CreateAccount(Contract contract, KeyPair? key = null); + + /// + /// Creates a watch-only account for the wallet. + /// + /// The hash of the account. + /// The created account. + public abstract WalletAccount CreateAccount(UInt160 scriptHash); + + /// + /// Deletes the entire database of the wallet. + /// + public abstract void Delete(); + + /// + /// Deletes an account from the wallet. + /// + /// The hash of the account. + /// if the account is removed; otherwise, . + public abstract bool DeleteAccount(UInt160 scriptHash); + + /// + /// Gets the account with the specified hash. + /// + /// The hash of the account. + /// The account with the specified hash. + public abstract WalletAccount? GetAccount(UInt160 scriptHash); + + /// + /// Gets all the accounts from the wallet. + /// + /// All accounts in the wallet. + public abstract IEnumerable GetAccounts(); + + /// + /// Initializes a new instance of the class. + /// + /// The path of the wallet file. + /// The to be used by the wallet. + protected Wallet(string path, ProtocolSettings settings) + { + ProtocolSettings = settings; + Path = path; + } + + /// + /// Constructs a special contract with empty script, will get the script with + /// scriptHash from blockchain when doing the verification. + /// + /// Note: + /// Creates "m" out of "n" type verification script using length + /// with the default BFT assumptions of Ceiling(n - (n-1) / 3) for "m". + /// + /// + /// The public keys of the contract. + /// Multi-Signature contract . + /// + /// is empty or length is greater than 1024. + /// + /// + public WalletAccount CreateMultiSigAccount(params ECPoint[] publicKeys) => + CreateMultiSigAccount((int)Math.Ceiling((2 * publicKeys.Length + 1) / 3m), publicKeys); + + /// + /// Constructs a special contract with empty script, will get the script with + /// scriptHash from blockchain when doing the verification. + /// + /// The number of correct signatures that need to be provided in order for the verification to pass. + /// The public keys of the contract. + /// Multi-Signature contract . + /// + /// is empty or is greater than length or + /// is less than 1 or is greater than 1024. + /// + /// + public WalletAccount CreateMultiSigAccount(int m, params ECPoint[] publicKeys) + { + ArgumentOutOfRangeException.ThrowIfEqual(publicKeys.Length, 0, nameof(publicKeys)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(m, publicKeys.Length, nameof(publicKeys)); + ArgumentOutOfRangeException.ThrowIfLessThan(m, 1, nameof(m)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(m, 1024, nameof(m)); + + var contract = Contract.CreateMultiSigContract(m, publicKeys); + var account = GetAccounts() + .FirstOrDefault( + f => + f.HasKey && + f.Lock == false && + publicKeys.Contains(f.GetKey()!.PublicKey)); + + return CreateAccount(contract, account?.GetKey()); + } + + /// + /// Creates a standard account for the wallet. + /// + /// The created account. + public WalletAccount CreateAccount() + { + var privateKey = new byte[32]; + using var rng = RandomNumberGenerator.Create(); + + do + { + try + { + rng.GetBytes(privateKey); + return CreateAccount(privateKey); + } + catch (ArgumentException) + { + // Try again + } + finally + { + Array.Clear(privateKey, 0, privateKey.Length); + } + } + while (true); + } + + /// + /// Creates a contract account for the wallet. + /// + /// The contract of the account. + /// The private key of the account. + /// The created account. + public WalletAccount CreateAccount(Contract contract, byte[] privateKey) + { + if (privateKey == null) return CreateAccount(contract); + return CreateAccount(contract, new KeyPair(privateKey)); + } + + private static List<(UInt160 Account, BigInteger Value)> FindPayingAccounts(List<(UInt160 Account, BigInteger Value)> orderedAccounts, BigInteger amount) + { + var result = new List<(UInt160 Account, BigInteger Value)>(); + var sum_balance = orderedAccounts.Select(p => p.Value).Sum(); + if (sum_balance == amount) + { + result.AddRange(orderedAccounts); + orderedAccounts.Clear(); + } + else + { + for (int i = 0; i < orderedAccounts.Count; i++) + { + if (orderedAccounts[i].Value < amount) + continue; + if (orderedAccounts[i].Value == amount) + { + result.Add(orderedAccounts[i]); + orderedAccounts.RemoveAt(i); + } + else + { + result.Add((orderedAccounts[i].Account, amount)); + orderedAccounts[i] = (orderedAccounts[i].Account, orderedAccounts[i].Value - amount); + } + break; + } + if (result.Count == 0) + { + int i = orderedAccounts.Count - 1; + while (orderedAccounts[i].Value <= amount) + { + result.Add(orderedAccounts[i]); + amount -= orderedAccounts[i].Value; + orderedAccounts.RemoveAt(i); + i--; + } + if (amount > 0) + { + for (i = 0; i < orderedAccounts.Count; i++) + { + if (orderedAccounts[i].Value < amount) + continue; + if (orderedAccounts[i].Value == amount) + { + result.Add(orderedAccounts[i]); + orderedAccounts.RemoveAt(i); + } + else + { + result.Add((orderedAccounts[i].Account, amount)); + orderedAccounts[i] = (orderedAccounts[i].Account, orderedAccounts[i].Value - amount); + } + break; + } + } + } + } + return result; + } + + public IEnumerable GetMultiSigAccounts() => + GetAccounts() + .Where(static w => + w.Lock == false && + w.Contract != null && + IsMultiSigContract(w.Contract.Script)); + + /// + /// Gets the account with the specified public key. + /// + /// The public key of the account. + /// The account with the specified public key. + public WalletAccount? GetAccount(ECPoint pubkey) + { + return GetAccount(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash()); + } + + /// + /// Gets the default account of the wallet. + /// + /// The default account of the wallet. + public virtual WalletAccount? GetDefaultAccount() + { + WalletAccount? first = null; + foreach (WalletAccount account in GetAccounts()) + { + if (account.IsDefault) return account; + first ??= account; + } + return first; + } + + /// + /// Gets the available balance for the specified asset in the wallet. + /// + /// The snapshot used to read data. + /// The id of the asset. + /// The available balance for the specified asset. + public BigDecimal GetAvailable(DataCache snapshot, UInt160 asset_id) + { + UInt160[] accounts = GetAccounts().Where(p => !p.WatchOnly).Select(p => p.ScriptHash).ToArray(); + return GetBalance(snapshot, asset_id, accounts); + } + + /// + /// Gets the balance for the specified asset in the wallet. + /// + /// The snapshot used to read data. + /// The id of the asset. + /// The accounts to be counted. + /// The balance for the specified asset. + public BigDecimal GetBalance(DataCache snapshot, UInt160 asset_id, params UInt160[] accounts) + { + byte[] script; + using (ScriptBuilder sb = new()) + { + sb.EmitPush(0); + foreach (UInt160 account in accounts) + { + sb.EmitDynamicCall(asset_id, "balanceOf", CallFlags.ReadOnly, account); + sb.Emit(OpCode.ADD); + } + sb.EmitDynamicCall(asset_id, "decimals", CallFlags.ReadOnly); + script = sb.ToArray(); + } + using ApplicationEngine engine = ApplicationEngine.Run(script, snapshot, settings: ProtocolSettings, gas: 0_60000000L * accounts.Length); + if (engine.State == VMState.FAULT) + return new BigDecimal(BigInteger.Zero, 0); + byte decimals = (byte)engine.ResultStack.Pop().GetInteger(); + BigInteger amount = engine.ResultStack.Pop().GetInteger(); + return new BigDecimal(amount, decimals); + } + + private static byte[] Decrypt(byte[] data, byte[] key) + { + using Aes aes = Aes.Create(); + aes.Key = key; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + using ICryptoTransform decryptor = aes.CreateDecryptor(); + return decryptor.TransformFinalBlock(data, 0, data.Length); + } + + /// + /// Decodes a private key from the specified NEP-2 string. + /// + /// The NEP-2 string to be decoded. + /// The passphrase of the private key. + /// The address version of NEO system. + /// The N field of the to be used. + /// The R field of the to be used. + /// The P field of the to be used. + /// The decoded private key. + public static byte[] GetPrivateKeyFromNEP2(string nep2, string passphrase, byte version, int N = 16384, int r = 8, int p = 8) + { + byte[] passphrasedata = Encoding.UTF8.GetBytes(passphrase); + try + { + return GetPrivateKeyFromNEP2(nep2, passphrasedata, version, N, r, p); + } + finally + { + passphrasedata.AsSpan().Clear(); + } + } + + /// + /// Decodes a private key from the specified NEP-2 string. + /// + /// The NEP-2 string to be decoded. + /// The passphrase of the private key. + /// The address version of NEO system. + /// The N field of the to be used. + /// The R field of the to be used. + /// The P field of the to be used. + /// The decoded private key. + public static byte[] GetPrivateKeyFromNEP2(string nep2, byte[] passphrase, byte version, int N = 16384, int r = 8, int p = 8) + { + ArgumentNullException.ThrowIfNull(nep2); + ArgumentNullException.ThrowIfNull(passphrase); + + byte[] data = nep2.Base58CheckDecode(); + if (data.Length != 39 || data[0] != 0x01 || data[1] != 0x42 || data[2] != 0xe0) + throw new FormatException("Invalid NEP-2 key"); + + byte[] addresshash = new byte[4]; + Buffer.BlockCopy(data, 3, addresshash, 0, 4); + + byte[] derivedkey = SCrypt.Generate(passphrase, addresshash, N, r, p, 64); + byte[] derivedhalf1 = derivedkey[..32]; + byte[] derivedhalf2 = derivedkey[32..]; + Array.Clear(derivedkey, 0, derivedkey.Length); + + byte[] encryptedkey = new byte[32]; + Buffer.BlockCopy(data, 7, encryptedkey, 0, 32); + Array.Clear(data, 0, data.Length); + + byte[] prikey = XOR(Decrypt(encryptedkey, derivedhalf2), derivedhalf1); + Array.Clear(derivedhalf1, 0, derivedhalf1.Length); + Array.Clear(derivedhalf2, 0, derivedhalf2.Length); + + ECPoint pubkey = ECCurve.Secp256r1.G * prikey; + UInt160 script_hash = Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash(); + string address = script_hash.ToAddress(version); + if (!Encoding.ASCII.GetBytes(address).Sha256().Sha256().AsSpan(0, 4).SequenceEqual(addresshash)) + throw new FormatException("The address hash in NEP-2 key is not valid"); + return prikey; + } + + /// + /// Decodes a private key from the specified WIF string. + /// + /// The WIF string to be decoded. + /// The decoded private key. + public static byte[] GetPrivateKeyFromWIF(string wif) + { + ArgumentNullException.ThrowIfNull(wif); + byte[] data = wif.Base58CheckDecode(); + + if (data.Length != 34 || data[0] != 0x80 || data[33] != 0x01) + throw new FormatException("Invalid WIF key"); + + byte[] privateKey = new byte[32]; + Buffer.BlockCopy(data, 1, privateKey, 0, privateKey.Length); + Array.Clear(data, 0, data.Length); + return privateKey; + } + + private static Signer[] GetSigners(UInt160 sender, Signer[] cosigners) + { + for (int i = 0; i < cosigners.Length; i++) + { + if (cosigners[i].Account.Equals(sender)) + { + if (i == 0) return cosigners; + List list = new(cosigners); + list.RemoveAt(i); + list.Insert(0, cosigners[i]); + return list.ToArray(); + } + } + return cosigners.Prepend(new Signer + { + Account = sender, + Scopes = WitnessScope.None + }).ToArray(); + } + + /// + /// Imports an account from a . + /// + /// The to import. + /// The imported account. + public virtual WalletAccount Import(X509Certificate2 cert) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + throw new PlatformNotSupportedException("Importing certificates is not supported on macOS."); + } + byte[] privateKey; + using (ECDsa ecdsa = cert.GetECDsaPrivateKey() ?? throw new ArgumentException("The certificate must contains a private key.", nameof(cert))) + { + privateKey = ecdsa.ExportParameters(true).D!; + } + WalletAccount account = CreateAccount(privateKey); + Array.Clear(privateKey, 0, privateKey.Length); + return account; + } + + /// + /// Imports an account from the specified WIF string. + /// + /// The WIF string to import. + /// The imported account. + public virtual WalletAccount Import(string wif) + { + byte[] privateKey = GetPrivateKeyFromWIF(wif); + WalletAccount account = CreateAccount(privateKey); + Array.Clear(privateKey, 0, privateKey.Length); + return account; + } + + /// + /// Imports an account from the specified NEP-2 string. + /// + /// The NEP-2 string to import. + /// The passphrase of the private key. + /// The N field of the to be used. + /// The R field of the to be used. + /// The P field of the to be used. + /// The imported account. + public virtual WalletAccount Import(string nep2, string passphrase, int N = 16384, int r = 8, int p = 8) + { + byte[] privateKey = GetPrivateKeyFromNEP2(nep2, passphrase, ProtocolSettings.AddressVersion, N, r, p); + WalletAccount account = CreateAccount(privateKey); + Array.Clear(privateKey, 0, privateKey.Length); + return account; + } + + /// + /// Makes a transaction to transfer assets. + /// + /// The snapshot used to read data. + /// The array of that contain the asset, amount, and targets of the transfer. + /// The account to transfer from. + /// The cosigners to be added to the transaction. + /// + /// The block environment to execute the transaction. + /// If null, will be used. + /// + /// The created transaction. + public Transaction MakeTransaction(DataCache snapshot, TransferOutput[] outputs, UInt160? from = null, Signer[]? cosigners = null, Block? persistingBlock = null) + { + UInt160[] accounts; + if (from is null) + { + accounts = GetAccounts().Where(p => !p.Lock && !p.WatchOnly).Select(p => p.ScriptHash).ToArray(); + } + else + { + accounts = new[] { from }; + } + Dictionary cosignerList = cosigners?.ToDictionary(p => p.Account) ?? new Dictionary(); + byte[] script; + List<(UInt160 Account, BigInteger Value)>? balances_gas = null; + using (ScriptBuilder sb = new()) + { + foreach (var (assetId, group, sum) in outputs.GroupBy(p => p.AssetId, (k, g) => (k, g, g.Select(p => p.Value.Value).Sum()))) + { + var balances = new List<(UInt160 Account, BigInteger Value)>(); + foreach (UInt160 account in accounts) + { + using ScriptBuilder sb2 = new(); + sb2.EmitDynamicCall(assetId, "balanceOf", CallFlags.ReadOnly, account); + using ApplicationEngine engine = ApplicationEngine.Run(sb2.ToArray(), snapshot, settings: ProtocolSettings, persistingBlock: persistingBlock); + if (engine.State != VMState.HALT) + throw new InvalidOperationException($"Failed to execute balanceOf method for asset {assetId} on account {account}. The smart contract execution faulted with state: {engine.State}."); + BigInteger value = engine.ResultStack.Pop().GetInteger(); + if (value.Sign > 0) balances.Add((account, value)); + } + BigInteger sum_balance = balances.Select(p => p.Value).Sum(); + if (sum_balance < sum) + throw new InvalidOperationException($"Insufficient balance for transfer: required {sum} units, but only {sum_balance} units are available across all accounts. Please ensure sufficient balance before attempting the transfer."); + foreach (TransferOutput output in group) + { + balances = balances.OrderBy(p => p.Value).ToList(); + var balances_used = FindPayingAccounts(balances, output.Value.Value); + foreach (var (account, value) in balances_used) + { + if (cosignerList.TryGetValue(account, out Signer? signer)) + { + if (signer.Scopes != WitnessScope.Global) + signer.Scopes |= WitnessScope.CalledByEntry; + } + else + { + cosignerList.Add(account, new Signer + { + Account = account, + Scopes = WitnessScope.CalledByEntry + }); + } + sb.EmitDynamicCall(output.AssetId, "transfer", account, output.ScriptHash, value, output.Data); + sb.Emit(OpCode.ASSERT); + } + } + if (assetId.Equals(NativeContract.GAS.Hash)) + balances_gas = balances; + } + script = sb.ToArray(); + } + balances_gas ??= accounts.Select(p => (Account: p, Value: NativeContract.GAS.BalanceOf(snapshot, p))).Where(p => p.Value.Sign > 0).ToList(); + + return MakeTransaction(snapshot, script, cosignerList.Values.ToArray(), [], balances_gas, persistingBlock: persistingBlock); + } + + /// + /// Makes a transaction to run a smart contract. + /// + /// The snapshot used to read data. + /// The script to be loaded in the transaction. + /// The sender of the transaction. + /// The cosigners to be added to the transaction. + /// The attributes to be added to the transaction. + /// + /// The maximum gas that can be spent to execute the script, in the unit of datoshi, 1 datoshi = 1e-8 GAS. + /// + /// + /// The block environment to execute the transaction. + /// If null, will be used. + /// + /// The created transaction. + public Transaction MakeTransaction(DataCache snapshot, ReadOnlyMemory script, + UInt160? sender = null, Signer[]? cosigners = null, TransactionAttribute[]? attributes = null, + long maxGas = ApplicationEngine.TestModeGas, Block? persistingBlock = null) + { + UInt160[] accounts; + if (sender is null) + { + accounts = GetAccounts().Where(p => !p.Lock && !p.WatchOnly).Select(p => p.ScriptHash).ToArray(); + } + else + { + accounts = new[] { sender }; + } + + var balancesGas = accounts.Select(p => (Account: p, Value: NativeContract.GAS.BalanceOf(snapshot, p))) + .Where(p => p.Value.Sign > 0) + .ToList(); + return MakeTransaction(snapshot, script, cosigners ?? [], attributes ?? [], balancesGas, maxGas, persistingBlock: persistingBlock); + } + + private Transaction MakeTransaction(DataCache snapshot, ReadOnlyMemory script, Signer[] cosigners, + TransactionAttribute[] attributes, List<(UInt160 Account, BigInteger Value)> balancesGas, + long maxGas = ApplicationEngine.TestModeGas, Block? persistingBlock = null) + { + foreach (var (account, value) in balancesGas) + { + Transaction tx = new() + { + Version = 0, + Nonce = RandomNumberFactory.NextUInt32(), + Script = script, + ValidUntilBlock = NativeContract.Ledger.CurrentIndex(snapshot) + ProtocolSettings.MaxValidUntilBlockIncrement, + Signers = GetSigners(account, cosigners), + Attributes = attributes, + Witnesses = null! + }; + + // will try to execute 'transfer' script to check if it works + using (ApplicationEngine engine = ApplicationEngine.Run(script, snapshot.CloneCache(), tx, + settings: ProtocolSettings, gas: maxGas, persistingBlock: persistingBlock)) + { + if (engine.State == VMState.FAULT) + { + throw new InvalidOperationException($"Smart contract execution failed for script '{Convert.ToBase64String(script.Span)}'. The execution faulted and cannot be completed.", engine.FaultException); + } + tx.SystemFee = engine.FeeConsumed; + } + + tx.NetworkFee = tx.CalculateNetworkFee(snapshot, ProtocolSettings, this, maxGas); + if (value >= tx.SystemFee + tx.NetworkFee) return tx; + } + throw new InvalidOperationException("Insufficient GAS balance to cover system and network fees. Please ensure your account has enough GAS to pay for transaction fees."); + } + + /// + /// Signs the in the specified with the wallet. + /// + /// The to be used. + /// + /// if any signature is successfully added to the context; + /// otherwise, . + /// + public bool Sign(ContractParametersContext context) + { + if (context.Network != ProtocolSettings.Network) return false; + + var fSuccess = false; + foreach (var scriptHash in context.ScriptHashes) + { + var account = GetAccount(scriptHash); + if (account != null) + { + if (account.Lock) continue; + + // Try to sign self-contained multiSig + var multiSigContract = account.Contract; + if (multiSigContract != null && + IsMultiSigContract(multiSigContract.Script, out int m, out ECPoint[]? points)) + { + foreach (var point in points) + { + account = GetAccount(point); + if (account?.HasKey != true) continue; // check `Lock` or not? + + var key = account.GetKey()!; + var signature = context.Verifiable.Sign(key, context.Network); + var ok = context.AddSignature(multiSigContract, key.PublicKey, signature); + if (ok) m--; + + fSuccess |= ok; + if (context.Completed || m <= 0) break; + } + continue; + } + else if (account.HasKey) + { + // Try to sign with regular accounts + var key = account.GetKey()!; + var signature = context.Verifiable.Sign(key, context.Network); + fSuccess |= context.AddSignature(account.Contract!, key.PublicKey, signature); + continue; + } + } + + // Try Smart contract verification + fSuccess |= context.AddWithScriptHash(scriptHash); + } + + return fSuccess; + } + + /// + /// Signs the specified extensible payload with the wallet. + /// + /// The extensible payload to sign. + /// The snapshot. + /// The network. + /// The signature. + /// Thrown when the payload is null. + public Witness SignExtensiblePayload(ExtensiblePayload payload, DataCache snapshot, uint network) + { + ArgumentNullException.ThrowIfNull(payload); + + var context = new ContractParametersContext(snapshot, payload, network); + Sign(context); + + return context.GetWitnesses()[0]; + } + + /// + /// Signs the specified block with the specified public key. + /// + /// The block to sign. + /// The public key. + /// The network. + /// The signature. + /// Thrown when the block or public key is null. + /// + /// Thrown when the account is not found, the private key is not found, the account is locked, + /// or the network is not matching. + /// + public ReadOnlyMemory SignBlock(Block block, ECPoint publicKey, uint network) + { + ArgumentNullException.ThrowIfNull(block); + ArgumentNullException.ThrowIfNull(publicKey); + if (network != ProtocolSettings.Network) + throw new SignException($"Network is not matching({ProtocolSettings.Network} != {network})"); + + var account = GetAccount(publicKey) + ?? throw new SignException("No such account found"); + + var privateKey = account.GetKey()?.PrivateKey + ?? throw new SignException("No private key found for the given public key"); + + if (account.Lock) + throw new SignException("Account is locked"); + + var signData = block.GetSignData(network); + return Crypto.Sign(signData, privateKey); + } + + /// + /// Checks if the wallet contains an account with the specified public key. + /// + /// The public key. + /// + /// if the account is found and has a private key and is not locked; + /// otherwise, . + /// + public bool ContainsSignable(ECPoint publicKey) + { + var account = GetAccount(publicKey); + return account != null && account.HasKey && !account.Lock; + } + + /// + /// Checks that the specified password is correct for the wallet. + /// + /// The password to be checked. + /// if the password is correct; otherwise, . + public abstract bool VerifyPassword(string password); + + /// + /// Saves the wallet file to the disk. It uses the value of property. + /// + public abstract void Save(); + + public static Wallet? Create(string? name, string path, string password, ProtocolSettings settings) + { + return GetFactory(path)?.CreateWallet(name, path, password, settings); + } + + public static Wallet? Open(string path, string password, ProtocolSettings settings) + { + return GetFactory(path)?.OpenWallet(path, password, settings); + } + + /// + /// Migrates the accounts from old wallet to a new . + /// + /// The password of the wallets. + /// The path of the new wallet file. + /// The path of the old wallet file. + /// The to be used by the wallet. + /// The created new wallet. + public static Wallet Migrate(string path, string oldPath, string password, ProtocolSettings settings) + { + IWalletFactory factoryOld = GetFactory(oldPath) + ?? throw new InvalidOperationException("The old wallet file format is not supported."); + IWalletFactory factoryNew = GetFactory(path) + ?? throw new InvalidOperationException("The new wallet file format is not supported."); + + Wallet oldWallet = factoryOld.OpenWallet(oldPath, password, settings); + Wallet newWallet = factoryNew.CreateWallet(oldWallet.Name, path, password, settings); + + foreach (WalletAccount account in oldWallet.GetAccounts()) + { + if (account.Contract != null) + newWallet.CreateAccount(account.Contract, account.GetKey()); + } + return newWallet; + } + + private static IWalletFactory? GetFactory(string path) + { + return factories.FirstOrDefault(p => p.Handle(path)); + } + + public static void RegisterFactory(IWalletFactory factory) + { + factories.Add(factory); + } +} diff --git a/src/Neo/Wallets/WalletAccount.cs b/src/Neo/Wallets/WalletAccount.cs new file mode 100644 index 0000000000..d9cae44679 --- /dev/null +++ b/src/Neo/Wallets/WalletAccount.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// WalletAccount.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.Wallets; + +/// +/// Represents an account in a wallet. +/// +public abstract class WalletAccount +{ + /// + /// The to be used by the wallet. + /// + protected readonly ProtocolSettings ProtocolSettings; + + /// + /// The hash of the account. + /// + public readonly UInt160 ScriptHash; + + /// + /// The label of the account. + /// + public string? Label; + + /// + /// Indicates whether the account is the default account in the wallet. + /// + public bool IsDefault; + + /// + /// Indicates whether the account is locked. + /// + public bool Lock; + + /// + /// The contract of the account. + /// + public Contract? Contract; + + /// + /// The address of the account. + /// + public string Address => ScriptHash.ToAddress(ProtocolSettings.AddressVersion); + + /// + /// Indicates whether the account contains a private key. + /// + public abstract bool HasKey { get; } + + /// + /// Indicates whether the account is a watch-only account. + /// + public bool WatchOnly => Contract == null; + + /// + /// Gets the private key of the account. + /// + /// The private key of the account. Or if there is no private key in the account. + public abstract KeyPair? GetKey(); + + /// + /// Initializes a new instance of the class. + /// + /// The hash of the account. + /// The to be used by the wallet. + protected WalletAccount(UInt160 scriptHash, ProtocolSettings settings) + { + ProtocolSettings = settings; + ScriptHash = scriptHash; + } +} diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 0000000000..f14e2107c2 --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,9 @@ +root = false + +[*.cs] + +# CA1861: 不要将常量数组作为参数 +dotnet_diagnostic.CA1861.severity = silent + +# SYSLIB1045: 转换为“GeneratedRegexAttribute”。 +dotnet_diagnostic.SYSLIB1045.severity = silent diff --git a/tests/AssemblyInfo.cs b/tests/AssemblyInfo.cs new file mode 100644 index 0000000000..fc6feea077 --- /dev/null +++ b/tests/AssemblyInfo.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2015-2025 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. + +// Test projects that wish to enable parallelization should add the following in csproj: +// +// $(DefineConstants);DISABLE_TEST_PARALLELIZATION +// +#if ENABLE_TEST_PARALLELIZATION +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] +#else +[assembly: DoNotParallelize] +#endif diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000000..1a252a9fbb --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,28 @@ + + + + + true + + + + Exe + net10.0 + enable + enable + false + + + + + + + + + + + + + + + diff --git a/tests/Neo.Extensions.Tests/Collections/UT_CollectionExtensions.cs b/tests/Neo.Extensions.Tests/Collections/UT_CollectionExtensions.cs new file mode 100644 index 0000000000..d6f10af275 --- /dev/null +++ b/tests/Neo.Extensions.Tests/Collections/UT_CollectionExtensions.cs @@ -0,0 +1,100 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_CollectionExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Collections; + +namespace Neo.Extensions.Tests.Collections; + +[TestClass] +public class UT_CollectionExtensions +{ + [TestMethod] + public void TestChunk() + { + var source = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var chunks = source.Chunk(3).GetEnumerator(); + + Assert.IsTrue(chunks.MoveNext()); + CollectionAssert.AreEqual(new[] { 1, 2, 3 }, chunks.Current); + + Assert.IsTrue(chunks.MoveNext()); + CollectionAssert.AreEqual(new[] { 4, 5, 6 }, chunks.Current); + + Assert.IsTrue(chunks.MoveNext()); + CollectionAssert.AreEqual(new[] { 7, 8, 9 }, chunks.Current); + + Assert.IsTrue(chunks.MoveNext()); + CollectionAssert.AreEqual(new[] { 10 }, chunks.Current); + + // Empty source + var empty = new List(); + var emptyChunks = empty.Chunk(3).GetEnumerator(); + Assert.IsFalse(emptyChunks.MoveNext()); + + // Zero chunk size + var zero = new List { 1, 2, 3 }; + var zeroChunks = zero.Chunk(0).GetEnumerator(); + Assert.ThrowsExactly(() => _ = zeroChunks.MoveNext()); + + // Null source + IReadOnlyCollection? nullSource = null; + var nullChunks = nullSource.Chunk(3).GetEnumerator(); + Assert.IsFalse(emptyChunks.MoveNext()); + + // HashSet + var hashSet = new HashSet { 1, 2, 3, 4 }; + chunks = hashSet.Chunk(3).GetEnumerator(); + + Assert.IsTrue(chunks.MoveNext()); + CollectionAssert.AreEqual(new[] { 1, 2, 3 }, chunks.Current); + + Assert.IsTrue(chunks.MoveNext()); + CollectionAssert.AreEqual(new[] { 4 }, chunks.Current); + } + + [TestMethod] + public void TestRemoveWhere() + { + var dict = new Dictionary + { + [1] = "a", + [2] = "b", + [3] = "c" + }; + + dict.RemoveWhere(p => p.Value == "b"); + + Assert.HasCount(2, dict); + Assert.IsFalse(dict.ContainsKey(2)); + Assert.AreEqual("a", dict[1]); + Assert.AreEqual("c", dict[3]); + } + + [TestMethod] + public void TestSample() + { + var list = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var sampled = list.Sample(5); + Assert.HasCount(5, sampled); + foreach (var item in sampled) Assert.Contains(item, list); + + sampled = list.Sample(10); + Assert.HasCount(10, sampled); + foreach (var item in sampled) Assert.Contains(item, list); + + sampled = list.Sample(0); + Assert.IsEmpty(sampled); + + sampled = list.Sample(100); + Assert.HasCount(10, sampled); + foreach (var item in sampled) Assert.Contains(item, list); + } +} diff --git a/tests/Neo.Extensions.Tests/Collections/UT_HashSetExtensions.cs b/tests/Neo.Extensions.Tests/Collections/UT_HashSetExtensions.cs new file mode 100644 index 0000000000..b285821762 --- /dev/null +++ b/tests/Neo.Extensions.Tests/Collections/UT_HashSetExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_HashSetExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Collections; + +namespace Neo.Extensions.Tests.Collections; + +[TestClass] +public class UT_HashSetExtensions +{ + [TestMethod] + public void TestRemoveHashsetDictionary() + { + var a = new HashSet + { + 1, + 2, + 3 + }; + + var b = new Dictionary + { + [2] = null + }; + + a.Remove(b); + + CollectionAssert.AreEqual(new int[] { 1, 3 }, a.ToArray()); + + b[4] = null; + b[5] = null; + b[1] = null; + a.Remove(b); + + CollectionAssert.AreEqual(new int[] { 3 }, a.ToArray()); + } + + [TestMethod] + public void TestRemoveHashsetSet() + { + var a = new HashSet + { + 1, + 2, + 3 + }; + + var b = new SortedSet() + { + 2 + }; + + a.Remove(b); + + CollectionAssert.AreEqual(new int[] { 1, 3 }, a.ToArray()); + + b.Add(4); + b.Add(5); + b.Add(1); + a.Remove(b); + + CollectionAssert.AreEqual(new int[] { 3 }, a.ToArray()); + } +} diff --git a/tests/Neo.Extensions.Tests/Exceptions/UT_TryCatchExceptions.cs b/tests/Neo.Extensions.Tests/Exceptions/UT_TryCatchExceptions.cs new file mode 100644 index 0000000000..47ea1ed9ef --- /dev/null +++ b/tests/Neo.Extensions.Tests/Exceptions/UT_TryCatchExceptions.cs @@ -0,0 +1,76 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_TryCatchExceptions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Exceptions; + +namespace Neo.Extensions.Tests.Exceptions; + +[TestClass] +public class UT_TryCatchExceptions +{ + [TestMethod] + public void TestTryCatchMethods() + { + var actualObject = new object(); + + // action + actualObject.TryCatch(a => actualObject = a = null); + Assert.IsNull(actualObject); + + // action + actualObject.TryCatch(a => throw new ArgumentException(), (_, ex) => actualObject = ex); + Assert.IsInstanceOfType(actualObject); + + var expectedObject = new object(); + + // func + actualObject = expectedObject.TryCatch( + a => throw new ArgumentException(), + (_, ex) => ex); + Assert.IsInstanceOfType(actualObject); + } + + [TestMethod] + public void TestTryCatchThrowMethods() + { + var actualObject = new object(); + + //action + Assert.ThrowsExactly( + () => actualObject.TryCatchThrow(a => throw new ArgumentException())); + + Assert.ThrowsExactly( + () => actualObject.TryCatchThrow(a => + { + throw new ArgumentException(); + })); + + var expectedMessage = "Hello World"; + + try + { + actualObject.TryCatchThrow(a => throw new ArgumentException(), expectedMessage); + } + catch (ArgumentException actualException) + { + Assert.AreEqual(expectedMessage, actualException.Message); + } + + try + { + actualObject.TryCatchThrow(a => throw new ArgumentException(), expectedMessage); + } + catch (ArgumentException actualException) + { + Assert.AreEqual(expectedMessage, actualException.Message); + } + } +} diff --git a/tests/Neo.Extensions.Tests/Factories/UT_RandomNumberFactory.cs b/tests/Neo.Extensions.Tests/Factories/UT_RandomNumberFactory.cs new file mode 100644 index 0000000000..01c79bfed2 --- /dev/null +++ b/tests/Neo.Extensions.Tests/Factories/UT_RandomNumberFactory.cs @@ -0,0 +1,323 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_RandomNumberFactory.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + + +// Copyright (C) 2015-2025 The Neo Project. +// +// RandomNumberFactory.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using System.Numerics; + +namespace Neo.Extensions.Tests.Factories; + +[TestClass] +public class UT_RandomNumberFactory +{ + [TestMethod] + public void CheckNextSByteInRange() + { + var expectedMax = sbyte.MaxValue; + sbyte expectedMin = 0; + + Assert.AreEqual(expectedMax, RandomNumberFactory.NextSByte(expectedMax, expectedMax)); + Assert.AreEqual(expectedMin, RandomNumberFactory.NextSByte(expectedMin, expectedMin)); + + var actualValue = RandomNumberFactory.NextSByte(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextSByte(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextSByteNegative() + { + sbyte expectedMax = 0; + var expectedMin = sbyte.MinValue; + + var actualValue = RandomNumberFactory.NextSByte(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextSByte(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue <= expectedMax); + } + + [TestMethod] + public void CheckNextSByteExceptions() + { + Assert.ThrowsExactly(() => RandomNumberFactory.NextSByte(-1)); + Assert.ThrowsExactly(() => RandomNumberFactory.NextSByte(-1, -2)); + } + + [TestMethod] + public void CheckNextByteInRange() + { + var expectedMax = byte.MaxValue; + var expectedMin = byte.MinValue; + + Assert.AreEqual(expectedMax, RandomNumberFactory.NextByte(expectedMax, expectedMax)); + Assert.AreEqual(expectedMin, RandomNumberFactory.NextByte(expectedMin, expectedMin)); + + var actualValue = RandomNumberFactory.NextByte(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextByte(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextInt16InRange() + { + var expectedMax = short.MaxValue; + short expectedMin = 0; + + Assert.AreEqual(expectedMax, RandomNumberFactory.NextInt16(expectedMax, expectedMax)); + Assert.AreEqual(expectedMin, RandomNumberFactory.NextInt16(expectedMin, expectedMin)); + + var actualValue = RandomNumberFactory.NextInt16(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextInt16(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextInt16InNegative() + { + short expectedMax = 0; + var expectedMin = short.MinValue; + + var actualValue = RandomNumberFactory.NextInt16(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextInt16(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue <= expectedMax); + } + + [TestMethod] + public void CheckNextInt16Exceptions() + { + Assert.ThrowsExactly(() => RandomNumberFactory.NextInt16(-1)); + Assert.ThrowsExactly(() => RandomNumberFactory.NextInt16(-1, -2)); + } + + [TestMethod] + public void CheckNextUInt16InRange() + { + var expectedMax = ushort.MaxValue; + var expectedMin = ushort.MinValue; + + Assert.AreEqual(expectedMax, RandomNumberFactory.NextUInt16(expectedMax, expectedMax)); + Assert.AreEqual(expectedMin, RandomNumberFactory.NextUInt16(expectedMin, expectedMin)); + + var actualValue = RandomNumberFactory.NextUInt16(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextUInt16(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextInt32InRange() + { + var expectedMax = int.MaxValue; + var expectedMin = 0; + + Assert.AreEqual(expectedMax, RandomNumberFactory.NextInt32(expectedMax, expectedMax)); + Assert.AreEqual(expectedMin, RandomNumberFactory.NextInt32(expectedMin, expectedMin)); + + var actualValue = RandomNumberFactory.NextInt32(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextInt32(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextInt32InNegative() + { + var expectedMax = 0; + var expectedMin = int.MinValue; + + var actualValue = RandomNumberFactory.NextInt32(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextInt32Exceptions() + { + Assert.ThrowsExactly(() => RandomNumberFactory.NextInt32(-1)); + Assert.ThrowsExactly(() => RandomNumberFactory.NextInt32(-1, -2)); + } + + [TestMethod] + public void CheckNextUInt32InRange() + { + var expectedMax = uint.MaxValue; + var expectedMin = uint.MinValue; + + Assert.AreEqual(expectedMax, RandomNumberFactory.NextUInt32(expectedMax, expectedMax)); + Assert.AreEqual(expectedMin, RandomNumberFactory.NextUInt32(expectedMin, expectedMin)); + + var actualValue = RandomNumberFactory.NextUInt32(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextUInt32(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextInt64InRange() + { + var expectedMax = long.MaxValue; + var expectedMin = 0L; + + Assert.AreEqual(expectedMax, RandomNumberFactory.NextInt64(expectedMax, expectedMax)); + Assert.AreEqual(expectedMin, RandomNumberFactory.NextInt64(expectedMin, expectedMin)); + + var actualValue = RandomNumberFactory.NextInt64(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextInt64(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextInt64InNegative() + { + var expectedMax = 0L; + var expectedMin = long.MinValue; + + var actualValue = RandomNumberFactory.NextInt64(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextInt64Exceptions() + { + Assert.ThrowsExactly(() => RandomNumberFactory.NextInt64(-1L)); + Assert.ThrowsExactly(() => RandomNumberFactory.NextInt64(-1L, -2L)); + } + + [TestMethod] + public void CheckNextUInt64InRange() + { + var expectedMax = ulong.MaxValue; + var expectedMin = ulong.MinValue; + + Assert.AreEqual(expectedMax, RandomNumberFactory.NextUInt64(expectedMax, expectedMax)); + Assert.AreEqual(expectedMin, RandomNumberFactory.NextUInt64(expectedMin, expectedMin)); + + var actualValue = RandomNumberFactory.NextUInt64(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextUInt64(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextBigIntegerSizeInBits() + { + var actualValue = RandomNumberFactory.NextBigInteger(byte.MaxValue); + Assert.IsTrue(actualValue >= BigInteger.Zero); + + actualValue = RandomNumberFactory.NextBigInteger(0); + Assert.AreEqual(BigInteger.Zero, actualValue); + + Assert.ThrowsExactly(() => RandomNumberFactory.NextBigInteger(-1)); + } + + [TestMethod] + public void CheckNextBigIntegerInRange() + { + var expectedMax = BigInteger.Pow(2, 100); + var expectedMin = BigInteger.Pow(2, 50); + + Assert.AreEqual(expectedMax, RandomNumberFactory.NextBigInteger(expectedMax, expectedMax)); + Assert.AreEqual(expectedMin, RandomNumberFactory.NextBigInteger(expectedMin, expectedMin)); + + var actualValue = RandomNumberFactory.NextBigInteger(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextBigInteger(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextBigIntegerInNegative() + { + var expectedMax = BigInteger.Zero; + var expectedMin = BigInteger.Pow(2, 100) * BigInteger.MinusOne; + + var actualValue = RandomNumberFactory.NextBigInteger(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + Assert.IsLessThan(0, actualValue.Sign); + } + + [TestMethod] + public void CheckNextBigIntegerMaxNegative() + { + Assert.ThrowsExactly(() => RandomNumberFactory.NextBigInteger(BigInteger.MinusOne)); + Assert.ThrowsExactly(() => RandomNumberFactory.NextBigInteger(BigInteger.MinusOne, -2)); + } + + [TestMethod] + public void CheckNextBigIntegerSmallValues() + { + var expectedMax = (BigInteger)10; + var expectedMin = BigInteger.Zero; + + var actualValue = RandomNumberFactory.NextBigInteger(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue < expectedMax); + + actualValue = RandomNumberFactory.NextBigInteger(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue < expectedMax); + } + + [TestMethod] + public void CheckNextBigIntegerZero() + { + var expectedMax = BigInteger.Zero; + var expectedMin = BigInteger.Zero; + + var actualValue = RandomNumberFactory.NextBigInteger(expectedMin, expectedMax); + Assert.IsTrue(actualValue >= expectedMin && actualValue <= expectedMax); + + actualValue = RandomNumberFactory.NextBigInteger(expectedMax); + Assert.IsTrue(actualValue >= 0 && actualValue <= expectedMax); + } + + [TestMethod] + public void CheckNextBytes() + { + var notExpectedBytes = new byte[10]; + + var actualBytes1 = RandomNumberFactory.NextBytes(10); + Assert.IsNotEmpty(actualBytes1); + CollectionAssert.AreNotEqual(notExpectedBytes, actualBytes1); + Assert.HasCount(10, actualBytes1); + + var actualBytes2 = RandomNumberFactory.NextBytes(10, cryptography: true); + Assert.IsNotEmpty(actualBytes2); + CollectionAssert.AreNotEqual(notExpectedBytes, actualBytes2); + Assert.HasCount(10, actualBytes2); + + CollectionAssert.AreNotEqual(actualBytes1, actualBytes2); + } +} diff --git a/tests/Neo.Extensions.Tests/Neo.Extensions.Tests.csproj b/tests/Neo.Extensions.Tests/Neo.Extensions.Tests.csproj new file mode 100644 index 0000000000..f966f60f01 --- /dev/null +++ b/tests/Neo.Extensions.Tests/Neo.Extensions.Tests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/Neo.Extensions.Tests/Net/UT_IpAddressExtensions.cs b/tests/Neo.Extensions.Tests/Net/UT_IpAddressExtensions.cs new file mode 100644 index 0000000000..d4057a8a8f --- /dev/null +++ b/tests/Neo.Extensions.Tests/Net/UT_IpAddressExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_IpAddressExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; +using System.Net; + +namespace Neo.Extensions.Tests.Net; + +[TestClass] +public class UT_IpAddressExtensions +{ + [TestMethod] + public void TestUnmapForIPAddress() + { + var addr = new IPAddress(new byte[] { 127, 0, 0, 1 }); + Assert.AreEqual(addr, addr.UnMap()); + + var addr2 = addr.MapToIPv6(); + Assert.AreEqual(addr, addr2.UnMap()); + } + + [TestMethod] + public void TestUnmapForIPEndPoin() + { + var addr = new IPAddress(new byte[] { 127, 0, 0, 1 }); + var endPoint = new IPEndPoint(addr, 8888); + Assert.AreEqual(endPoint, endPoint.UnMap()); + + var addr2 = addr.MapToIPv6(); + var endPoint2 = new IPEndPoint(addr2, 8888); + Assert.AreEqual(endPoint, endPoint2.UnMap()); + } +} diff --git a/tests/Neo.Extensions.Tests/UT_BigIntegerExtensions.cs b/tests/Neo.Extensions.Tests/UT_BigIntegerExtensions.cs new file mode 100644 index 0000000000..f8440cee46 --- /dev/null +++ b/tests/Neo.Extensions.Tests/UT_BigIntegerExtensions.cs @@ -0,0 +1,352 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_BigIntegerExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using Neo.Json; +using System.Buffers.Binary; +using System.Numerics; + +namespace Neo.Extensions.Tests; + +[TestClass] +public class UT_BigIntegerExtensions +{ + [TestMethod] + public void CeilingDivide_NegativeNumerator() + { + var actual = BigIntegerExtensions.DivideCeiling(-7, 3); + Assert.AreEqual(-2, actual); + + actual = BigIntegerExtensions.DivideCeiling(-7, -3); + Assert.AreEqual(3, actual); + + actual = BigIntegerExtensions.DivideCeiling(-1, -3); + Assert.AreEqual(1, actual); + + actual = BigIntegerExtensions.DivideCeiling(-1, 3); + Assert.AreEqual(0, actual); + + actual = BigIntegerExtensions.DivideCeiling(1, -3); + Assert.AreEqual(0, actual); + + actual = BigIntegerExtensions.DivideCeiling(7, -3); + Assert.AreEqual(-2, actual); + + actual = BigIntegerExtensions.DivideCeiling(12345, -1234); + Assert.AreEqual(-10, actual); + } + + [TestMethod] + public void CeilingDivide_DividesExactly() + { + var actual = BigIntegerExtensions.DivideCeiling(9, 3); + Assert.AreEqual(3, actual); + } + + [TestMethod] + public void CeilingDivide_RoundsUp() + { + var actual = BigIntegerExtensions.DivideCeiling(10, 3); + Assert.AreEqual(4, actual); + } + + [TestMethod] + public void CeilingDivide_LargeNumbers() + { + var a = BigInteger.Parse("1000000000000000000000000000000000"); + var b = new BigInteger(7); + var actual = BigIntegerExtensions.DivideCeiling(a, b); + + Assert.AreEqual((a + b - 1) / b, actual); + } + + [TestMethod] + public void CeilingDivide_DivisorOne() + { + var actual = BigIntegerExtensions.DivideCeiling(12345, 1); + Assert.AreEqual(12345, actual); + } + + [TestMethod] + public void CeilingDivide_ThrowsOnZeroDivisor() + { + Assert.Throws(() => BigIntegerExtensions.DivideCeiling(10, 0)); + } + + [TestMethod] + public void TestGetLowestSetBit() + { + var big1 = new BigInteger(0); + Assert.AreEqual(-1, big1.GetLowestSetBit()); + Assert.AreEqual(32, BigInteger.TrailingZeroCount(big1)); // NOTE: 32 if zero in standard library + + var big2 = new BigInteger(512); + Assert.AreEqual(9, big2.GetLowestSetBit()); + Assert.AreEqual(9, BigInteger.TrailingZeroCount(big2)); + + var big3 = new BigInteger(int.MinValue); + Assert.AreEqual(31, big3.GetLowestSetBit()); + Assert.AreEqual(31, BigInteger.TrailingZeroCount(big3)); + + var big4 = new BigInteger(long.MinValue); + Assert.AreEqual(63, big4.GetLowestSetBit()); + Assert.AreEqual(63, BigInteger.TrailingZeroCount(big4)); + + var big5 = new BigInteger(-18); + Assert.AreEqual(1, big5.GetLowestSetBit()); + Assert.AreEqual(1, BigInteger.TrailingZeroCount(big5)); + + var big6 = BigInteger.Pow(2, 1000); + Assert.AreEqual(1000, big6.GetLowestSetBit()); + Assert.AreEqual(1000, BigInteger.TrailingZeroCount(big6)); + + for (var i = 0; i < 64; i++) + { + var b = new BigInteger(1ul << i); + Assert.AreEqual(i, BigInteger.TrailingZeroCount(b)); + } + + for (var i = 0; i < 128; i++) + { + var buffer = new byte[16]; + BinaryPrimitives.WriteInt128LittleEndian(buffer, Int128.One << i); + + var b = new BigInteger(buffer, isUnsigned: false); + Assert.AreEqual(i, BigInteger.TrailingZeroCount(b)); + + BinaryPrimitives.WriteUInt128LittleEndian(buffer, UInt128.One << i); + b = new BigInteger(buffer, isUnsigned: true); + Assert.AreEqual(i, BigInteger.TrailingZeroCount(b)); + } + } + + [TestMethod] + public void TestGetLowestSetBit_EdgeCases() + { + Assert.AreEqual(0, BigInteger.MinusOne.GetLowestSetBit()); + Assert.AreEqual(0, BigInteger.One.GetLowestSetBit()); + Assert.AreEqual(0, new BigInteger(ulong.MaxValue).GetLowestSetBit()); + Assert.AreEqual(1000, (BigInteger.One << 1000).GetLowestSetBit()); + } + + [TestMethod] + public void TestToByteArrayStandard() + { + BigInteger number = BigInteger.Zero; + CollectionAssert.AreEqual(Array.Empty(), number.ToByteArrayStandard()); + + number = BigInteger.One; + CollectionAssert.AreEqual(new byte[] { 0x01 }, number.ToByteArrayStandard()); + + number = new BigInteger(256); // Binary: 100000000 + CollectionAssert.AreEqual(new byte[] { 0x00, 0x01 }, number.ToByteArrayStandard()); + } + + [TestMethod] + public void TestToByteArrayStandard_EdgeCases() + { + CollectionAssert.AreEqual(new byte[] { 0xFF }, BigInteger.MinusOne.ToByteArrayStandard()); + CollectionAssert.AreEqual(new byte[] { 0xFF, 0x00 }, new BigInteger(byte.MaxValue).ToByteArrayStandard()); + CollectionAssert.AreEqual( + new byte[] { 0xFF, 0xFF, 0x00 }, + new BigInteger(ushort.MaxValue).ToByteArrayStandard() + ); + CollectionAssert.AreEqual( + new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0 }, + new BigInteger(JNumber.MIN_SAFE_INTEGER).ToByteArrayStandard() + ); + } + + [TestMethod] + public void TestMod() + { + var x = new BigInteger(-13); + var y = new BigInteger(5); + var result = x.Mod(y); + Assert.AreEqual(2, result); // -13 % 5 is -3, but Mod method should return 2 + } + + [TestMethod] + public void TestMod_EdgeCases() + { + // Test case 1: Mod of zero + Assert.AreEqual(0, BigInteger.Zero.Mod(5), "Mod of zero should always be zero"); + + // Test case 2: Mod of -1 + Assert.AreEqual(4, BigInteger.MinusOne.Mod(5), "Mod of -1 should return the modulus minus 1"); + + // Test case 3: Mod with large numbers + var minValue = new BigInteger(long.MinValue); + var maxValue = new BigInteger(long.MaxValue); + Assert.AreEqual(9223372036854775806, minValue.Mod(maxValue), "Mod with large numbers should be calculated correctly"); + + // Test case 4: Comparing Mod with % operator + BigInteger result = minValue.Mod(maxValue); + Assert.AreNotEqual(long.MinValue % long.MaxValue, result, "Mod should always return non-negative values, unlike % operator"); + + // Test case 5: Verifying % operator behavior + Assert.AreEqual(long.MinValue % long.MaxValue, (long)(minValue % maxValue), "% operator should behave consistently for BigInteger and long"); + + // Test case 6: Mod with prime numbers + Assert.AreEqual(17, new BigInteger(17).Mod(19), "Mod with a larger prime should return the original number"); + + // Test case 7: Mod with powers of 2 + Assert.AreEqual(0, new BigInteger(1024).Mod(16), "Mod with powers of 2 should utilize bitwise operations efficiently"); + } + + [TestMethod] + public void TestModInverse() + { + var a = new BigInteger(3); + var n = new BigInteger(11); + var result = a.ModInverse(n); + Assert.AreEqual(4, result); // 3 * 4 % 11 == 1 + + a = new BigInteger(1); + n = new BigInteger(11); + result = a.ModInverse(n); + Assert.AreEqual(1, result); // 1 * 1 % 11 == 1 + + a = new BigInteger(13); + n = new BigInteger(11); + result = a.ModInverse(n); + Assert.AreEqual(6, result); // 13 % 11 = 2, and 2 * 6 % 11 == 1 + + a = new BigInteger(6); + n = new BigInteger(12); // 6 and 12 are not coprime + Assert.ThrowsExactly(() => _ = a.ModInverse(n)); + } + + [TestMethod] + public void TestModInverse_EdgeCases() + { + Assert.ThrowsExactly(() => _ = BigInteger.Zero.ModInverse(11)); + + Assert.AreEqual(1, BigInteger.One.ModInverse(2)); + + Assert.ThrowsExactly(() => _ = new BigInteger(2).ModInverse(4)); + + Assert.AreEqual(long.MaxValue - 1, new BigInteger(long.MaxValue - 1).ModInverse(long.MaxValue)); + } + + [TestMethod] + public void TestBit() + { + var value = new BigInteger(5); // Binary: 101 + Assert.IsTrue(value.TestBit(2)); // Bit at index 2 is set (1) + + value = new BigInteger(5); // Binary: 101 + Assert.IsFalse(value.TestBit(1)); // Bit at index 1 is not set (0) + Assert.IsFalse(value.TestBit(10)); // Bit at index 10 is not set (0) + + value = new BigInteger(-3); + Assert.AreEqual(2, value.GetBitLength()); // 2, without sign bit + Assert.IsTrue(value.TestBit(255)); // Bit at index 255 is set (1) + + value = new BigInteger(3); // Binary: 11 + Assert.AreEqual(2, value.GetBitLength()); // 2, without sign bit + Assert.IsFalse(value.TestBit(255)); // Bit at index 255 is not set (0) + Assert.IsTrue(value.TestBit(0)); // Bit at index 0 is set (1) + Assert.IsTrue(value.TestBit(1)); // Bit at index 1 is set (0) + Assert.IsFalse(value.TestBit(2)); // Bit at index 2 is not set (0) + Assert.IsFalse(value.TestBit(-1)); // Bit at index -1 is not set (0) + } + + [TestMethod] + public void TestBit_EdgeCases() + { + Assert.IsFalse(BigInteger.Zero.TestBit(0)); + Assert.IsFalse(BigInteger.Zero.TestBit(100)); + Assert.IsTrue(BigInteger.MinusOne.TestBit(0)); + Assert.IsTrue(BigInteger.MinusOne.TestBit(1000)); + Assert.IsTrue((BigInteger.One << 1000).TestBit(1000)); + Assert.IsFalse((BigInteger.One << 1000).TestBit(999)); + } + + [TestMethod] + public void TestSum() + { + var bigIntegers = new List { 1, 2, 3, 4 }; + var result = bigIntegers.Sum(); + Assert.AreEqual(10, result); + } + + [TestMethod] + public void TestSum_EdgeCases() + { + Assert.AreEqual(0, new List().Sum()); + Assert.AreEqual(0, new List { JNumber.MIN_SAFE_INTEGER, JNumber.MAX_SAFE_INTEGER }.Sum()); + Assert.AreEqual(JNumber.MAX_SAFE_INTEGER * 2, new List { JNumber.MAX_SAFE_INTEGER, JNumber.MAX_SAFE_INTEGER }.Sum()); + } + + [TestMethod] + public void TestSqrtTest() + { + Assert.ThrowsExactly(() => _ = BigInteger.MinusOne.Sqrt()); + + Assert.AreEqual(BigInteger.Zero, BigInteger.Zero.Sqrt()); + Assert.AreEqual(new BigInteger(1), new BigInteger(1).Sqrt()); + Assert.AreEqual(new BigInteger(1), new BigInteger(2).Sqrt()); + Assert.AreEqual(new BigInteger(1), new BigInteger(3).Sqrt()); + Assert.AreEqual(new BigInteger(2), new BigInteger(4).Sqrt()); + Assert.AreEqual(new BigInteger(9), new BigInteger(81).Sqrt()); + } + + private static byte[] GetRandomByteArray() + { + var byteValue = RandomNumberFactory.NextInt32(0, 32); + return RandomNumberFactory.NextBytes(byteValue); + } + + private static void VerifyGetBitLength(BigInteger value, long expected) + { + Assert.AreEqual(expected, value.GetBitLength(), "Native method has not the expected result"); + } + + [TestMethod] + public void TestGetBitLength() + { + // Big Number (net standard didn't work) + Assert.ThrowsExactly(() => VerifyGetBitLength(BigInteger.One << 32 << int.MaxValue, 2147483680)); + + // Trivial cases + // sign bit|shortest two's complement + // string w/o sign bit + VerifyGetBitLength(0, 0); // 0| + VerifyGetBitLength(1, 1); // 0|1 + VerifyGetBitLength(-1, 0); // 1| + VerifyGetBitLength(2, 2); // 0|10 + VerifyGetBitLength(-2, 1); // 1|0 + VerifyGetBitLength(3, 2); // 0|11 + VerifyGetBitLength(-3, 2); // 1|01 + VerifyGetBitLength(4, 3); // 0|100 + VerifyGetBitLength(-4, 2); // 1|00 + VerifyGetBitLength(5, 3); // 0|101 + VerifyGetBitLength(-5, 3); // 1|011 + VerifyGetBitLength(6, 3); // 0|110 + VerifyGetBitLength(-6, 3); // 1|010 + VerifyGetBitLength(7, 3); // 0|111 + VerifyGetBitLength(-7, 3); // 1|001 + VerifyGetBitLength(8, 4); // 0|1000 + VerifyGetBitLength(-8, 3); // 1|000 + } + + [TestMethod] + public void TestModInverseTest() + { + Assert.ThrowsExactly(() => _ = BigInteger.One.ModInverse(BigInteger.Zero)); + Assert.ThrowsExactly(() => _ = BigInteger.One.ModInverse(BigInteger.One)); + Assert.ThrowsExactly(() => _ = BigInteger.Zero.ModInverse(BigInteger.Zero)); + Assert.ThrowsExactly(() => _ = BigInteger.Zero.ModInverse(BigInteger.One)); + Assert.ThrowsExactly(() => _ = new BigInteger(ushort.MaxValue).ModInverse(byte.MaxValue)); + Assert.AreEqual(new BigInteger(52), new BigInteger(19).ModInverse(141)); + } +} diff --git a/tests/Neo.Extensions.Tests/UT_ByteArrayComparer.cs b/tests/Neo.Extensions.Tests/UT_ByteArrayComparer.cs new file mode 100644 index 0000000000..1cce0243bf --- /dev/null +++ b/tests/Neo.Extensions.Tests/UT_ByteArrayComparer.cs @@ -0,0 +1,81 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ByteArrayComparer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Extensions.Tests; + +[TestClass] +public class UT_ByteArrayComparer +{ + [TestMethod] + public void TestCompare() + { + ByteArrayComparer comparer = ByteArrayComparer.Default; + byte[]? x = null, y = null; + Assert.AreEqual(0, comparer.Compare(x, y)); + + x = [1, 2, 3, 4, 5]; + y = x; + Assert.AreEqual(0, comparer.Compare(x, y)); + Assert.AreEqual(0, comparer.Compare(x, x)); + + y = null; + Assert.IsGreaterThan(0, comparer.Compare(x, y)); + + y = x; + x = null; + Assert.IsLessThan(0, comparer.Compare(x, y)); + + x = [1]; + y = []; + Assert.IsGreaterThan(0, comparer.Compare(x, y)); + y = x; + Assert.AreEqual(0, comparer.Compare(x, y)); + + x = [1]; + y = [2]; + Assert.IsLessThan(0, comparer.Compare(x, y)); + + Assert.AreEqual(0, comparer.Compare(null, Array.Empty())); + Assert.AreEqual(0, comparer.Compare(Array.Empty(), null)); + + x = [1, 2, 3, 4, 5]; + y = [1, 2, 3]; + Assert.IsGreaterThan(0, comparer.Compare(x, y)); + + x = [1, 2, 3, 4, 5]; + y = [1, 2, 3, 4, 5, 6]; + Assert.IsLessThan(0, comparer.Compare(x, y)); + + // cases for reverse comparer + comparer = ByteArrayComparer.Reverse; + + x = [3]; + Assert.IsLessThan(0, comparer.Compare(x, y)); + + y = x; + Assert.AreEqual(0, comparer.Compare(x, y)); + + x = [1]; + y = [2]; + Assert.IsGreaterThan(0, comparer.Compare(x, y)); + + Assert.AreEqual(0, comparer.Compare(null, Array.Empty())); + Assert.AreEqual(0, comparer.Compare(Array.Empty(), null)); + + x = [1, 2, 3, 4, 5]; + y = [1, 2, 3]; + Assert.IsLessThan(0, comparer.Compare(x, y)); + + x = [1, 2, 3, 4, 5]; + y = [1, 2, 3, 4, 5, 6]; + Assert.IsGreaterThan(0, comparer.Compare(x, y)); + } +} diff --git a/tests/Neo.Extensions.Tests/UT_ByteArrayEqualityComparer.cs b/tests/Neo.Extensions.Tests/UT_ByteArrayEqualityComparer.cs new file mode 100644 index 0000000000..4793f3fc41 --- /dev/null +++ b/tests/Neo.Extensions.Tests/UT_ByteArrayEqualityComparer.cs @@ -0,0 +1,50 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_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. + +namespace Neo.Extensions.Tests; + +[TestClass] +public class UT_ByteArrayEqualityComparer +{ + [TestMethod] + public void TestEqual() + { + var a = new byte[] { 1, 2, 3, 4, 1, 2, 3, 4, 5 }; + var b = new byte[] { 1, 2, 3, 4, 1, 2, 3, 4, 5 }; + var check = ByteArrayEqualityComparer.Default; + + Assert.IsTrue(check.Equals(a, a)); + Assert.IsTrue(check.Equals(a, b)); + Assert.IsFalse(check.Equals(null, b)); + Assert.IsFalse(check.Equals(a, null)); + Assert.IsTrue(check.Equals(null, null)); + + Assert.IsFalse(check.Equals(a, new byte[] { 1, 2, 3 })); + Assert.IsTrue(check.Equals(Array.Empty(), Array.Empty())); + + b[8]++; + Assert.IsFalse(check.Equals(a, b)); + b[8]--; + b[0]--; + Assert.IsFalse(check.Equals(a, b)); + } + + [TestMethod] + public void TestGetHashCode() + { + var a = new byte[] { 1, 2, 3, 4, 1, 2, 3, 4, 5 }; + var b = new byte[] { 1, 2, 3, 4, 1, 2, 3, 4, 5 }; + var check = ByteArrayEqualityComparer.Default; + + Assert.AreEqual(check.GetHashCode(a), check.GetHashCode(b)); + Assert.AreNotEqual(check.GetHashCode(a), check.GetHashCode(b.Take(8).ToArray())); + } +} diff --git a/tests/Neo.Extensions.Tests/UT_ByteExtensions.cs b/tests/Neo.Extensions.Tests/UT_ByteExtensions.cs new file mode 100644 index 0000000000..d4b904df13 --- /dev/null +++ b/tests/Neo.Extensions.Tests/UT_ByteExtensions.cs @@ -0,0 +1,100 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ByteExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Hashing; +using System.Text; + +namespace Neo.Extensions.Tests; + +[TestClass] +public class UT_ByteExtensions +{ + [TestMethod] + public void TestToHexString() + { + byte[]? nullStr = null; + Assert.ThrowsExactly(() => _ = nullStr.ToHexString()); + Assert.ThrowsExactly(() => _ = nullStr.ToHexString(false)); + Assert.ThrowsExactly(() => _ = nullStr.ToHexString(true)); + + byte[] empty = Array.Empty(); + Assert.AreEqual("", empty.ToHexString()); + Assert.AreEqual("", empty.ToHexString(false)); + Assert.AreEqual("", empty.ToHexString(true)); + + byte[] str1 = [(byte)'n', (byte)'e', (byte)'o']; + Assert.AreEqual("6e656f", str1.ToHexString()); + Assert.AreEqual("6e656f", str1.ToHexString(false)); + Assert.AreEqual("6f656e", str1.ToHexString(true)); + } + + [TestMethod] + public void TestXxHash3() + { + byte[] data = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat("Hello, World!^_^", 16 * 1024))); + Assert.AreEqual(HashCode.Combine(XxHash3.HashToUInt64(data, 40343)), data.XxHash3_32()); + } + + [TestMethod] + public void TestReadOnlySpanToHexString() + { + byte[] input = { 0x0F, 0xA4, 0x3B }; + var span = new ReadOnlySpan(input); + string result = span.ToHexString(); + Assert.AreEqual("0fa43b", result); + + input = Array.Empty(); + span = new ReadOnlySpan(input); + result = span.ToHexString(); + Assert.AreEqual(0, result.Length); + + input = [0x5A]; + span = new ReadOnlySpan(input); + result = span.ToHexString(); + Assert.AreEqual("5a", result); + + input = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]; + span = new ReadOnlySpan(input); + result = span.ToHexString(); + Assert.AreEqual("0123456789abcdef", result); + } + + [TestMethod] + public void TestNotZero() + { + Assert.IsFalse(new ReadOnlySpan(Array.Empty()).NotZero()); + Assert.IsFalse(new ReadOnlySpan(new byte[4]).NotZero()); + Assert.IsFalse(new ReadOnlySpan(new byte[7]).NotZero()); + Assert.IsFalse(new ReadOnlySpan(new byte[8]).NotZero()); + Assert.IsFalse(new ReadOnlySpan(new byte[9]).NotZero()); + Assert.IsFalse(new ReadOnlySpan(new byte[11]).NotZero()); + + Assert.IsTrue(new ReadOnlySpan([0x00, 0x00, 0x00, 0x01]).NotZero()); + Assert.IsTrue(new ReadOnlySpan([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).NotZero()); + Assert.IsTrue(new ReadOnlySpan([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).NotZero()); + Assert.IsTrue(new ReadOnlySpan([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00]).NotZero()); + Assert.IsTrue(new ReadOnlySpan([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).NotZero()); + + var bytes = new byte[64]; + for (int i = 0; i < bytes.Length; i++) + { + ReadOnlySpan span = bytes.AsSpan(); + Assert.IsFalse(span[i..].NotZero()); + + for (int j = i; j < bytes.Length; j++) + { + bytes[j] = 0x01; + Assert.IsTrue(span[i..].NotZero()); + bytes[j] = 0x00; + } + } + } +} diff --git a/tests/Neo.Extensions.Tests/UT_DateTimeExtensions.cs b/tests/Neo.Extensions.Tests/UT_DateTimeExtensions.cs new file mode 100644 index 0000000000..179085b32a --- /dev/null +++ b/tests/Neo.Extensions.Tests/UT_DateTimeExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_DateTimeExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Extensions.Tests; + +[TestClass] +public class UT_DateTimeExtensions +{ + private static readonly DateTime unixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + [TestMethod] + public void TestToTimestamp() + { + var time = DateTime.UtcNow; + var expected = (uint)(time.ToUniversalTime() - unixEpoch).TotalSeconds; + var actual = time.ToTimestamp(); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void TestToTimestampMS() + { + var time = DateTime.UtcNow; + var expected = (ulong)(time.ToUniversalTime() - unixEpoch).TotalMilliseconds; + var actual = time.ToTimestampMS(); + + Assert.AreEqual(expected, actual); + } +} diff --git a/tests/Neo.Extensions.Tests/UT_IntegerExtensions.cs b/tests/Neo.Extensions.Tests/UT_IntegerExtensions.cs new file mode 100644 index 0000000000..109eb84ed7 --- /dev/null +++ b/tests/Neo.Extensions.Tests/UT_IntegerExtensions.cs @@ -0,0 +1,167 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_IntegerExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.Collections; + +namespace Neo.Extensions.Tests; + +[TestClass] +public class UT_IntegerExtensions +{ + [TestMethod] + public void TestGetVarSizeInt() + { + for (int i = 0; i < 4; i++) + { + if (i == 0) + { + int result = 1.GetVarSize(); + int old = OldGetVarSize(1); + Assert.AreEqual(1, result); + Assert.AreEqual(1, old); + } + else if (i == 1) + { + int result = ushort.MaxValue.GetVarSize(); + int old = OldGetVarSize(ushort.MaxValue); + Assert.AreEqual(3, result); + Assert.AreEqual(3, old); + } + else if (i == 2) + { + int result = uint.MaxValue.GetVarSize(); + int old = OldGetVarSize(int.MaxValue); + Assert.AreEqual(5, result); + Assert.AreEqual(5, old); + } + else + { + int result = long.MaxValue.GetVarSize(); + Assert.AreEqual(9, result); + } + } + } + + [TestMethod] + public void TestGetVarSizeGeneric() + { + for (int i = 0; i < 9; i++) + { + if (i == 0) + { + int result = new UInt160[] { UInt160.Zero }.GetVarSize(); + Assert.AreEqual(21, result); + } + else if (i == 1) // sbyte + { + List initList = [TestEnum0.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(2, result); + } + else if (i == 2) // byte + { + List initList = [TestEnum1.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(2, result); + } + else if (i == 3) // short + { + List initList = [TestEnum2.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(3, result); + } + else if (i == 4) // ushort + { + List initList = [TestEnum3.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(3, result); + } + else if (i == 5) // int + { + List initList = [TestEnum4.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(5, result); + } + else if (i == 6) // uint + { + List initList = [TestEnum5.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(5, result); + } + else if (i == 7) // long + { + List initList = [TestEnum6.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(9, result); + } + else if (i == 8) + { + List initList = [1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(5, result); + } + } + } + + enum TestEnum0 : sbyte + { + case1 = 1, case2 = 2 + } + + enum TestEnum1 : byte + { + case1 = 1, case2 = 2 + } + + enum TestEnum2 : short + { + case1 = 1, case2 = 2 + } + + enum TestEnum3 : ushort + { + case1 = 1, case2 = 2 + } + + enum TestEnum4 : int + { + case1 = 1, case2 = 2 + } + + enum TestEnum5 : uint + { + case1 = 1, case2 = 2 + } + + enum TestEnum6 : long + { + case1 = 1, case2 = 2 + } + + public static int OldGetVarSize(int value) + { + if (value < 0xFD) + return sizeof(byte); + else if (value <= ushort.MaxValue) + return sizeof(byte) + sizeof(ushort); + else + return sizeof(byte) + sizeof(uint); + } +} diff --git a/tests/Neo.Extensions.Tests/UT_Logs.cs b/tests/Neo.Extensions.Tests/UT_Logs.cs new file mode 100644 index 0000000000..b0ffddc194 --- /dev/null +++ b/tests/Neo.Extensions.Tests/UT_Logs.cs @@ -0,0 +1,45 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Logs.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Tests; + +[TestClass] +public class UT_Log +{ + [TestMethod] + public void TestGetLogger() + { + var logger = Logs.GetLogger("test"); + Assert.IsNotNull(logger); + logger.Information("test"); + + var logDir = Logs.LogDirectory; + Assert.IsNull(logDir); + + Logs.LogDirectory = Path.Combine(Environment.CurrentDirectory, "Logs"); + logger = Logs.GetLogger("test"); + Assert.IsNotNull(logger); + logger.Information("test"); + + var fileName = $"log-{DateTime.Now:yyyyMMdd}.txt"; + Assert.IsTrue(File.Exists(Path.Combine(Logs.LogDirectory, "test", fileName))); + Assert.ThrowsExactly(() => Logs.LogDirectory = "test"); + } + + [TestMethod] + public void TestConsoleLogger() + { + var logger = Logs.ConsoleLogger; + Assert.IsNotNull(logger); + + logger.Information("test"); + } +} diff --git a/tests/Neo.Extensions.Tests/UT_SecureStringExtensions.cs b/tests/Neo.Extensions.Tests/UT_SecureStringExtensions.cs new file mode 100644 index 0000000000..819504d0e9 --- /dev/null +++ b/tests/Neo.Extensions.Tests/UT_SecureStringExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_SecureStringExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Extensions.Tests; + +[TestClass] +public class UT_SecureStringExtensions +{ + [TestMethod] + public void Test_String_To_SecureString() + { + var expected = "Hello World"; + var expectedSecureString = expected.ToSecureString(); + + var actual = expectedSecureString.GetClearText(); + + Assert.IsTrue(expectedSecureString.IsReadOnly()); + Assert.AreEqual(expected, actual); + } +} diff --git a/tests/Neo.Extensions.Tests/UT_StringExtensions.cs b/tests/Neo.Extensions.Tests/UT_StringExtensions.cs new file mode 100644 index 0000000000..752892e82c --- /dev/null +++ b/tests/Neo.Extensions.Tests/UT_StringExtensions.cs @@ -0,0 +1,439 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_StringExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.Collections; +using System.Text; + +namespace Neo.Extensions.Tests; + +[TestClass] +public class UT_StringExtensions +{ + [TestMethod] + public void TestHexToBytes() + { + string? value = null; + Assert.AreEqual(Array.Empty().ToHexString(), value.HexToBytes().ToHexString()); + + string empty = ""; + Assert.AreEqual(Array.Empty().ToHexString(), empty.HexToBytes().ToHexString()); + + string str1 = "hab"; + Assert.ThrowsExactly(() => str1.HexToBytes()); + + string str2 = "0102"; + byte[] bytes = str2.HexToBytes(); + Assert.AreEqual(new byte[] { 0x01, 0x02 }.ToHexString(), bytes.ToHexString()); + + string str3 = "0A0b0C"; + bytes = str3.AsSpan().HexToBytes(); + Assert.AreEqual(new byte[] { 0x0A, 0x0B, 0x0C }.ToHexString(), bytes.ToHexString()); + + bytes = str3.AsSpan().HexToBytesReversed(); + Assert.AreEqual(new byte[] { 0x0C, 0x0B, 0x0A }.ToHexString(), bytes.ToHexString()); + } + + [TestMethod] + public void TestGetVarSizeString() + { + int result = "AA".GetVarSize(); + Assert.AreEqual(3, result); + } + + [TestMethod] + public void TestGetVarSizeInt() + { + for (int i = 0; i < 3; i++) + { + if (i == 0) + { + int result = 1.GetVarSize(); + Assert.AreEqual(1, result); + } + else if (i == 1) + { + int result = 0xFFFF.GetVarSize(); + Assert.AreEqual(3, result); + } + else + { + int result = 0xFFFFFF.GetVarSize(); + Assert.AreEqual(5, result); + } + } + } + + [TestMethod] + public void TestGetStrictUTF8String() + { + var bytes = new byte[] { (byte)'A', (byte)'B', (byte)'C' }; + Assert.AreEqual("ABC", bytes.ToStrictUtf8String()); + Assert.AreEqual("ABC", bytes.ToStrictUtf8String(0, 3)); + Assert.AreEqual("ABC", ((ReadOnlySpan)bytes.AsSpan()).ToStrictUtf8String()); + + Assert.AreEqual(bytes.ToHexString(), "ABC".ToStrictUtf8Bytes().ToHexString()); + Assert.AreEqual(bytes.Length, "ABC".GetStrictUtf8ByteCount()); + } + + [TestMethod] + public void TestIsHex() + { + Assert.IsTrue("010203".IsHex()); + Assert.IsFalse("01020".IsHex()); + Assert.IsFalse("01020g".IsHex()); + Assert.IsTrue("".IsHex()); + } + + [TestMethod] + public void TestTrimStartIgnoreCase() + { + Assert.AreEqual("010203", "0x010203".AsSpan().TrimStartIgnoreCase("0x").ToString()); + Assert.AreEqual("010203", "0x010203".AsSpan().TrimStartIgnoreCase("0X").ToString()); + Assert.AreEqual("010203", "0X010203".AsSpan().TrimStartIgnoreCase("0x").ToString()); + } + + [TestMethod] + public void TestGetVarSizeGeneric() + { + for (int i = 0; i < 9; i++) + { + if (i == 0) + { + int result = new UInt160[] { UInt160.Zero }.GetVarSize(); + Assert.AreEqual(21, result); + } + else if (i == 1)//sbyte + { + List initList = [TestEnum0.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(2, result); + } + else if (i == 2)//byte + { + List initList = [TestEnum1.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(2, result); + } + else if (i == 3)//short + { + List initList = [TestEnum2.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(3, result); + } + else if (i == 4)//ushort + { + List initList = [TestEnum3.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(3, result); + } + else if (i == 5)//int + { + List initList = [TestEnum4.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(5, result); + } + else if (i == 6)//uint + { + List initList = [TestEnum5.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(5, result); + } + else if (i == 7)//long + { + List initList = [TestEnum6.case1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(9, result); + } + else if (i == 8) + { + List initList = [1]; + IReadOnlyCollection testList = initList.AsReadOnly(); + int result = testList.GetVarSize(); + Assert.AreEqual(5, result); + } + } + } + + enum TestEnum0 : sbyte + { + case1 = 1, case2 = 2 + } + + enum TestEnum1 : byte + { + case1 = 1, case2 = 2 + } + + enum TestEnum2 : short + { + case1 = 1, case2 = 2 + } + + enum TestEnum3 : ushort + { + case1 = 1, case2 = 2 + } + + enum TestEnum4 : int + { + case1 = 1, case2 = 2 + } + + enum TestEnum5 : uint + { + case1 = 1, case2 = 2 + } + + enum TestEnum6 : long + { + case1 = 1, case2 = 2 + } + + #region Exception Message Tests + + [TestMethod] + public void TestToStrictUtf8String_ByteArray_WithInvalidBytes_ShouldThrowWithDetailedMessage() + { + // Test invalid UTF-8 bytes + byte[] invalidUtf8 = new byte[] { 0xFF, 0xFE, 0xFD }; + + var ex = Assert.ThrowsExactly(() => invalidUtf8.ToStrictUtf8String()); + + Assert.Contains("Failed to decode byte array to UTF-8 string (strict mode)", ex.Message); + Assert.Contains("invalid UTF-8 byte sequences", ex.Message); + Assert.Contains("FF-FE-FD", ex.Message); + Assert.Contains("Ensure all bytes form valid UTF-8 character sequences", ex.Message); + } + + [TestMethod] + public void TestToStrictUtf8String_ByteArray_WithNull_ShouldThrowWithParameterName() + { + byte[]? nullArray = null; + + var ex = Assert.ThrowsExactly(() => nullArray!.ToStrictUtf8String()); + + Assert.AreEqual("value", ex.ParamName); + Assert.Contains("Cannot decode null byte array to UTF-8 string", ex.Message); + } + + [TestMethod] + public void TestToStrictUtf8String_ByteArrayWithRange_WithInvalidParameters_ShouldThrowWithDetailedMessage() + { + byte[] validArray = new byte[] { 65, 66, 67 }; // "ABC" + + // Test negative start + var ex1 = Assert.ThrowsExactly(() => validArray.ToStrictUtf8String(-1, 2)); + Assert.AreEqual("start", ex1.ParamName); + Assert.Contains("Start index cannot be negative", ex1.Message); + + // Test negative count + var ex2 = Assert.ThrowsExactly(() => validArray.ToStrictUtf8String(0, -1)); + Assert.AreEqual("count", ex2.ParamName); + Assert.Contains("Count cannot be negative", ex2.Message); + + // Test range exceeds bounds + var ex3 = Assert.ThrowsExactly(() => validArray.ToStrictUtf8String(1, 5)); + Assert.AreEqual("count", ex3.ParamName); + Assert.Contains("exceeds the array bounds", ex3.Message); + Assert.Contains("length: 3", ex3.Message); + Assert.Contains("start + count <= array.Length", ex3.Message); + } + + [TestMethod] + public void TestToStrictUtf8String_ReadOnlySpan_WithInvalidBytes_ShouldThrowWithDetailedMessage() + { + // Test invalid UTF-8 bytes + byte[] invalidUtf8 = new byte[] { 0x80, 0x81, 0x82 }; + + var ex = Assert.ThrowsExactly(() => ((ReadOnlySpan)invalidUtf8).ToStrictUtf8String()); + + Assert.Contains("Failed to decode byte span to UTF-8 string (strict mode)", ex.Message); + Assert.Contains("invalid UTF-8 byte sequences", ex.Message); + Assert.Contains("0x80, 0x81, 0x82", ex.Message); + Assert.Contains("Ensure all bytes form valid UTF-8 character sequences", ex.Message); + } + + [TestMethod] + public void TestToStrictUtf8Bytes_WithNull_ShouldThrowWithParameterName() + { + string? nullString = null; + + var ex = Assert.ThrowsExactly(() => nullString!.ToStrictUtf8Bytes()); + + Assert.AreEqual("value", ex.ParamName); + Assert.Contains("Cannot encode null string to UTF-8 bytes", ex.Message); + } + + [TestMethod] + public void TestGetStrictUtf8ByteCount_WithNull_ShouldThrowWithParameterName() + { + string? nullString = null; + + var ex = Assert.ThrowsExactly(() => nullString!.GetStrictUtf8ByteCount()); + + Assert.AreEqual("value", ex.ParamName); + Assert.Contains("Cannot get UTF-8 byte count for null string", ex.Message); + } + + [TestMethod] + public void TestHexToBytes_String_WithInvalidLength_ShouldThrowWithDetailedMessage() + { + string invalidHex = "abc"; // Odd length + + var ex = Assert.ThrowsExactly(() => invalidHex.HexToBytes()); + + Assert.Contains("Failed to convert hex string to bytes", ex.Message); + Assert.Contains("invalid hexadecimal characters", ex.Message); + Assert.Contains("Input: 'abc'", ex.Message); + Assert.Contains("Valid hex characters are 0-9, A-F, and a-f", ex.Message); + } + + [TestMethod] + public void TestHexToBytes_String_WithInvalidCharacters_ShouldThrowWithDetailedMessage() + { + string invalidHex = "abgh"; // Contains 'g' and 'h' + + var ex = Assert.ThrowsExactly(() => invalidHex.HexToBytes()); + + Assert.Contains("Failed to convert hex string to bytes", ex.Message); + Assert.Contains("invalid hexadecimal characters", ex.Message); + Assert.Contains("Input: 'abgh'", ex.Message); + Assert.Contains("Valid hex characters are 0-9, A-F, and a-f", ex.Message); + } + + [TestMethod] + public void TestHexToBytes_ReadOnlySpan_WithInvalidCharacters_ShouldThrowWithDetailedMessage() + { + string invalidHex = "12xyz"; + + var ex = Assert.ThrowsExactly(() => invalidHex.AsSpan().HexToBytes()); + + Assert.Contains("Failed to convert hex span to bytes", ex.Message); + Assert.Contains("invalid hexadecimal characters", ex.Message); + Assert.Contains("Input: '12xyz'", ex.Message); + Assert.Contains("Valid hex characters are 0-9, A-F, and a-f", ex.Message); + } + + [TestMethod] + public void TestHexToBytesReversed_WithInvalidCharacters_ShouldThrowWithDetailedMessage() + { + string invalidHex = "12zz"; + + var ex = Assert.ThrowsExactly(() => invalidHex.AsSpan().HexToBytesReversed()); + + Assert.Contains("Failed to convert hex span to reversed bytes", ex.Message); + Assert.Contains("invalid hexadecimal characters", ex.Message); + Assert.Contains("Input: '12zz'", ex.Message); + Assert.Contains("Valid hex characters are 0-9, A-F, and a-f", ex.Message); + } + + [TestMethod] + public void TestGetVarSize_WithNull_ShouldThrowWithParameterName() + { + string? nullString = null; + + var ex = Assert.ThrowsExactly(() => nullString!.GetVarSize()); + + Assert.AreEqual("value", ex.ParamName); + Assert.Contains("Cannot calculate variable size for null string", ex.Message); + } + + [TestMethod] + public void TestExceptionMessages_WithLongInputs_ShouldTruncateAppropriately() + { + // Test long string truncation in exception messages + string longString = new('a', 150); + + var ex = Assert.ThrowsExactly(() => ((string?)null)!.GetStrictUtf8ByteCount()); + Assert.Contains("Cannot get UTF-8 byte count for null string", ex.Message); + + // Test long hex string + string longHexString = new('z', 120); // Invalid hex with 'z' + + var hexEx = Assert.ThrowsExactly(() => longHexString.HexToBytes()); + Assert.Contains("Input length: 120 characters", hexEx.Message); + } + + [TestMethod] + public void TestExceptionMessages_WithLargeByteArrays_ShouldShowLimitedBytes() + { + // Create a large byte array with some invalid UTF-8 sequences + byte[] largeInvalidUtf8 = new byte[100]; + for (int i = 0; i < 100; i++) + { + largeInvalidUtf8[i] = 0xFF; // Invalid UTF-8 + } + + var ex = Assert.ThrowsExactly(() => largeInvalidUtf8.ToStrictUtf8String()); + + Assert.Contains("Length: 100 bytes", ex.Message); + Assert.Contains("First 16:", ex.Message); + Assert.Contains("FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF", ex.Message); + } + + [TestMethod] + public void TestExceptionParameterNames_AreCorrect() + { + // Verify that all ArgumentException and ArgumentNullException have correct parameter names + + // ToStrictUtf8String with null byte array + var ex1 = Assert.ThrowsExactly(() => ((byte[]?)null)!.ToStrictUtf8String()); + Assert.AreEqual("value", ex1.ParamName); + + // ToStrictUtf8String with invalid range parameters + byte[] validArray = new byte[] { 65, 66, 67 }; + var ex2 = Assert.ThrowsExactly(() => validArray.ToStrictUtf8String(-1, 1)); + Assert.AreEqual("start", ex2.ParamName); + + var ex3 = Assert.ThrowsExactly(() => validArray.ToStrictUtf8String(0, -1)); + Assert.AreEqual("count", ex3.ParamName); + + // ToStrictUtf8Bytes with null string + var ex4 = Assert.ThrowsExactly(() => ((string?)null)!.ToStrictUtf8Bytes()); + Assert.AreEqual("value", ex4.ParamName); + + // GetStrictUtf8ByteCount with null string + var ex5 = Assert.ThrowsExactly(() => ((string?)null)!.GetStrictUtf8ByteCount()); + Assert.AreEqual("value", ex5.ParamName); + + // HexToBytes with invalid hex string + var ex6 = Assert.ThrowsExactly(() => "abc".HexToBytes()); + // FormatException doesn't have ParamName, so we just check the message + Assert.Contains("Failed to convert hex string to bytes", ex6.Message); + + // GetVarSize with null string + var ex7 = Assert.ThrowsExactly(() => ((string?)null)!.GetVarSize()); + Assert.AreEqual("value", ex7.ParamName); + } + + [TestMethod] + public void TestTryToStrictUtf8String_DoesNotThrowOnInvalidInput() + { + // Verify that TryToStrictUtf8String doesn't throw exceptions for invalid input + byte[] invalidUtf8 = new byte[] { 0xFF, 0xFE, 0xFD }; + ReadOnlySpan span = invalidUtf8; + + bool result = span.TryToStrictUtf8String(out string? value); + + Assert.IsFalse(result); + Assert.IsNull(value); + } + + #endregion +} diff --git a/tests/Neo.Json.UnitTests/Neo.Json.UnitTests.csproj b/tests/Neo.Json.UnitTests/Neo.Json.UnitTests.csproj new file mode 100644 index 0000000000..c12ecb9263 --- /dev/null +++ b/tests/Neo.Json.UnitTests/Neo.Json.UnitTests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/Neo.Json.UnitTests/UT_JArray.cs b/tests/Neo.Json.UnitTests/UT_JArray.cs new file mode 100644 index 0000000000..6e4ae6c91c --- /dev/null +++ b/tests/Neo.Json.UnitTests/UT_JArray.cs @@ -0,0 +1,429 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_JArray.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Collections; + +namespace Neo.Json.UnitTests; + +enum Foo +{ + male, + female +} + +[TestClass] +public class UT_JArray +{ + private JObject alice = null!; + private JObject bob = null!; + + [TestInitialize] + public void SetUp() + { + alice = new JObject() + { + ["name"] = "alice", + ["age"] = 30, + ["score"] = 100.001, + ["gender"] = Foo.female, + ["isMarried"] = true, + }; + + var pet1 = new JObject() + { + ["name"] = "Tom", + ["type"] = "cat", + }; + alice["pet"] = pet1; + + bob = new JObject() + { + ["name"] = "bob", + ["age"] = 100000, + ["score"] = 0.001, + ["gender"] = Foo.male, + ["isMarried"] = false, + }; + + var pet2 = new JObject() + { + ["name"] = "Paul", + ["type"] = "dog", + }; + bob["pet"] = pet2; + } + + [TestMethod] + public void TestAdd() + { + var jArray = new JArray + { + alice, + bob + }; + var jAlice = jArray[0]!; + var jBob = jArray[1]!; + Assert.AreEqual(alice["name"]!.ToString(), jAlice["name"]!.ToString()); + Assert.AreEqual(alice["age"]!.ToString(), jAlice["age"]!.ToString()); + Assert.AreEqual(alice["score"]!.ToString(), jAlice["score"]!.ToString()); + Assert.AreEqual(alice["gender"]!.ToString(), jAlice["gender"]!.ToString()); + Assert.AreEqual(alice["isMarried"]!.ToString(), jAlice["isMarried"]!.ToString()); + Assert.AreEqual(alice["pet"]!.ToString(), jAlice["pet"]!.ToString()); + Assert.AreEqual(bob["name"]!.ToString(), jBob["name"]!.ToString()); + Assert.AreEqual(bob["age"]!.ToString(), jBob["age"]!.ToString()); + Assert.AreEqual(bob["score"]!.ToString(), jBob["score"]!.ToString()); + Assert.AreEqual(bob["gender"]!.ToString(), jBob["gender"]!.ToString()); + Assert.AreEqual(bob["isMarried"]!.ToString(), jBob["isMarried"]!.ToString()); + Assert.AreEqual(bob["pet"]!.ToString(), jBob["pet"]!.ToString()); + } + + [TestMethod] + public void TestSetItem() + { + var jArray = new JArray + { + alice + }; + jArray[0] = bob; + Assert.AreEqual(jArray[0], bob); + + Assert.ThrowsExactly(() => jArray[1] = alice); + } + + [TestMethod] + public void TestClear() + { + var jArray = new JArray + { + alice + }; + var jAlice = jArray[0]!; + Assert.AreEqual(alice["name"]!.ToString(), jAlice["name"]!.ToString()); + Assert.AreEqual(alice["age"]!.ToString(), jAlice["age"]!.ToString()); + Assert.AreEqual(alice["score"]!.ToString(), jAlice["score"]!.ToString()); + Assert.AreEqual(alice["gender"]!.ToString(), jAlice["gender"]!.ToString()); + Assert.AreEqual(alice["isMarried"]!.ToString(), jAlice["isMarried"]!.ToString()); + Assert.AreEqual(alice["pet"]!.ToString(), jAlice["pet"]!.ToString()); + + jArray.Clear(); + Assert.ThrowsExactly(() => jArray[0]); + } + + [TestMethod] + public void TestContains() + { + var jArray = new JArray + { + alice + }; + Assert.Contains(alice, jArray); + Assert.DoesNotContain(bob, jArray); + } + + [TestMethod] + public void TestCopyTo() + { + var jArray = new JArray + { + alice, + bob + }; + + JObject[] jObjects1 = new JObject[2]; + jArray.CopyTo(jObjects1, 0); + var jAlice1 = jObjects1[0]; + var jBob1 = jObjects1[1]; + Assert.AreEqual(alice, jAlice1); + Assert.AreEqual(bob, jBob1); + + JObject[] jObjects2 = new JObject[4]; + jArray.CopyTo(jObjects2, 2); + var jAlice2 = jObjects2[2]; + var jBob2 = jObjects2[3]; + Assert.IsNull(jObjects2[0]); + Assert.IsNull(jObjects2[1]); + Assert.AreEqual(alice, jAlice2); + Assert.AreEqual(bob, jBob2); + } + + [TestMethod] + public void TestInsert() + { + var jArray = new JArray + { + alice, + alice, + alice, + alice + }; + + jArray.Insert(1, bob); + Assert.AreEqual(5, jArray.Count); + Assert.AreEqual(alice, jArray[0]); + Assert.AreEqual(bob, jArray[1]); + Assert.AreEqual(alice, jArray[2]); + + jArray.Insert(5, bob); + Assert.AreEqual(6, jArray.Count); + Assert.AreEqual(bob, jArray[5]); + } + + [TestMethod] + public void TestIndexOf() + { + var jArray = new JArray(); + Assert.AreEqual(-1, jArray.IndexOf(alice)); + + jArray.Add(alice); + jArray.Add(alice); + jArray.Add(alice); + jArray.Add(alice); + Assert.AreEqual(0, jArray.IndexOf(alice)); + + jArray.Insert(1, bob); + Assert.AreEqual(1, jArray.IndexOf(bob)); + } + + [TestMethod] + public void TestIsReadOnly() + { + var jArray = new JArray(); + Assert.IsFalse(jArray.IsReadOnly); + } + + [TestMethod] + public void TestRemove() + { + var jArray = new JArray + { + alice + }; + Assert.AreEqual(1, jArray.Count); + jArray.Remove(alice); + Assert.AreEqual(0, jArray.Count); + + jArray.Add(alice); + jArray.Add(alice); + Assert.AreEqual(2, jArray.Count); + jArray.Remove(alice); + Assert.AreEqual(1, jArray.Count); + } + + [TestMethod] + public void TestRemoveAt() + { + var jArray = new JArray + { + alice, + bob, + alice + }; + jArray.RemoveAt(1); + Assert.AreEqual(2, jArray.Count); + Assert.DoesNotContain(bob, jArray); + } + + [TestMethod] + public void TestGetEnumerator() + { + var jArray = new JArray + { + alice, + bob, + alice, + bob + }; + int i = 0; + foreach (var item in jArray) + { + if (i % 2 == 0) Assert.AreEqual(alice, item); + if (i % 2 != 0) Assert.AreEqual(bob, item); + i++; + } + Assert.IsNotNull(((IEnumerable)jArray).GetEnumerator()); + } + + [TestMethod] + public void TestAsString() + { + var jArray = new JArray + { + alice, + bob, + }; + var s = jArray.AsString(); + Assert.AreEqual("[{\"name\":\"alice\",\"age\":30,\"score\":100.001,\"gender\":\"female\",\"isMarried\":true,\"pet\":{\"name\":\"Tom\",\"type\":\"cat\"}},{\"name\":\"bob\",\"age\":100000,\"score\":0.001,\"gender\":\"male\",\"isMarried\":false,\"pet\":{\"name\":\"Paul\",\"type\":\"dog\"}}]", s); + } + + [TestMethod] + public void TestCount() + { + var jArray = new JArray { alice, bob }; + Assert.HasCount(2, jArray); + } + + [TestMethod] + public void TestInvalidIndexAccess() + { + var jArray = new JArray { alice }; + Assert.ThrowsExactly(() => jArray[1]); + } + + [TestMethod] + public void TestEmptyEnumeration() + { + var jArray = new JArray(); + foreach (var _ in jArray) + { + Assert.Fail("Enumeration should not occur on an empty JArray"); + } + } + + [TestMethod] + public void TestImplicitConversionFromJTokenArray() + { + JToken[] jTokens = { alice, bob }; + JArray jArray = jTokens; + + Assert.HasCount(2, jArray); + Assert.AreEqual(alice, jArray[0]); + Assert.AreEqual(bob, jArray[1]); + } + + [TestMethod] + public void TestAddNullValues() + { + var jArray = new JArray + { + null + }; + Assert.HasCount(1, jArray); + Assert.IsNull(jArray[0]); + } + + [TestMethod] + public void TestClone() + { + var jArray = new JArray { alice, bob }; + var clone = (JArray)jArray.Clone(); + + Assert.AreNotSame(jArray, clone); + Assert.AreEqual(jArray.Count, clone.Count); + + for (int i = 0; i < jArray.Count; i++) + { + Assert.AreEqual(jArray[i]?.AsString(), clone[i]?.AsString()); + } + + var a = jArray.AsString(); + var b = jArray.Clone().AsString(); + Assert.AreEqual(a, b); + } + + [TestMethod] + public void TestReadOnlyBehavior() + { + var jArray = new JArray(); + Assert.IsFalse(jArray.IsReadOnly); + } + + [TestMethod] + public void TestAddNull() + { + var jArray = new JArray { null }; + + Assert.HasCount(1, jArray); + Assert.IsNull(jArray[0]); + } + + [TestMethod] + public void TestSetNull() + { + var jArray = new JArray { alice }; + jArray[0] = null; + + Assert.HasCount(1, jArray); + Assert.IsNull(jArray[0]); + } + + [TestMethod] + public void TestInsertNull() + { + var jArray = new JArray { alice }; + jArray.Insert(0, null); + + Assert.HasCount(2, jArray); + Assert.IsNull(jArray[0]); + Assert.AreEqual(alice, jArray[1]); + } + + [TestMethod] + public void TestRemoveNull() + { + var jArray = new JArray { null, alice }; + jArray.Remove(null); + + Assert.HasCount(1, jArray); + Assert.AreEqual(alice, jArray[0]); + } + + [TestMethod] + public void TestContainsNull() + { + var jArray = new JArray { null, alice }; + Assert.Contains((JToken?)null, jArray); + Assert.DoesNotContain(bob, jArray); + } + + [TestMethod] + public void TestIndexOfNull() + { + var jArray = new JArray { null, alice }; + Assert.AreEqual(0, jArray.IndexOf(null)); + Assert.AreEqual(1, jArray.IndexOf(alice)); + } + + [TestMethod] + public void TestCopyToWithNull() + { + var jArray = new JArray { null, alice }; + JObject[] jObjects = new JObject[2]; + jArray.CopyTo(jObjects, 0); + + Assert.IsNull(jObjects[0]); + Assert.AreEqual(alice, jObjects[1]); + } + + [TestMethod] + public void TestToStringWithNull() + { + var jArray = new JArray { null, alice, bob }; + var jsonString = jArray.ToString(); + var asString = jArray.AsString(); + // JSON string should properly represent the null value + Assert.AreEqual("[null,{\"name\":\"alice\",\"age\":30,\"score\":100.001,\"gender\":\"female\",\"isMarried\":true,\"pet\":{\"name\":\"Tom\",\"type\":\"cat\"}},{\"name\":\"bob\",\"age\":100000,\"score\":0.001,\"gender\":\"male\",\"isMarried\":false,\"pet\":{\"name\":\"Paul\",\"type\":\"dog\"}}]", jsonString); + Assert.AreEqual("[null,{\"name\":\"alice\",\"age\":30,\"score\":100.001,\"gender\":\"female\",\"isMarried\":true,\"pet\":{\"name\":\"Tom\",\"type\":\"cat\"}},{\"name\":\"bob\",\"age\":100000,\"score\":0.001,\"gender\":\"male\",\"isMarried\":false,\"pet\":{\"name\":\"Paul\",\"type\":\"dog\"}}]", asString); + } + + [TestMethod] + public void TestFromStringWithNull() + { + var jsonString = "[null,{\"name\":\"alice\",\"age\":30,\"score\":100.001,\"gender\":\"female\",\"isMarried\":true,\"pet\":{\"name\":\"Tom\",\"type\":\"cat\"}},{\"name\":\"bob\",\"age\":100000,\"score\":0.001,\"gender\":\"male\",\"isMarried\":false,\"pet\":{\"name\":\"Paul\",\"type\":\"dog\"}}]"; + var jArray = (JArray)JToken.Parse(jsonString)!; + + Assert.HasCount(3, jArray); + Assert.IsNull(jArray[0]); + + // Checking the second and third elements + Assert.AreEqual("alice", jArray[1]!["name"]!.AsString()); + Assert.AreEqual("bob", jArray[2]!["name"]!.AsString()); + } +} diff --git a/tests/Neo.Json.UnitTests/UT_JBoolean.cs b/tests/Neo.Json.UnitTests/UT_JBoolean.cs new file mode 100644 index 0000000000..4015f53bb2 --- /dev/null +++ b/tests/Neo.Json.UnitTests/UT_JBoolean.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_JBoolean.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Newtonsoft.Json; + +namespace Neo.Json.UnitTests; + +[TestClass] +public class UT_JBoolean +{ + private JBoolean jFalse = null!; + private JBoolean jTrue = null!; + + [TestInitialize] + public void SetUp() + { + jFalse = new JBoolean(); + jTrue = new JBoolean(true); + } + + [TestMethod] + public void TestAsNumber() + { + Assert.AreEqual(0, jFalse.AsNumber()); + Assert.AreEqual(1, jTrue.AsNumber()); + } + + [TestMethod] + public void TestDefaultConstructor() + { + var defaultJBoolean = new JBoolean(); + Assert.AreEqual(0, defaultJBoolean.AsNumber()); + } + + [TestMethod] + public void TestExplicitFalse() + { + var explicitFalse = new JBoolean(false); + Assert.AreEqual(0, explicitFalse.AsNumber()); + } + + [TestMethod] + public void TestConversionToOtherTypes() + { + Assert.AreEqual("true", jTrue.ToString()); + Assert.AreEqual("false", jFalse.ToString()); + } + + [TestMethod] + public void TestComparisonsWithOtherBooleans() + { + Assert.IsTrue(jTrue.Equals(new JBoolean(true))); + Assert.IsTrue(jFalse.Equals(new JBoolean())); + } + + [TestMethod] + public void TestSerializationAndDeserialization() + { + string serialized = JsonConvert.SerializeObject(jTrue); + var deserialized = JsonConvert.DeserializeObject(serialized); + Assert.AreEqual(jTrue, deserialized); + } + + [TestMethod] + public void TestEqual() + { + Assert.IsTrue(jTrue.Equals(new JBoolean(true))); + Assert.IsTrue(jTrue == new JBoolean(true)); + Assert.IsTrue(jTrue != new JBoolean(false)); + Assert.IsTrue(jFalse.Equals(new JBoolean())); + Assert.IsTrue(jFalse == new JBoolean()); + Assert.AreEqual(jFalse.ToString(), jFalse.GetBoolean().ToString().ToLowerInvariant()); + } +} diff --git a/tests/Neo.Json.UnitTests/UT_JNumber.cs b/tests/Neo.Json.UnitTests/UT_JNumber.cs new file mode 100644 index 0000000000..565605b1c4 --- /dev/null +++ b/tests/Neo.Json.UnitTests/UT_JNumber.cs @@ -0,0 +1,93 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_JNumber.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Json.UnitTests; + +enum Woo +{ + Tom, + Jerry, + James +} + +[TestClass] +public class UT_JNumber +{ + private JNumber maxInt = null!; + private JNumber minInt = null!; + private JNumber zero = null!; + + [TestInitialize] + public void SetUp() + { + maxInt = new JNumber(JNumber.MAX_SAFE_INTEGER); + minInt = new JNumber(JNumber.MIN_SAFE_INTEGER); + zero = new JNumber(); + } + + [TestMethod] + public void TestAsBoolean() + { + Assert.IsTrue(maxInt.AsBoolean()); + Assert.IsFalse(zero.AsBoolean()); + } + + [TestMethod] + public void TestAsString() + { + Assert.ThrowsExactly(() => new JNumber(double.PositiveInfinity).AsString()); + Assert.ThrowsExactly(() => new JNumber(double.NegativeInfinity).AsString()); + Assert.ThrowsExactly(() => new JNumber(double.NaN).AsString()); + } + + [TestMethod] + public void TestGetEnum() + { + Assert.AreEqual(Woo.Tom, zero.GetEnum()); + Assert.AreEqual(Woo.Jerry, new JNumber(1).GetEnum()); + Assert.AreEqual(Woo.James, new JNumber(2).GetEnum()); + Assert.AreEqual(Woo.Tom, new JNumber(3).AsEnum()); + Assert.ThrowsExactly(() => new JNumber(3).GetEnum()); + } + + [TestMethod] + public void TestEqual() + { + Assert.IsTrue(maxInt.Equals(JNumber.MAX_SAFE_INTEGER)); + Assert.IsTrue(maxInt == JNumber.MAX_SAFE_INTEGER); + Assert.IsTrue(minInt.Equals(JNumber.MIN_SAFE_INTEGER)); + Assert.IsTrue(minInt == JNumber.MIN_SAFE_INTEGER); + Assert.IsTrue(zero == new JNumber()); + Assert.IsFalse(zero != new JNumber()); + Assert.AreEqual(zero.GetNumber(), zero.AsNumber()); + Assert.IsFalse(zero == null); + + var jnum = new JNumber(1); + Assert.IsTrue(jnum.Equals(new JNumber(1))); + Assert.IsTrue(jnum.Equals((uint)1)); + Assert.IsTrue(jnum.Equals((int)1)); + Assert.IsTrue(jnum.Equals((ulong)1)); + Assert.IsTrue(jnum.Equals((long)1)); + Assert.IsTrue(jnum.Equals((byte)1)); + Assert.IsTrue(jnum.Equals((sbyte)1)); + Assert.IsTrue(jnum.Equals((short)1)); + Assert.IsTrue(jnum.Equals((ushort)1)); + Assert.IsTrue(jnum.Equals((decimal)1)); + Assert.IsTrue(jnum.Equals((float)1)); + Assert.IsTrue(jnum.Equals((double)1)); + Assert.IsFalse(jnum.Equals(null)); + var x = jnum; + Assert.IsTrue(jnum.Equals(x)); + Assert.ThrowsExactly(() => _ = jnum.Equals(new BigInteger(1))); + } +} diff --git a/tests/Neo.Json.UnitTests/UT_JObject.cs b/tests/Neo.Json.UnitTests/UT_JObject.cs new file mode 100644 index 0000000000..9ec561f3a5 --- /dev/null +++ b/tests/Neo.Json.UnitTests/UT_JObject.cs @@ -0,0 +1,140 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_JObject.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Json.UnitTests; + +[TestClass] +public class UT_JObject +{ + private JObject _alice = null!; + private JObject _bob = null!; + + [TestInitialize] + public void SetUp() + { + _alice = new JObject() + { + ["name"] = "alice", + ["age"] = 30, + ["score"] = 100.001, + ["gender"] = Foo.female, + ["isMarried"] = true, + }; + + var pet1 = new JObject(new Dictionary() + { + ["name"] = "Tom", + ["type"] = "cat", + }); + _alice["pet"] = pet1; + _bob = new JObject() + { + ["name"] = "bob", + ["age"] = 100000, + ["score"] = 0.001, + ["gender"] = Foo.male, + ["isMarried"] = false, + }; + var pet2 = new JObject() + { + ["name"] = "Paul", + ["type"] = "dog", + }; + _bob["pet"] = pet2; + } + + [TestMethod] + public void TestAsBoolean() + { + Assert.IsTrue(_alice.AsBoolean()); + } + + [TestMethod] + public void TestAsNumber() + { + Assert.AreEqual(double.NaN, _alice.AsNumber()); + } + + [TestMethod] + public void TestParse() + { + Assert.ThrowsExactly(() => _ = JToken.Parse("", -1)); + Assert.ThrowsExactly(() => _ = JToken.Parse("aaa")); + Assert.ThrowsExactly(() => _ = JToken.Parse("hello world")); + Assert.ThrowsExactly(() => _ = JToken.Parse("100.a")); + Assert.ThrowsExactly(() => _ = JToken.Parse("100.+")); + Assert.ThrowsExactly(() => _ = JToken.Parse("\"\\s\"")); + Assert.ThrowsExactly(() => _ = JToken.Parse("\"a")); + Assert.ThrowsExactly(() => _ = JToken.Parse("{\"k1\":\"v1\",\"k1\":\"v2\"}")); + Assert.ThrowsExactly(() => _ = JToken.Parse("{\"k1\",\"k1\"}")); + Assert.ThrowsExactly(() => _ = JToken.Parse("{\"k1\":\"v1\"")); + Assert.ThrowsExactly(() => _ = JToken.Parse(new byte[] { 0x22, 0x01, 0x22 })); + Assert.ThrowsExactly(() => _ = JToken.Parse("{\"color\":\"red\",\"\\uDBFF\\u0DFFF\":\"#f00\"}")); + Assert.ThrowsExactly(() => _ = JToken.Parse("{\"color\":\"\\uDBFF\\u0DFFF\"}")); + Assert.ThrowsExactly(() => _ = JToken.Parse("\"\\uDBFF\\u0DFFF\"")); + + Assert.IsNull(JToken.Parse("null")); + Assert.IsTrue(JToken.Parse("true")!.AsBoolean()); + Assert.IsFalse(JToken.Parse("false")!.AsBoolean()); + Assert.AreEqual("hello world", JToken.Parse("\"hello world\"")!.AsString()); + Assert.AreEqual("\"\\/\b\f\n\r\t", JToken.Parse("\"\\\"\\\\\\/\\b\\f\\n\\r\\t\"")!.AsString()); + Assert.AreEqual("0", JToken.Parse("\"\\u0030\"")!.AsString()); + Assert.AreEqual("{\"k1\":\"v1\"}", JToken.Parse("{\"k1\":\"v1\"}", 100)!.ToString()); + } + + [TestMethod] + public void TestGetEnum() + { + Assert.AreEqual(Woo.Tom, _alice.AsEnum()); + Assert.ThrowsExactly(() => _alice.GetEnum()); + } + + [TestMethod] + public void TestOpImplicitEnum() + { + JToken obj = Woo.Tom; + Assert.AreEqual("Tom", obj.AsString()); + } + + [TestMethod] + public void TestOpImplicitString() + { + JToken? obj = null; + Assert.IsNull(obj); + + obj = "{\"aaa\":\"111\"}"; + Assert.AreEqual("{\"aaa\":\"111\"}", obj.AsString()); + } + + [TestMethod] + public void TestClone() + { + var bobClone = (JObject)_bob.Clone(); + Assert.AreNotSame(_bob, bobClone); + foreach (var key in bobClone.Properties.Keys) + { + switch (_bob[key]) + { + case JToken.Null: + Assert.IsNull(bobClone[key]); + break; + case JObject obj: + CollectionAssert.AreEqual( + obj.Properties.ToList(), + ((JObject)bobClone[key]!).Properties.ToList()); + break; + default: + Assert.AreEqual(_bob[key], bobClone[key]); + break; + } + } + } +} diff --git a/tests/Neo.Json.UnitTests/UT_JPath.cs b/tests/Neo.Json.UnitTests/UT_JPath.cs new file mode 100644 index 0000000000..e1ad1f2f82 --- /dev/null +++ b/tests/Neo.Json.UnitTests/UT_JPath.cs @@ -0,0 +1,172 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_JPath.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://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.Json.UnitTests; + +[TestClass] +public class UT_JPath +{ + private static readonly JObject json = new() + { + ["store"] = new JObject + { + ["book"] = new JArray + { + new JObject + { + ["category"] = "reference", + ["author"] = "Nigel Rees", + ["title"] = "Sayings of the Century", + ["price"] = 8.95 + }, + new JObject + { + ["category"] = "fiction", + ["author"] = "Evelyn Waugh", + ["title"] = "Sword of Honour", + ["price"] = 12.99 + }, + new JObject + { + ["category"] = "fiction", + ["author"] = "Herman Melville", + ["title"] = "Moby Dick", + ["isbn"] = "0-553-21311-3", + ["price"] = 8.99 + }, + new JObject + { + ["category"] = "fiction", + ["author"] = "J. R. R. Tolkien", + ["title"] = "The Lord of the Rings", + ["isbn"] = "0-395-19395-8", + ["price"] = null + } + }, + ["bicycle"] = new JObject + { + ["color"] = "red", + ["price"] = 19.95 + } + }, + ["expensive"] = 10, + ["data"] = null, + }; + + [TestMethod] + public void TestOOM() + { + var filter = "$" + string.Concat(Enumerable.Repeat("[0" + string.Concat(Enumerable.Repeat(",0", 64)) + "]", 6)); + Assert.ThrowsExactly(() => _ = JToken.Parse("[[[[[[{}]]]]]]")!.JsonPath(filter)); + } + + [TestMethod] + public void TestJsonPath() + { + Assert.AreEqual(@"[""Nigel Rees"",""Evelyn Waugh"",""Herman Melville"",""J. R. R. Tolkien""]", json.JsonPath("$.store.book[*].author").ToString()); + Assert.AreEqual(@"[""Nigel Rees"",""Evelyn Waugh"",""Herman Melville"",""J. R. R. Tolkien""]", json.JsonPath("$..author").ToString()); + Assert.AreEqual(@"[[{""category"":""reference"",""author"":""Nigel Rees"",""title"":""Sayings of the Century"",""price"":8.95},{""category"":""fiction"",""author"":""Evelyn Waugh"",""title"":""Sword of Honour"",""price"":12.99},{""category"":""fiction"",""author"":""Herman Melville"",""title"":""Moby Dick"",""isbn"":""0-553-21311-3"",""price"":8.99},{""category"":""fiction"",""author"":""J. R. R. Tolkien"",""title"":""The Lord of the Rings"",""isbn"":""0-395-19395-8"",""price"":null}],{""color"":""red"",""price"":19.95}]", json.JsonPath("$.store.*").ToString()); + Assert.AreEqual(@"[19.95,8.95,12.99,8.99,null]", json.JsonPath("$.store..price").ToString()); + Assert.AreEqual(@"[{""category"":""fiction"",""author"":""Herman Melville"",""title"":""Moby Dick"",""isbn"":""0-553-21311-3"",""price"":8.99}]", json.JsonPath("$..book[2]").ToString()); + Assert.AreEqual(@"[{""category"":""fiction"",""author"":""Herman Melville"",""title"":""Moby Dick"",""isbn"":""0-553-21311-3"",""price"":8.99}]", json.JsonPath("$..book[-2]").ToString()); + Assert.AreEqual(@"[{""category"":""reference"",""author"":""Nigel Rees"",""title"":""Sayings of the Century"",""price"":8.95},{""category"":""fiction"",""author"":""Evelyn Waugh"",""title"":""Sword of Honour"",""price"":12.99}]", json.JsonPath("$..book[0,1]").ToString()); + Assert.AreEqual(@"[{""category"":""reference"",""author"":""Nigel Rees"",""title"":""Sayings of the Century"",""price"":8.95},{""category"":""fiction"",""author"":""Evelyn Waugh"",""title"":""Sword of Honour"",""price"":12.99}]", json.JsonPath("$..book[:2]").ToString()); + Assert.AreEqual(@"[{""category"":""fiction"",""author"":""Evelyn Waugh"",""title"":""Sword of Honour"",""price"":12.99}]", json.JsonPath("$..book[1:2]").ToString()); + Assert.AreEqual(@"[{""category"":""fiction"",""author"":""Herman Melville"",""title"":""Moby Dick"",""isbn"":""0-553-21311-3"",""price"":8.99},{""category"":""fiction"",""author"":""J. R. R. Tolkien"",""title"":""The Lord of the Rings"",""isbn"":""0-395-19395-8"",""price"":null}]", json.JsonPath("$..book[-2:]").ToString()); + Assert.AreEqual(@"[{""category"":""fiction"",""author"":""Herman Melville"",""title"":""Moby Dick"",""isbn"":""0-553-21311-3"",""price"":8.99},{""category"":""fiction"",""author"":""J. R. R. Tolkien"",""title"":""The Lord of the Rings"",""isbn"":""0-395-19395-8"",""price"":null}]", json.JsonPath("$..book[2:]").ToString()); + Assert.AreEqual(@"[{""store"":{""book"":[{""category"":""reference"",""author"":""Nigel Rees"",""title"":""Sayings of the Century"",""price"":8.95},{""category"":""fiction"",""author"":""Evelyn Waugh"",""title"":""Sword of Honour"",""price"":12.99},{""category"":""fiction"",""author"":""Herman Melville"",""title"":""Moby Dick"",""isbn"":""0-553-21311-3"",""price"":8.99},{""category"":""fiction"",""author"":""J. R. R. Tolkien"",""title"":""The Lord of the Rings"",""isbn"":""0-395-19395-8"",""price"":null}],""bicycle"":{""color"":""red"",""price"":19.95}},""expensive"":10,""data"":null}]", json.JsonPath("").ToString()); + Assert.AreEqual(@"[{""book"":[{""category"":""reference"",""author"":""Nigel Rees"",""title"":""Sayings of the Century"",""price"":8.95},{""category"":""fiction"",""author"":""Evelyn Waugh"",""title"":""Sword of Honour"",""price"":12.99},{""category"":""fiction"",""author"":""Herman Melville"",""title"":""Moby Dick"",""isbn"":""0-553-21311-3"",""price"":8.99},{""category"":""fiction"",""author"":""J. R. R. Tolkien"",""title"":""The Lord of the Rings"",""isbn"":""0-395-19395-8"",""price"":null}],""bicycle"":{""color"":""red"",""price"":19.95}},10,null]", json.JsonPath("$.*").ToString()); + Assert.AreEqual(@"[]", json.JsonPath("$..invalidfield").ToString()); + } + + [TestMethod] + public void TestMaxDepth() + { + Assert.ThrowsExactly(() => _ = json.JsonPath("$..book[*].author")); + } + + [TestMethod] + public void TestInvalidFormat() + { + Assert.ThrowsExactly(() => _ = json.JsonPath("$..*")); + Assert.ThrowsExactly(() => _ = json.JsonPath("..book")); + Assert.ThrowsExactly(() => _ = json.JsonPath("$..")); + + // Test with an empty JSON Path + // Assert.ThrowsException(() => json.JsonPath("")); + + // Test with only special characters + Assert.ThrowsExactly(() => _ = json.JsonPath("@#$%^&*()")); + + // Test with unmatched brackets + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book[")); + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book)]")); + + // Test with invalid operators + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book=>2")); + + // Test with incorrect field syntax + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.'book'")); + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.[book]")); + + // Test with unexpected end of expression + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book[?(@.price<")); + + // Test with invalid array indexing + // Assert.ThrowsException(() => json.JsonPath("$.store.book['one']")); + // Assert.ThrowsException(() => json.JsonPath("$.store.book[999]")); + + // Test with invalid recursive descent + Assert.ThrowsExactly(() => _ = json.JsonPath("$..*..author")); + + // Test with nonexistent functions + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book.length()")); + + // Test with incorrect use of wildcards + // Assert.ThrowsException(() => json.JsonPath("$.*.store")); + + // Test with improper use of filters + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book[?(@.price)]")); + + // Test with mixing of valid and invalid syntax + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book[*],$.invalid")); + + // Test with invalid escape sequences + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book[\\]")); + + // Test with incorrect property access + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.'b?ook'")); + + // Test with invalid use of wildcard in array index + // Assert.ThrowsException(() => json.JsonPath("$.store.book[*]")); + + // Test with missing operators in filter expressions + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book[?(@.price)]")); + + // Test with incorrect boolean logic in filters + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book[?(@.price AND @.title)]")); + + // Test with nested filters without proper closure + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book[?(@.price[?(@ < 10)])]")); + + // Test with misplaced recursive descent operator + // Assert.ThrowsException(() => json.JsonPath("$..store..book")); + + // Test with using JSONPath reserved keywords incorrectly + Assert.ThrowsExactly(() => _ = json.JsonPath("$..@.book")); + + // Test with incorrect combinations of valid operators + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book..[0]")); + + // Test with invalid script expressions (if supported) + Assert.ThrowsExactly(() => _ = json.JsonPath("$.store.book[(@.length-1)]")); + } +} diff --git a/tests/Neo.Json.UnitTests/UT_JString.cs b/tests/Neo.Json.UnitTests/UT_JString.cs new file mode 100644 index 0000000000..a96345ea73 --- /dev/null +++ b/tests/Neo.Json.UnitTests/UT_JString.cs @@ -0,0 +1,422 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_JString.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Text; +using System.Text.Json; + +namespace Neo.Json.UnitTests; + +[TestClass] +public class UT_JString +{ + private static readonly JString AsicString = "hello world"; + private static readonly JString EscapeString = "\n\t\'\""; + private static readonly JString BadChar = ((char)0xff).ToString(); + private static readonly JString IntegerString = "123"; + private static readonly JString EmptyString = ""; + private static readonly JString SpaceString = " "; + private static readonly JString DoubleString = "123.456"; + private static readonly JString UnicodeString = "\ud83d\ude03\ud83d\ude01"; + private static readonly JString EmojString = "ã🦆"; + private static readonly JString MixedString = "abc123!@# "; + private static readonly JString LongString = new string('x', 5000); // 5000 + private static readonly JString MultiLangString = "Hello 你好 مرحبا"; + private static readonly JString JsonString = "{\"key\": \"value\"}"; + private static readonly JString HtmlEntityString = "& < >"; + private static readonly JString ControlCharString = "\t\n\r"; + private static readonly JString SingleCharString = "a"; + private static readonly JString LongWordString = "Supercalifragilisticexpialidocious"; + private static readonly JString ConcatenatedString = new("Hello" + "123" + "!@#"); + private static readonly JString WhiteSpaceString = new(" leading and trailing spaces "); + private static readonly JString FilePathString = new(@"C:\Users\Example\file.txt"); + private static readonly JString LargeNumberString = new("12345678901234567890"); + private static readonly JString HexadecimalString = new("0x1A3F"); + private static readonly JString PalindromeString = new("racecar"); + private static readonly JString SqlInjectionString = new("SELECT * FROM users WHERE name = 'a'; DROP TABLE users;"); + private static readonly JString RegexString = new(@"^\d{3}-\d{2}-\d{4}$"); + private static readonly JString DateTimeString = new("2023-01-01T00:00:00"); + private static readonly JString SpecialCharString = new("!?@#$%^&*()"); + private static readonly JString SubstringString = new("Hello world"[..5]); + private static readonly JString CaseSensitiveString1 = new("TestString"); + private static readonly JString CaseSensitiveString2 = new("teststring"); + private static readonly JString BooleanString = new("true"); + private static readonly JString FormatSpecifierString = new("{0:C}"); + private static readonly JString EmojiSequenceString = new("👨‍👩‍👦"); + private static readonly JString NullCharString = new("Hello\0World"); + private static readonly JString RepeatingPatternString = new("abcabcabc"); + + [TestMethod] + public void TestConstructor() + { + string s = "hello world"; + var jstring = new JString(s); + Assert.AreEqual(s, jstring.Value); + } + + [TestMethod] + public void TestConstructorEmpty() + { + string s = ""; + var jstring = new JString(s); + Assert.AreEqual(s, jstring.Value); + } + + [TestMethod] + public void TestConstructorSpace() + { + string s = " "; + var jstring = new JString(s); + Assert.AreEqual(s, jstring.Value); + } + + [TestMethod] + public void TestAsBoolean() + { + Assert.IsTrue(AsicString.AsBoolean()); + Assert.IsTrue(EscapeString.AsBoolean()); + Assert.IsTrue(BadChar.AsBoolean()); + Assert.IsTrue(IntegerString.AsBoolean()); + Assert.IsFalse(EmptyString.AsBoolean()); + Assert.IsTrue(SpaceString.AsBoolean()); + Assert.IsTrue(DoubleString.AsBoolean()); + Assert.IsTrue(UnicodeString.AsBoolean()); + Assert.IsTrue(EmojString.AsBoolean()); + Assert.IsTrue(MixedString.AsBoolean()); + Assert.IsTrue(LongString.AsBoolean()); + Assert.IsTrue(MultiLangString.AsBoolean()); + Assert.IsTrue(JsonString.AsBoolean()); + Assert.IsTrue(HtmlEntityString.AsBoolean()); + Assert.IsTrue(ControlCharString.AsBoolean()); + Assert.IsTrue(SingleCharString.AsBoolean()); + Assert.IsTrue(LongWordString.AsBoolean()); + Assert.IsTrue(ConcatenatedString.AsBoolean()); + Assert.IsTrue(WhiteSpaceString.AsBoolean()); + Assert.IsTrue(FilePathString.AsBoolean()); + Assert.IsTrue(LargeNumberString.AsBoolean()); + Assert.IsTrue(HexadecimalString.AsBoolean()); + Assert.IsTrue(PalindromeString.AsBoolean()); + Assert.IsTrue(SqlInjectionString.AsBoolean()); + Assert.IsTrue(RegexString.AsBoolean()); + Assert.IsTrue(DateTimeString.AsBoolean()); + Assert.IsTrue(SpecialCharString.AsBoolean()); + Assert.IsTrue(SubstringString.AsBoolean()); + Assert.IsTrue(CaseSensitiveString1.AsBoolean()); + Assert.IsTrue(CaseSensitiveString2.AsBoolean()); + Assert.IsTrue(BooleanString.AsBoolean()); + Assert.IsTrue(FormatSpecifierString.AsBoolean()); + Assert.IsTrue(EmojiSequenceString.AsBoolean()); + Assert.IsTrue(NullCharString.AsBoolean()); + Assert.IsTrue(RepeatingPatternString.AsBoolean()); + } + + [TestMethod] + public void TestAsNumber() + { + Assert.AreEqual(double.NaN, AsicString.AsNumber()); + Assert.AreEqual(double.NaN, EscapeString.AsNumber()); + Assert.AreEqual(double.NaN, BadChar.AsNumber()); + Assert.AreEqual(123, IntegerString.AsNumber()); + Assert.AreEqual(0, EmptyString.AsNumber()); + Assert.AreEqual(double.NaN, SpaceString.AsNumber()); + Assert.AreEqual(123.456, DoubleString.AsNumber()); + Assert.AreEqual(double.NaN, UnicodeString.AsNumber()); + Assert.AreEqual(double.NaN, EmojString.AsNumber()); + Assert.AreEqual(double.NaN, MixedString.AsNumber()); + Assert.AreEqual(double.NaN, LongString.AsNumber()); + Assert.AreEqual(double.NaN, MultiLangString.AsNumber()); + Assert.AreEqual(double.NaN, JsonString.AsNumber()); + Assert.AreEqual(double.NaN, HtmlEntityString.AsNumber()); + Assert.AreEqual(double.NaN, ControlCharString.AsNumber()); + Assert.AreEqual(double.NaN, SingleCharString.AsNumber()); + Assert.AreEqual(double.NaN, LongWordString.AsNumber()); + Assert.AreEqual(double.NaN, ConcatenatedString.AsNumber()); + Assert.AreEqual(double.NaN, WhiteSpaceString.AsNumber()); + Assert.AreEqual(double.NaN, FilePathString.AsNumber()); + Assert.AreEqual(12345678901234567890d, LargeNumberString.AsNumber()); + Assert.AreEqual(double.NaN, HexadecimalString.AsNumber()); // Depending on how hexadecimal strings are handled + Assert.AreEqual(double.NaN, PalindromeString.AsNumber()); + Assert.AreEqual(double.NaN, SqlInjectionString.AsNumber()); + Assert.AreEqual(double.NaN, RegexString.AsNumber()); + Assert.AreEqual(double.NaN, DateTimeString.AsNumber()); + Assert.AreEqual(double.NaN, SpecialCharString.AsNumber()); + Assert.AreEqual(double.NaN, SubstringString.AsNumber()); + Assert.AreEqual(double.NaN, CaseSensitiveString1.AsNumber()); + Assert.AreEqual(double.NaN, CaseSensitiveString2.AsNumber()); + Assert.AreEqual(double.NaN, BooleanString.AsNumber()); + Assert.AreEqual(double.NaN, FormatSpecifierString.AsNumber()); + Assert.AreEqual(double.NaN, EmojiSequenceString.AsNumber()); + Assert.AreEqual(double.NaN, NullCharString.AsNumber()); + Assert.AreEqual(double.NaN, RepeatingPatternString.AsNumber()); + } + + [TestMethod] + public void TestValidGetEnum() + { + JString validEnum = "James"; + + Woo woo = validEnum.GetEnum(); + Assert.AreEqual(Woo.James, woo); + + validEnum = ""; + woo = validEnum.AsEnum(Woo.Jerry, false); + Assert.AreEqual(Woo.Jerry, woo); + } + + [TestMethod] + public void TestInValidGetEnum() + { + JString validEnum = "_James"; + Assert.ThrowsExactly(() => validEnum.GetEnum()); + } + + [TestMethod] + public void TestMixedString() + { + Assert.AreEqual("abc123!@# ", MixedString.Value); + } + + [TestMethod] + public void TestLongString() + { + Assert.AreEqual(new string('x', 5000), LongString.Value); + } + + [TestMethod] + public void TestMultiLangString() + { + Assert.AreEqual("Hello 你好 مرحبا", MultiLangString.Value); + } + + [TestMethod] + public void TestJsonString() + { + Assert.AreEqual("{\"key\": \"value\"}", JsonString.Value); + } + + [TestMethod] + public void TestHtmlEntityString() + { + Assert.AreEqual("& < >", HtmlEntityString.Value); + } + + [TestMethod] + public void TestControlCharString() + { + Assert.AreEqual("\t\n\r", ControlCharString.Value); + } + + [TestMethod] + public void TestSingleCharString() + { + Assert.AreEqual("a", SingleCharString.Value); + } + + [TestMethod] + public void TestLongWordString() + { + Assert.AreEqual("Supercalifragilisticexpialidocious", LongWordString.Value); + } + + [TestMethod] + public void TestConcatenatedString() + { + Assert.AreEqual("Hello123!@#", ConcatenatedString.Value); + } + + [TestMethod] + public void TestWhiteSpaceString() + { + Assert.AreEqual(" leading and trailing spaces ", WhiteSpaceString.Value); + } + + [TestMethod] + public void TestFilePathString() + { + Assert.AreEqual(@"C:\Users\Example\file.txt", FilePathString.Value); + } + + [TestMethod] + public void TestLargeNumberString() + { + Assert.AreEqual("12345678901234567890", LargeNumberString.Value); + } + + [TestMethod] + public void TestHexadecimalString() + { + Assert.AreEqual("0x1A3F", HexadecimalString.Value); + } + + [TestMethod] + public void TestPalindromeString() + { + Assert.AreEqual("racecar", PalindromeString.Value); + } + + [TestMethod] + public void TestSqlInjectionString() + { + Assert.AreEqual("SELECT * FROM users WHERE name = 'a'; DROP TABLE users;", SqlInjectionString.Value); + } + + [TestMethod] + public void TestRegexString() + { + Assert.AreEqual(@"^\d{3}-\d{2}-\d{4}$", RegexString.Value); + } + + [TestMethod] + public void TestDateTimeString() + { + Assert.AreEqual("2023-01-01T00:00:00", DateTimeString.Value); + } + + [TestMethod] + public void TestSpecialCharString() + { + Assert.AreEqual("!?@#$%^&*()", SpecialCharString.Value); + } + + [TestMethod] + public void TestSubstringString() + { + Assert.AreEqual("Hello", SubstringString.Value); + } + + [TestMethod] + public void TestCaseSensitiveStrings() + { + Assert.AreNotEqual(CaseSensitiveString1.Value, CaseSensitiveString2.Value); + } + + [TestMethod] + public void TestBooleanString() + { + Assert.AreEqual("true", BooleanString.Value); + } + + [TestMethod] + public void TestFormatSpecifierString() + { + Assert.AreEqual("{0:C}", FormatSpecifierString.Value); + } + + [TestMethod] + public void TestEmojiSequenceString() + { + Assert.AreEqual("👨‍👩‍👦", EmojiSequenceString.Value); + } + + [TestMethod] + public void TestNullCharString() + { + Assert.AreEqual("Hello\0World", NullCharString.Value); + } + + [TestMethod] + public void TestRepeatingPatternString() + { + Assert.AreEqual("abcabcabc", RepeatingPatternString.Value); + } + + [TestMethod] + public void TestEqual() + { + var str = "hello world"; + var str2 = "hello world2"; + var jString = new JString(str); + var jString2 = new JString(str2); + + Assert.IsTrue(jString == str); + Assert.IsFalse(jString == null); + Assert.IsTrue(jString != str2); + Assert.IsFalse(jString == str2); + + Assert.AreEqual(str, jString.GetString()); + Assert.IsTrue(jString.Equals(str)); + Assert.IsFalse(jString.Equals(jString2)); + Assert.IsFalse(jString.Equals(null)); + Assert.IsFalse(jString.Equals(123)); + var reference = jString; + Assert.IsTrue(jString.Equals(reference)); + } + + [TestMethod] + public void TestWrite() + { + var jString = new JString("hello world"); + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + jString.Write(writer); + writer.Flush(); + var json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.AreEqual("\"hello world\"", json); + } + + [TestMethod] + public void TestClone() + { + var jString = new JString("hello world"); + var clone = jString.Clone(); + Assert.AreEqual(jString, clone); + Assert.AreSame(jString, clone); // Cloning should return the same instance for immutable objects + } + + [TestMethod] + public void TestEqualityWithDifferentTypes() + { + var jString = new JString("hello world"); + Assert.IsFalse(jString.Equals(123)); + Assert.IsFalse(jString.Equals(new object())); + Assert.IsFalse(jString.Equals(new JBoolean())); + } + + [TestMethod] + public void TestImplicitOperators() + { + JString fromEnum = EnumExample.Value; + Assert.AreEqual("Value", fromEnum.Value); + + JString fromString = "test string"; + Assert.AreEqual("test string", fromString.Value); + + JString? nullString = (string?)null; + Assert.IsNull(nullString); + } + + [TestMethod] + public void TestBoundaryAndSpecialCases() + { + JString largeString = new string('a', ushort.MaxValue); + Assert.AreEqual(ushort.MaxValue, largeString.Value.Length); + + JString specialUnicode = "\uD83D\uDE00"; // 😀 emoji + Assert.AreEqual("\uD83D\uDE00", specialUnicode.Value); + + JString complexJson = "{\"nested\":{\"key\":\"value\"}}"; + Assert.AreEqual("{\"nested\":{\"key\":\"value\"}}", complexJson.Value); + } + + [TestMethod] + public void TestExceptionHandling() + { + JString invalidEnum = "invalid_value"; + + var result = invalidEnum.AsEnum(Woo.Jerry); + Assert.AreEqual(Woo.Jerry, result); + + Assert.ThrowsExactly(() => _ = invalidEnum.GetEnum()); + } +} +public enum EnumExample +{ + Value +} diff --git a/tests/Neo.Json.UnitTests/Usings.cs b/tests/Neo.Json.UnitTests/Usings.cs new file mode 100644 index 0000000000..14b61dab70 --- /dev/null +++ b/tests/Neo.Json.UnitTests/Usings.cs @@ -0,0 +1,12 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Usings.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +global using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/tests/Neo.UnitTests/Builders/UT_SignerBuilder.cs b/tests/Neo.UnitTests/Builders/UT_SignerBuilder.cs new file mode 100644 index 0000000000..e95d0b37ea --- /dev/null +++ b/tests/Neo.UnitTests/Builders/UT_SignerBuilder.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_SignerBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Builders; +using Neo.Cryptography.ECC; +using Neo.Network.P2P.Payloads; +using Neo.Network.P2P.Payloads.Conditions; + +namespace Neo.UnitTests.Builders; + +[TestClass] +public class UT_SignerBuilder +{ + [TestMethod] + public void TestAccount() + { + var signer = SignerBuilder.Create(UInt160.Zero) + .Build(); + + Assert.IsNotNull(signer); + Assert.AreEqual(UInt160.Zero, signer.Account); + } + + [TestMethod] + public void TestAllowContract() + { + var signer = SignerBuilder.Create(UInt160.Zero) + .AllowContract(UInt160.Zero) + .Build(); + + Assert.HasCount(1, signer.AllowedContracts!); + Assert.AreEqual(UInt160.Zero, signer.AllowedContracts![0]); + } + + [TestMethod] + public void TestAllowGroup() + { + var myPublicKey = ECPoint.Parse("021821807f923a3da004fb73871509d7635bcc05f41edef2a3ca5c941d8bbc1231", ECCurve.Secp256r1); + var signer = SignerBuilder.Create(UInt160.Zero) + .AllowGroup(myPublicKey) + .Build(); + + Assert.HasCount(1, signer.AllowedGroups!); + Assert.AreEqual(myPublicKey, signer.AllowedGroups![0]); + } + + [TestMethod] + public void TestAddWitnessScope() + { + var signer = SignerBuilder.Create(UInt160.Zero) + .AddWitnessScope(WitnessScope.Global) + .Build(); + + Assert.AreEqual(WitnessScope.Global, signer.Scopes); + } + + [TestMethod] + public void TestAddWitnessRule() + { + var signer = SignerBuilder.Create(UInt160.Zero) + .AddWitnessRule(WitnessRuleAction.Allow, rb => + { + rb.AddCondition(cb => + { + cb.ScriptHash(UInt160.Zero); + }); + }) + .Build(); + + Assert.HasCount(1, signer.Rules!); + Assert.AreEqual(WitnessRuleAction.Allow, signer.Rules![0].Action); + Assert.IsInstanceOfType(signer.Rules[0].Condition); + } +} diff --git a/tests/Neo.UnitTests/Builders/UT_TransactionAttributesBuilder.cs b/tests/Neo.UnitTests/Builders/UT_TransactionAttributesBuilder.cs new file mode 100644 index 0000000000..89015334de --- /dev/null +++ b/tests/Neo.UnitTests/Builders/UT_TransactionAttributesBuilder.cs @@ -0,0 +1,85 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_TransactionAttributesBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Builders; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Builders; + +[TestClass] +public class UT_TransactionAttributesBuilder +{ + [TestMethod] + public void TestCreateEmpty() + { + var builder = TransactionAttributesBuilder.CreateEmpty(); + + Assert.IsNotNull(builder); + } + + [TestMethod] + public void TestConflict() + { + var attr = TransactionAttributesBuilder.CreateEmpty() + .AddConflict(UInt256.Zero) + .Build(); + + Assert.IsNotNull(attr); + Assert.HasCount(1, attr); + Assert.IsInstanceOfType(attr[0]); + Assert.AreEqual(UInt256.Zero, ((Conflicts)attr[0]).Hash); + } + + [TestMethod] + public void TestOracleResponse() + { + var attr = TransactionAttributesBuilder.CreateEmpty() + .AddOracleResponse(c => + { + c.Id = 1ul; + c.Code = OracleResponseCode.Success; + c.Result = new byte[] { 0x01, 0x02, 0x03 }; + }) + .Build(); + + Assert.IsNotNull(attr); + Assert.HasCount(1, attr); + Assert.IsInstanceOfType(attr[0]); + Assert.AreEqual(1ul, ((OracleResponse)attr[0]).Id); + Assert.AreEqual(OracleResponseCode.Success, ((OracleResponse)attr[0]).Code); + CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03 }, ((OracleResponse)attr[0]).Result.ToArray()); + } + + [TestMethod] + public void TestHighPriority() + { + var attr = TransactionAttributesBuilder.CreateEmpty() + .AddHighPriority() + .Build(); + + Assert.IsNotNull(attr); + Assert.HasCount(1, attr); + Assert.IsInstanceOfType(attr[0]); + } + + [TestMethod] + public void TestNotValidBefore() + { + var attr = TransactionAttributesBuilder.CreateEmpty() + .AddNotValidBefore(10u) + .Build(); + + Assert.IsNotNull(attr); + Assert.HasCount(1, attr); + Assert.IsInstanceOfType(attr[0]); + Assert.AreEqual(10u, ((NotValidBefore)attr[0]).Height); + } +} diff --git a/tests/Neo.UnitTests/Builders/UT_TransactionBuilder.cs b/tests/Neo.UnitTests/Builders/UT_TransactionBuilder.cs new file mode 100644 index 0000000000..7f8e7d0382 --- /dev/null +++ b/tests/Neo.UnitTests/Builders/UT_TransactionBuilder.cs @@ -0,0 +1,191 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_TransactionBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Builders; +using Neo.Cryptography.ECC; +using Neo.Factories; +using Neo.Network.P2P.Payloads; +using Neo.Network.P2P.Payloads.Conditions; +using Neo.VM; + +namespace Neo.UnitTests.Builders; + +[TestClass] +public class UT_TransactionBuilder +{ + [TestMethod] + public void TestCreateEmpty() + { + var builder = TransactionBuilder.CreateEmpty(); + + Assert.IsNotNull(builder); + } + + [TestMethod] + public void TestEmptyTransaction() + { + var tx = TransactionBuilder.CreateEmpty() + .Build(); + + Assert.IsNotNull(tx.Hash); + } + + [TestMethod] + public void TestVersion() + { + byte expectedVersion = 1; + var tx = TransactionBuilder.CreateEmpty() + .Version(expectedVersion) + .Build(); + + Assert.AreEqual(expectedVersion, tx.Version); + Assert.IsNotNull(tx.Hash); + } + + [TestMethod] + public void TestNonce() + { + var expectedNonce = RandomNumberFactory.NextUInt32(); + var tx = TransactionBuilder.CreateEmpty() + .Nonce(expectedNonce) + .Build(); + + Assert.AreEqual(expectedNonce, tx.Nonce); + Assert.IsNotNull(tx.Hash); + } + + [TestMethod] + public void TestSystemFee() + { + var expectedSystemFee = RandomNumberFactory.NextUInt32(); + var tx = TransactionBuilder.CreateEmpty() + .SystemFee(expectedSystemFee) + .Build(); + + Assert.AreEqual(expectedSystemFee, tx.SystemFee); + Assert.IsNotNull(tx.Hash); + } + + [TestMethod] + public void TestNetworkFee() + { + var expectedNetworkFee = RandomNumberFactory.NextUInt32(); + var tx = TransactionBuilder.CreateEmpty() + .NetworkFee(expectedNetworkFee) + .Build(); + + Assert.AreEqual(expectedNetworkFee, tx.NetworkFee); + Assert.IsNotNull(tx.Hash); + } + + [TestMethod] + public void TestValidUntilBlock() + { + var expectedValidUntilBlock = RandomNumberFactory.NextUInt32(); + var tx = TransactionBuilder.CreateEmpty() + .ValidUntil(expectedValidUntilBlock) + .Build(); + + Assert.AreEqual(expectedValidUntilBlock, tx.ValidUntilBlock); + Assert.IsNotNull(tx.Hash); + } + + [TestMethod] + public void TestAttachScript() + { + byte[] expectedScript = [(byte)OpCode.NOP]; + var tx = TransactionBuilder.CreateEmpty() + .AttachSystem(sb => sb.Emit(OpCode.NOP)) + .Build(); + + CollectionAssert.AreEqual(expectedScript, tx.Script.ToArray()); + Assert.IsNotNull(tx.Hash); + } + + [TestMethod] + public void TestTransactionAttributes() + { + var tx = TransactionBuilder.CreateEmpty() + .AddAttributes(ab => ab.AddHighPriority()) + .Build(); + + Assert.HasCount(1, tx.Attributes); + Assert.IsInstanceOfType(tx.Attributes[0]); + Assert.IsNotNull(tx.Hash); + } + + [TestMethod] + public void TestWitness() + { + var tx = TransactionBuilder.CreateEmpty() + .AddWitness(wb => + { + // Contract signature + wb.AddInvocation([]); + wb.AddVerification([]); + }) + .Build(); + + Assert.HasCount(1, tx.Witnesses); + Assert.AreEqual(0, tx.Witnesses[0].InvocationScript.Length); + Assert.AreEqual(0, tx.Witnesses[0].VerificationScript.Length); + Assert.IsNotNull(tx.Hash); + } + + [TestMethod] + public void TestWitnessWithTransactionParameter() + { + var tx = TransactionBuilder.CreateEmpty() + .AddWitness((wb, tx) => + { + // Checks to make sure the transaction is hash able + // NOTE: transaction can be used for signing here + Assert.IsNotNull(tx.Hash); + }) + .Build(); + } + + [TestMethod] + public void TestSigner() + { + var expectedPublicKey = ECPoint.Parse("021821807f923a3da004fb73871509d7635bcc05f41edef2a3ca5c941d8bbc1231", ECCurve.Secp256r1); + var expectedContractHash = UInt160.Zero; + + var tx = TransactionBuilder.CreateEmpty() + .AddSigner(expectedContractHash, (sb, tx) => + { + sb.AllowContract(expectedContractHash); + sb.AllowGroup(expectedPublicKey); + sb.AddWitnessScope(WitnessScope.WitnessRules); + sb.AddWitnessRule(WitnessRuleAction.Deny, wrb => + { + wrb.AddCondition(cb => + { + cb.ScriptHash(expectedContractHash); + }); + }); + }) + .Build(); + + Assert.IsNotNull(tx.Hash); + Assert.HasCount(1, tx.Signers); + Assert.AreEqual(expectedContractHash, tx.Signers[0].Account); + Assert.HasCount(1, tx.Signers[0].AllowedContracts!); + Assert.AreEqual(expectedContractHash, tx.Signers[0].AllowedContracts![0]); + Assert.HasCount(1, tx.Signers[0].AllowedGroups!); + Assert.AreEqual(expectedPublicKey, tx.Signers[0].AllowedGroups![0]); + Assert.AreEqual(WitnessScope.CustomContracts | WitnessScope.CustomGroups | WitnessScope.WitnessRules, tx.Signers[0].Scopes); + Assert.HasCount(1, tx.Signers[0].Rules!); + Assert.AreEqual(WitnessRuleAction.Deny, tx.Signers[0].Rules![0].Action); + Assert.IsNotNull(tx.Signers[0].Rules![0].Condition); + Assert.IsInstanceOfType(tx.Signers[0].Rules![0].Condition); + } +} diff --git a/tests/Neo.UnitTests/Builders/UT_WitnessBuilder.cs b/tests/Neo.UnitTests/Builders/UT_WitnessBuilder.cs new file mode 100644 index 0000000000..86fa3971e8 --- /dev/null +++ b/tests/Neo.UnitTests/Builders/UT_WitnessBuilder.cs @@ -0,0 +1,84 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_WitnessBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Builders; +using Neo.VM; + +namespace Neo.UnitTests.Builders; + +[TestClass] +public class UT_WitnessBuilder +{ + [TestMethod] + public void TestCreateEmpty() + { + var wb = WitnessBuilder.CreateEmpty(); + Assert.IsNotNull(wb); + } + + [TestMethod] + public void TestAddInvocationWithScriptBuilder() + { + var witness = WitnessBuilder.CreateEmpty() + .AddInvocation(sb => + { + sb.Emit(OpCode.NOP); + sb.Emit(OpCode.NOP); + sb.Emit(OpCode.NOP); + }) + .Build(); + + Assert.IsNotNull(witness); + Assert.AreEqual(3, witness.InvocationScript.Length); + CollectionAssert.AreEqual(new byte[] { 0x21, 0x21, 0x21 }, witness.InvocationScript.ToArray()); + } + + [TestMethod] + public void TestAddInvocation() + { + var witness = WitnessBuilder.CreateEmpty() + .AddInvocation(new byte[] { 0x01, 0x02, 0x03 }) + .Build(); + + Assert.IsNotNull(witness); + Assert.AreEqual(3, witness.InvocationScript.Length); + CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03 }, witness.InvocationScript.ToArray()); + } + + [TestMethod] + public void TestAddVerificationWithScriptBuilder() + { + var witness = WitnessBuilder.CreateEmpty() + .AddVerification(sb => + { + sb.Emit(OpCode.NOP); + sb.Emit(OpCode.NOP); + sb.Emit(OpCode.NOP); + }) + .Build(); + + Assert.IsNotNull(witness); + Assert.AreEqual(3, witness.VerificationScript.Length); + CollectionAssert.AreEqual(new byte[] { 0x21, 0x21, 0x21 }, witness.VerificationScript.ToArray()); + } + + [TestMethod] + public void TestAddVerification() + { + var witness = WitnessBuilder.CreateEmpty() + .AddVerification(new byte[] { 0x01, 0x02, 0x03 }) + .Build(); + + Assert.IsNotNull(witness); + Assert.AreEqual(3, witness.VerificationScript.Length); + CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03 }, witness.VerificationScript.ToArray()); + } +} diff --git a/tests/Neo.UnitTests/Builders/UT_WitnessConditionBuilder.cs b/tests/Neo.UnitTests/Builders/UT_WitnessConditionBuilder.cs new file mode 100644 index 0000000000..0cb9445eea --- /dev/null +++ b/tests/Neo.UnitTests/Builders/UT_WitnessConditionBuilder.cs @@ -0,0 +1,199 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_WitnessConditionBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Builders; +using Neo.Cryptography.ECC; +using Neo.Network.P2P.Payloads.Conditions; + +namespace Neo.UnitTests.Builders; + +[TestClass] +public class UT_WitnessConditionBuilder +{ + [TestMethod] + public void TestAndCondition() + { + var expectedPublicKey = ECPoint.Parse("021821807f923a3da004fb73871509d7635bcc05f41edef2a3ca5c941d8bbc1231", ECCurve.Secp256r1); + var expectedContractHash = UInt160.Zero; + var condition = WitnessConditionBuilder.Create() + .And(and => + { + and.CalledByContract(expectedContractHash); + and.CalledByGroup(expectedPublicKey); + }) + .Build(); + + Assert.IsInstanceOfType(condition, out var actual); + Assert.HasCount(2, actual.Expressions); + Assert.IsInstanceOfType(actual.Expressions[0], out var exp0); + Assert.IsInstanceOfType(actual.Expressions[1], out var exp1); + Assert.AreEqual(expectedContractHash, exp0.Hash); + Assert.AreEqual(expectedPublicKey, exp1.Group); + } + + [TestMethod] + public void TestOrCondition() + { + var expectedPublicKey = ECPoint.Parse("021821807f923a3da004fb73871509d7635bcc05f41edef2a3ca5c941d8bbc1231", ECCurve.Secp256r1); + var expectedContractHash = UInt160.Zero; + var condition = WitnessConditionBuilder.Create() + .Or(or => + { + or.CalledByContract(expectedContractHash); + or.CalledByGroup(expectedPublicKey); + }) + .Build(); + + Assert.IsInstanceOfType(condition, out var actual); + Assert.HasCount(2, actual.Expressions); + Assert.IsInstanceOfType(actual.Expressions[0], out var exp0); + Assert.IsInstanceOfType(actual.Expressions[1], out var exp1); + Assert.AreEqual(expectedContractHash, exp0.Hash); + Assert.AreEqual(expectedPublicKey, exp1.Group); + } + + [TestMethod] + public void TestBoolean() + { + var condition = WitnessConditionBuilder.Create() + .Boolean(true) + .Build(); + + var actual = condition as BooleanCondition; + + Assert.IsNotNull(actual); + Assert.IsInstanceOfType(condition); + Assert.IsTrue(actual.Expression); + } + + [TestMethod] + public void TestCalledByContract() + { + var expectedContractHash = UInt160.Zero; + var condition = WitnessConditionBuilder.Create() + .CalledByContract(expectedContractHash) + .Build(); + + var actual = condition as CalledByContractCondition; + + Assert.IsNotNull(actual); + Assert.IsInstanceOfType(condition); + Assert.AreEqual(expectedContractHash, actual.Hash); + } + + [TestMethod] + public void TestCalledByEntry() + { + var condition = WitnessConditionBuilder.Create() + .CalledByEntry() + .Build(); + + var actual = condition as CalledByEntryCondition; + + Assert.IsNotNull(actual); + Assert.IsInstanceOfType(condition); + } + + [TestMethod] + public void TestCalledByGroup() + { + var expectedPublicKey = ECPoint.Parse("021821807f923a3da004fb73871509d7635bcc05f41edef2a3ca5c941d8bbc1231", ECCurve.Secp256r1); + var condition = WitnessConditionBuilder.Create() + .CalledByGroup(expectedPublicKey) + .Build(); + + var actual = condition as CalledByGroupCondition; + + Assert.IsNotNull(actual); + Assert.IsInstanceOfType(condition); + Assert.AreEqual(expectedPublicKey, actual.Group); + } + + [TestMethod] + public void TestGroup() + { + var expectedPublicKey = ECPoint.Parse("021821807f923a3da004fb73871509d7635bcc05f41edef2a3ca5c941d8bbc1231", ECCurve.Secp256r1); + var condition = WitnessConditionBuilder.Create() + .Group(expectedPublicKey) + .Build(); + + var actual = condition as GroupCondition; + + Assert.IsNotNull(actual); + Assert.IsInstanceOfType(condition); + Assert.AreEqual(expectedPublicKey, actual.Group); + } + + [TestMethod] + public void TestScriptHash() + { + var expectedContractHash = UInt160.Zero; + var condition = WitnessConditionBuilder.Create() + .ScriptHash(expectedContractHash) + .Build(); + + var actual = condition as ScriptHashCondition; + + Assert.IsNotNull(actual); + Assert.IsInstanceOfType(condition); + Assert.AreEqual(expectedContractHash, actual.Hash); + } + + [TestMethod] + public void TestNotConditionWithAndCondition() + { + var expectedPublicKey = ECPoint.Parse("021821807f923a3da004fb73871509d7635bcc05f41edef2a3ca5c941d8bbc1231", ECCurve.Secp256r1); + var expectedContractHash = UInt160.Zero; + var condition = WitnessConditionBuilder.Create() + .Not(not => + { + not.And(and => + { + and.CalledByContract(expectedContractHash); + and.CalledByGroup(expectedPublicKey); + }); + }) + .Build(); + + Assert.IsInstanceOfType(condition, out var actual); + Assert.IsInstanceOfType(actual.Expression, out var actualAndCondition); + Assert.HasCount(2, actualAndCondition.Expressions); + Assert.IsInstanceOfType(actualAndCondition.Expressions[0], out var exp0); + Assert.IsInstanceOfType(actualAndCondition.Expressions[1], out var exp1); + Assert.AreEqual(expectedContractHash, exp0.Hash); + Assert.AreEqual(expectedPublicKey, exp1.Group); + } + + [TestMethod] + public void TestNotConditionWithOrCondition() + { + var expectedPublicKey = ECPoint.Parse("021821807f923a3da004fb73871509d7635bcc05f41edef2a3ca5c941d8bbc1231", ECCurve.Secp256r1); + var expectedContractHash = UInt160.Zero; + var condition = WitnessConditionBuilder.Create() + .Not(not => + { + not.Or(or => + { + or.CalledByContract(expectedContractHash); + or.CalledByGroup(expectedPublicKey); + }); + }) + .Build(); + + Assert.IsInstanceOfType(condition, out var actual); + Assert.IsInstanceOfType(actual.Expression, out var actualOrCondition); + Assert.HasCount(2, actualOrCondition.Expressions); + Assert.IsInstanceOfType(actualOrCondition.Expressions[0], out var exp0); + Assert.IsInstanceOfType(actualOrCondition.Expressions[1], out var exp1); + Assert.AreEqual(expectedContractHash, exp0.Hash); + Assert.AreEqual(expectedPublicKey, exp1.Group); + } +} diff --git a/tests/Neo.UnitTests/Builders/UT_WitnessRuleBuilder.cs b/tests/Neo.UnitTests/Builders/UT_WitnessRuleBuilder.cs new file mode 100644 index 0000000000..1c7ad6e7d0 --- /dev/null +++ b/tests/Neo.UnitTests/Builders/UT_WitnessRuleBuilder.cs @@ -0,0 +1,61 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_WitnessRuleBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Builders; +using Neo.Network.P2P.Payloads; +using Neo.Network.P2P.Payloads.Conditions; + +namespace Neo.UnitTests.Builders; + +[TestClass] +public class UT_WitnessRuleBuilder +{ + [TestMethod] + public void TestCreate() + { + var builder = WitnessRuleBuilder.Create(WitnessRuleAction.Allow); + + Assert.IsNotNull(builder); + } + + [TestMethod] + public void TestCondition() + { + var rule = WitnessRuleBuilder.Create(WitnessRuleAction.Allow) + .AddCondition(wcb => + { + wcb.ScriptHash(UInt160.Zero); + }).Build(); + + Assert.IsNotNull(rule.Condition); + Assert.AreEqual(WitnessRuleAction.Allow, rule.Action); + Assert.IsInstanceOfType(rule.Condition); + Assert.AreEqual(UInt160.Zero, ((ScriptHashCondition)rule.Condition).Hash); + } + + [TestMethod] + public void TestCondition2() + { + var rule = WitnessRuleBuilder.Create(WitnessRuleAction.Allow) + .AddCondition(wcb => + { + wcb.And(and => + { + and.ScriptHash(UInt160.Zero); + }); + }).Build(); + + Assert.AreEqual(WitnessRuleAction.Allow, rule.Action); + Assert.IsInstanceOfType(rule.Condition, out var condition); + Assert.IsInstanceOfType(condition.Expressions[0], out var exp0); + Assert.AreEqual(UInt160.Zero, exp0.Hash); + } +} diff --git a/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs b/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs new file mode 100644 index 0000000000..2772435e3f --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs @@ -0,0 +1,123 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ECFieldElement.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 System.Globalization; +using System.Numerics; +using System.Reflection; + +namespace Neo.UnitTests.Cryptography.ECC; + +[TestClass] +public class UT_ECFieldElement +{ + [TestMethod] + public void TestECFieldElementConstructor() + { + BigInteger input = new(100); + + try + { + _ = new ECFieldElement(input, ECCurve.Secp256k1); + } + catch + { + Assert.Fail(); + } + + input = ECCurve.Secp256k1.Q; + Assert.ThrowsExactly(() => new ECFieldElement(input, ECCurve.Secp256k1)); + } + + [TestMethod] + public void TestGetHashCode() + { + var pointA = new ECFieldElement(new BigInteger(100), ECCurve.Secp256k1); + var pointB = new ECFieldElement(new BigInteger(100), ECCurve.Secp256k1); + var pointC = new ECFieldElement(new BigInteger(100), ECCurve.Secp256r1); // different curve + var pointD = new ECFieldElement(new BigInteger(101), ECCurve.Secp256k1); + + Assert.AreEqual(pointA.GetHashCode(), pointB.GetHashCode()); + Assert.AreNotEqual(pointA.GetHashCode(), pointC.GetHashCode()); + Assert.AreNotEqual(pointB.GetHashCode(), pointC.GetHashCode()); + Assert.AreNotEqual(pointB.GetHashCode(), pointD.GetHashCode()); + } + + [TestMethod] + public void TestCompareTo() + { + ECFieldElement X1 = new(new BigInteger(100), ECCurve.Secp256k1); + ECFieldElement Y1 = new(new BigInteger(200), ECCurve.Secp256k1); + ECFieldElement X2 = new(new BigInteger(300), ECCurve.Secp256k1); + ECFieldElement Y2 = new(new BigInteger(400), ECCurve.Secp256k1); + ECFieldElement X3 = new(new BigInteger(100), ECCurve.Secp256r1); + ECFieldElement Y3 = new(new BigInteger(400), ECCurve.Secp256r1); + ECPoint point1 = new(X1, Y1, ECCurve.Secp256k1); + ECPoint point2 = new(X2, Y1, ECCurve.Secp256k1); + ECPoint point3 = new(X1, Y2, ECCurve.Secp256k1); + ECPoint point4 = new(X3, Y3, ECCurve.Secp256r1); + + Assert.AreEqual(0, point1.CompareTo(point1)); + Assert.AreEqual(-1, point1.CompareTo(point2)); + Assert.AreEqual(1, point2.CompareTo(point1)); + Assert.AreEqual(-1, point1.CompareTo(point3)); + Assert.AreEqual(1, point3.CompareTo(point1)); + + var action = new Action(() => point3.CompareTo(point4)); + Assert.ThrowsExactly(() => action()); + } + + [TestMethod] + public void TestEquals() + { + BigInteger input = new(100); + object element = new ECFieldElement(input, ECCurve.Secp256k1); + Assert.IsTrue(element.Equals(element)); + Assert.IsFalse(element.Equals(1)); + Assert.IsFalse(element.Equals(new ECFieldElement(input, ECCurve.Secp256r1))); + + input = new BigInteger(200); + Assert.IsFalse(element.Equals(new ECFieldElement(input, ECCurve.Secp256k1))); + } + + [TestMethod] + public void TestSqrt() + { + ECFieldElement element = new(new BigInteger(100), ECCurve.Secp256k1); + Assert.AreEqual(new ECFieldElement(BigInteger.Parse("115792089237316195423570985008687907853269984665640564039457584007908834671653"), ECCurve.Secp256k1), element.Sqrt()); + + var constructor = typeof(ECCurve).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, + [typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(byte[]), typeof(string)], null)!; + var testCruve = (ECCurve)constructor.Invoke([ + BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFF0", NumberStyles.AllowHexSpecifier), + BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFF00", NumberStyles.AllowHexSpecifier), + BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier), + BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier), + ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes(), + "secp256k1"]); + element = new ECFieldElement(new BigInteger(200), testCruve); + Assert.IsNull(element.Sqrt()); + } + + [TestMethod] + public void TestToByteArray() + { + byte[] result = new byte[32]; + result[31] = 100; + CollectionAssert.AreEqual(result, new ECFieldElement(new BigInteger(100), ECCurve.Secp256k1).ToByteArray()); + + byte[] result2 = { 2, 53, 250, 221, 129, 194, 130, 43, 179, 240, 120, 119, 151, 61, 80, 242, 139, 242, 42, 49, 190, 142, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + CollectionAssert.AreEqual(result2, new ECFieldElement(BigInteger.Pow(new BigInteger(10), 75), ECCurve.Secp256k1).ToByteArray()); + + byte[] result3 = { 221, 21, 254, 134, 175, 250, 217, 18, 73, 239, 14, 183, 19, 243, 158, 190, 170, 152, 123, 110, 111, 210, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + CollectionAssert.AreEqual(result3, new ECFieldElement(BigInteger.Pow(new BigInteger(10), 77), ECCurve.Secp256k1).ToByteArray()); + } +} diff --git a/tests/Neo.UnitTests/Cryptography/ECC/UT_ECPoint.cs b/tests/Neo.UnitTests/Cryptography/ECC/UT_ECPoint.cs new file mode 100644 index 0000000000..7b00f7618e --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/ECC/UT_ECPoint.cs @@ -0,0 +1,453 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ECPoint.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#nullable enable + +using Neo.Cryptography.ECC; +using Neo.Extensions.IO; +using Neo.IO; +using System.Numerics; +using ECCurve = Neo.Cryptography.ECC.ECCurve; +using ECPoint = Neo.Cryptography.ECC.ECPoint; + +namespace Neo.UnitTests.Cryptography.ECC; + +[TestClass] +public class UT_ECPoint +{ + private static readonly string s_uncompressed = "04" + + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + + "483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"; + + private static readonly string s_compressedX = + "02" + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; + + private static readonly string s_compressedY = + "03" + "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"; + + public static byte[] GeneratePrivateKey(int privateKeyLength) + { + byte[] privateKey = new byte[privateKeyLength]; + for (int i = 0; i < privateKeyLength; i++) + { + privateKey[i] = (byte)((byte)i % byte.MaxValue); + } + return privateKey; + } + + [TestMethod] + public void TestCompareTo() + { + var X1 = new ECFieldElement(new BigInteger(100), ECCurve.Secp256k1); + var X2 = new ECFieldElement(new BigInteger(200), ECCurve.Secp256k1); + var X3 = new ECFieldElement(new BigInteger(100), ECCurve.Secp256r1); + + Assert.AreEqual(-1, X1.CompareTo(X2)); + Assert.ThrowsExactly(() => X1.CompareTo(X3)); + } + + [TestMethod] + public void TestECPointConstructor() + { + ECPoint point = new(); + Assert.IsNull(point.X); + Assert.IsNull(point.Y); + Assert.AreEqual(ECCurve.Secp256r1, point.Curve); + + var X = new ECFieldElement(new BigInteger(100), ECCurve.Secp256k1); + var Y = new ECFieldElement(new BigInteger(200), ECCurve.Secp256k1); + point = new ECPoint(X, Y, ECCurve.Secp256k1); + Assert.AreEqual(X, point.X); + Assert.AreEqual(Y, point.Y); + Assert.AreEqual(ECCurve.Secp256k1, point.Curve); + Assert.ThrowsExactly(() => new ECPoint(X, null, ECCurve.Secp256k1)); + Assert.ThrowsExactly(() => new ECPoint(null, Y, ECCurve.Secp256k1)); + } + + [TestMethod] + public void TestDecodePoint() + { + byte[] input1 = [0]; + Action action = () => ECPoint.DecodePoint(input1, ECCurve.Secp256k1); + Assert.ThrowsExactly(action); + + var uncompressed = s_uncompressed.HexToBytes(); + Assert.AreEqual(ECCurve.Secp256k1.G, ECPoint.DecodePoint(uncompressed, ECCurve.Secp256k1)); + action = () => ECPoint.DecodePoint(uncompressed.Take(32).ToArray(), ECCurve.Secp256k1); + Assert.ThrowsExactly(action); + + byte[] input3 = s_compressedX.HexToBytes(); + byte[] input4 = s_compressedY.HexToBytes(); + Assert.AreEqual(ECCurve.Secp256k1.G, ECPoint.DecodePoint(input3, ECCurve.Secp256k1)); + Assert.AreEqual(ECCurve.Secp256r1.G, ECPoint.DecodePoint(input4, ECCurve.Secp256r1)); + + action = () => ECPoint.DecodePoint(input3.Take(input3.Length - 1).ToArray(), ECCurve.Secp256k1); + Assert.ThrowsExactly(action); + } + + [TestMethod] + public void TestDeserializeFrom() + { + byte[] input1 = [0]; + var reader1 = new MemoryReader(input1); + try + { + ECPoint.DeserializeFrom(ref reader1, ECCurve.Secp256k1); + Assert.Fail("Expected FormatException was not thrown"); + } + catch (FormatException) { } + + var input2 = s_uncompressed.HexToBytes(); + var reader2 = new MemoryReader(input2); + Assert.AreEqual(ECPoint.DeserializeFrom(ref reader2, ECCurve.Secp256k1), ECCurve.Secp256k1.G); + reader2 = new(input2.Take(32).ToArray()); + try + { + ECPoint.DeserializeFrom(ref reader2, ECCurve.Secp256k1); + Assert.Fail(); + } + catch (FormatException) { } + + var input3 = s_compressedX.HexToBytes(); + var reader3 = new MemoryReader(input3); + Assert.AreEqual(ECCurve.Secp256k1.G, ECPoint.DeserializeFrom(ref reader3, ECCurve.Secp256k1)); + + var input4 = s_compressedY.HexToBytes(); + var reader4 = new MemoryReader(input4); + Assert.AreEqual(ECCurve.Secp256r1.G, ECPoint.DeserializeFrom(ref reader4, ECCurve.Secp256r1)); + + reader3 = new(input3.Take(input3.Length - 1).ToArray()); + try + { + ECPoint.DeserializeFrom(ref reader3, ECCurve.Secp256k1); + Assert.Fail("Expected FormatException was not thrown"); + } + catch (FormatException) { } + } + + [TestMethod] + public void TestEncodePoint() + { + var point = new ECPoint(null, null, ECCurve.Secp256k1); + byte[] result1 = [0]; + CollectionAssert.AreEqual(result1, point.EncodePoint(true)); + + point = ECCurve.Secp256k1.G; + var result2 = s_uncompressed.HexToBytes(); + CollectionAssert.AreEqual(result2, point.EncodePoint(false)); + CollectionAssert.AreEqual(result2, point.EncodePoint(false)); + + var result3 = s_compressedX.HexToBytes(); + CollectionAssert.AreEqual(result3, point.EncodePoint(true)); + CollectionAssert.AreEqual(result3, point.EncodePoint(true)); + + point = ECCurve.Secp256r1.G; + var result4 = s_compressedY.HexToBytes(); + CollectionAssert.AreEqual(result4, point.EncodePoint(true)); + CollectionAssert.AreEqual(result4, point.EncodePoint(true)); + + // Test cache + point = ECPoint.DecodePoint(ECCurve.Secp256r1.G.EncodePoint(true), ECCurve.Secp256r1); + CollectionAssert.AreEqual(result4, point.EncodePoint(true)); + CollectionAssert.AreEqual(result4, point.EncodePoint(true)); + + var result5 = "04" + "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296" + + "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5"; + point = ECPoint.DecodePoint(ECCurve.Secp256r1.G.EncodePoint(false), ECCurve.Secp256r1); + CollectionAssert.AreEqual(result4, point.EncodePoint(true)); + CollectionAssert.AreEqual(result4, point.EncodePoint(true)); + CollectionAssert.AreEqual(result5.HexToBytes(), point.EncodePoint(false)); + CollectionAssert.AreEqual(result5.HexToBytes(), point.EncodePoint(false)); + } + + [TestMethod] + public void TestEquals() + { + var point = ECCurve.Secp256k1.G; + Assert.IsTrue(point.Equals(point)); + Assert.IsFalse(point.Equals(null)); + + point = new ECPoint(null, null, ECCurve.Secp256k1); + Assert.IsFalse(point.Equals(new ECPoint(null, null, ECCurve.Secp256r1))); + Assert.IsFalse(point.Equals(ECCurve.Secp256r1.G)); + Assert.IsFalse(ECCurve.Secp256r1.G.Equals(point)); + + var X1 = new ECFieldElement(new BigInteger(100), ECCurve.Secp256k1); + var Y1 = new ECFieldElement(new BigInteger(200), ECCurve.Secp256k1); + var X2 = new ECFieldElement(new BigInteger(300), ECCurve.Secp256k1); + var Y2 = new ECFieldElement(new BigInteger(400), ECCurve.Secp256k1); + var point1 = new ECPoint(X1, Y1, ECCurve.Secp256k1); + var point2 = new ECPoint(X2, Y1, ECCurve.Secp256k1); + var point3 = new ECPoint(X1, Y2, ECCurve.Secp256k1); + Assert.IsFalse(point1.Equals(point2)); + Assert.IsFalse(point1.Equals(point3)); + } + + [TestMethod] + public void TestGetHashCode() + { + var pointA = new ECPoint(ECCurve.Secp256k1.G.X, ECCurve.Secp256k1.G.Y, ECCurve.Secp256k1); + var pointB = new ECPoint(ECCurve.Secp256k1.G.Y, ECCurve.Secp256k1.G.X, ECCurve.Secp256k1); + var pointC = new ECPoint(ECCurve.Secp256k1.G.Y, ECCurve.Secp256k1.G.X, ECCurve.Secp256r1); // different curve + var pointD = new ECPoint(ECCurve.Secp256k1.G.Y, ECCurve.Secp256k1.G.X, ECCurve.Secp256k1); + + Assert.AreNotEqual(pointA.GetHashCode(), pointB.GetHashCode()); + Assert.AreNotEqual(pointA.GetHashCode(), pointC.GetHashCode()); + Assert.AreNotEqual(pointB.GetHashCode(), pointC.GetHashCode()); + Assert.AreEqual(pointB.GetHashCode(), pointD.GetHashCode()); + } + + [TestMethod] + public void TestEqualsObject() + { + object point = ECCurve.Secp256k1.G; + Assert.IsTrue(point.Equals(point)); + Assert.IsFalse(point.Equals(null)); + Assert.IsFalse(point.Equals(1u)); + + point = new ECPoint(null, null, ECCurve.Secp256k1); + Assert.IsFalse(point.Equals(new ECPoint(null, null, ECCurve.Secp256r1))); + Assert.IsFalse(point.Equals(ECCurve.Secp256r1.G)); + Assert.IsFalse(ECCurve.Secp256r1.G.Equals(point)); + + var X1 = new ECFieldElement(new BigInteger(100), ECCurve.Secp256k1); + var Y1 = new ECFieldElement(new BigInteger(200), ECCurve.Secp256k1); + var X2 = new ECFieldElement(new BigInteger(300), ECCurve.Secp256k1); + var Y2 = new ECFieldElement(new BigInteger(400), ECCurve.Secp256k1); + object point1 = new ECPoint(X1, Y1, ECCurve.Secp256k1); + object point2 = new ECPoint(X2, Y1, ECCurve.Secp256k1); + object point3 = new ECPoint(X1, Y2, ECCurve.Secp256k1); + Assert.IsFalse(point1.Equals(point2)); + Assert.IsFalse(point1.Equals(point3)); + } + + [TestMethod] + public void TestFromBytes() + { + byte[] input1 = [0]; + Assert.ThrowsExactly(() => ECPoint.FromBytes(input1, ECCurve.Secp256k1)); + + var input2 = s_uncompressed.HexToBytes(); + Assert.AreEqual(ECCurve.Secp256k1.G, ECPoint.FromBytes(input2, ECCurve.Secp256k1)); + + var input3 = s_compressedX.HexToBytes(); + Assert.AreEqual(ECCurve.Secp256k1.G, ECPoint.FromBytes(input3, ECCurve.Secp256k1)); + Assert.AreEqual(ECCurve.Secp256k1.G, ECPoint.FromBytes(input2.Skip(1).ToArray(), ECCurve.Secp256k1)); + + var input4 = GeneratePrivateKey(72); + Assert.AreEqual( + new ECPoint( + new(BigInteger.Parse("3634473727541135791764834762056624681715094789735830699031648273128038409767"), ECCurve.Secp256k1), + new(BigInteger.Parse("18165245710263168158644330920009617039772504630129940696140050972160274286151"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.FromBytes(input4, ECCurve.Secp256k1)); + + var input5 = GeneratePrivateKey(96); + Assert.AreEqual( + new ECPoint( + new(BigInteger.Parse("1780731860627700044960722568376592200742329637303199754547598369979440671"), ECCurve.Secp256k1), + new(BigInteger.Parse("14532552714582660066924456880521368950258152170031413196862950297402215317055"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.FromBytes(input5, ECCurve.Secp256k1)); + + var input6 = GeneratePrivateKey(104); + Assert.AreEqual( + new ECPoint( + new(BigInteger.Parse("3634473727541135791764834762056624681715094789735830699031648273128038409767"), ECCurve.Secp256k1), + new(BigInteger.Parse("18165245710263168158644330920009617039772504630129940696140050972160274286151"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.FromBytes(input6, ECCurve.Secp256k1)); + } + + [TestMethod] + public void TestGetSize() + { + Assert.AreEqual(33, ECCurve.Secp256k1.G.Size); + Assert.AreEqual(1, ECCurve.Secp256k1.Infinity.Size); + } + + [TestMethod] + public void TestMultiply() + { + ECPoint p = ECCurve.Secp256k1.G; + BigInteger k = BigInteger.Parse("100"); + Assert.AreEqual( + new ECPoint( + new(BigInteger.Parse("107303582290733097924842193972465022053148211775194373671539518313500194639752"), ECCurve.Secp256k1), + new(BigInteger.Parse("103795966108782717446806684023742168462365449272639790795591544606836007446638"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.Multiply(p, k)); + + k = BigInteger.Parse("10000"); + Assert.AreEqual(new ECPoint( + new(BigInteger.Parse("55279067612272658004429375184716238028207484982037227804583126224321918234542"), ECCurve.Secp256k1), + new(BigInteger.Parse("93139664895507357192565643142424306097487832058389223752321585898830257071353"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.Multiply(p, k)); + + k = BigInteger.Parse("10000000000000"); + Assert.AreEqual(new ECPoint( + new(BigInteger.Parse("115045167963494515061513744671884131783397561769819471159495798754884242293003"), ECCurve.Secp256k1), + new(BigInteger.Parse("93759167105263077270762304290738437383691912799231615884447658154878797241853"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.Multiply(p, k)); + + k = BigInteger.Parse("1000000000000000000000000000000000000000"); + Assert.AreEqual(new ECPoint( + new(BigInteger.Parse("114831276968810911840931876895388845736099852671055832194631099067239418074350"), ECCurve.Secp256k1), + new(BigInteger.Parse("16721517996619732311261078486295444964227498319433363271180755596201863690708"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.Multiply(p, k)); + + k = new BigInteger(GeneratePrivateKey(100)); + Assert.AreEqual(new ECPoint( + new(BigInteger.Parse("19222995016448259376216431079553428738726180595337971417371897285865264889977"), ECCurve.Secp256k1), + new(BigInteger.Parse("6637081904924493791520919212064582313497884724460823966446023080706723904419"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.Multiply(p, k)); + + k = new BigInteger(GeneratePrivateKey(120)); + Assert.AreEqual(new ECPoint( + new(BigInteger.Parse("79652345192111851576650978679091010173409410384772942769927955775006682639778"), ECCurve.Secp256k1), + new(BigInteger.Parse("6460429961979335115790346961011058418773289452368186110818621539624566803831"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.Multiply(p, k)); + + k = new BigInteger(GeneratePrivateKey(300)); + Assert.AreEqual(new ECPoint( + new(BigInteger.Parse("105331914562708556186724786757483927866790351460145374033180496740107603569412"), ECCurve.Secp256k1), + new(BigInteger.Parse("60523670886755698512704385951571322569877668383890769288780681319304421873758"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), + ECPoint.Multiply(p, k)); + } + + [TestMethod] + public void TestDeserialize() + { + var point = new ECPoint(null, null, ECCurve.Secp256k1); + ISerializable serializable = point; + + var input = s_uncompressed.HexToBytes(); + var reader = new MemoryReader(input); + serializable.Deserialize(ref reader); + Assert.AreEqual(ECCurve.Secp256k1.G.X, point.X); + Assert.AreEqual(ECCurve.Secp256k1.G.Y, point.Y); + } + + [TestMethod] + public void TestSerialize() + { + var stream = new MemoryStream(); + var point = new ECPoint(null, null, ECCurve.Secp256k1); + ISerializable serializable = point; + serializable.Serialize(new BinaryWriter(stream)); + CollectionAssert.AreEqual(new byte[] { 0 }, stream.ToArray()); + + CollectionAssert.AreEqual(point.GetSpan().ToArray(), stream.ToArray()); + point = ECCurve.Secp256r1.G; + CollectionAssert.AreEqual(point.GetSpan().ToArray(), point.ToArray()); + } + + [TestMethod] + public void TestOpAddition() + { + Assert.AreEqual(ECCurve.Secp256k1.Infinity + ECCurve.Secp256k1.G, ECCurve.Secp256k1.G); + Assert.AreEqual(ECCurve.Secp256k1.G + ECCurve.Secp256k1.Infinity, ECCurve.Secp256k1.G); + Assert.AreEqual(ECCurve.Secp256k1.G + ECCurve.Secp256k1.G, new ECPoint( + new(BigInteger.Parse("89565891926547004231252920425935692360644145829622209833684329913297188986597"), ECCurve.Secp256k1), + new(BigInteger.Parse("12158399299693830322967808612713398636155367887041628176798871954788371653930"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + )); + + Assert.AreEqual(ECCurve.Secp256k1.Infinity, + ECCurve.Secp256k1.G + new ECPoint(ECCurve.Secp256k1.G.X, new(BigInteger.One, ECCurve.Secp256k1), ECCurve.Secp256k1)); + Assert.AreEqual(ECCurve.Secp256k1.G + ECCurve.Secp256k1.G + ECCurve.Secp256k1.G, new ECPoint( + new(BigInteger.Parse("112711660439710606056748659173929673102114977341539408544630613555209775888121"), ECCurve.Secp256k1), + new(BigInteger.Parse("25583027980570883691656905877401976406448868254816295069919888960541586679410"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + )); + } + + [TestMethod] + public void TestOpMultiply() + { + var p = ECCurve.Secp256k1.G; + byte[] n = [1]; + Assert.ThrowsExactly(() => p *= n); + + p = ECCurve.Secp256k1.Infinity; + n = new byte[32]; + Assert.AreEqual(p, p * n); + + p = ECCurve.Secp256k1.G; + Assert.AreEqual(ECCurve.Secp256k1.Infinity, p * n); + + n[0] = 1; + Assert.AreEqual(new ECPoint( + new(BigInteger.Parse("63395642421589016740518975608504846303065672135176650115036476193363423546538"), ECCurve.Secp256k1), + new(BigInteger.Parse("29236048674093813394523910922582374630829081423043497254162533033164154049666"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), p * n); + } + + [TestMethod] + public void TestOpSubtraction() + { + Assert.AreEqual(ECCurve.Secp256k1.G, ECCurve.Secp256k1.G - ECCurve.Secp256k1.Infinity); + Assert.AreEqual(ECCurve.Secp256k1.Infinity, ECCurve.Secp256k1.G - ECCurve.Secp256k1.G); + } + + [TestMethod] + public void TestOpUnaryNegation() + { + Assert.AreEqual(new ECPoint(ECCurve.Secp256k1.G.X, -ECCurve.Secp256k1.G.Y!, ECCurve.Secp256k1), -ECCurve.Secp256k1.G); + } + + [TestMethod] + public void TestTryParse() + { + Assert.IsFalse(ECPoint.TryParse("00", ECCurve.Secp256k1, out var result)); + Assert.IsNull(result); + + Assert.IsTrue(ECPoint.TryParse(s_uncompressed, ECCurve.Secp256k1, out result)); + Assert.AreEqual(ECCurve.Secp256k1.G, result); + } + + [TestMethod] + public void TestTwice() + { + Assert.AreEqual(ECCurve.Secp256k1.Infinity, ECCurve.Secp256k1.Infinity.Twice()); + Assert.AreEqual( + ECCurve.Secp256k1.Infinity, + new ECPoint( + new(BigInteger.Zero, ECCurve.Secp256k1), + new(BigInteger.Zero, ECCurve.Secp256k1), + ECCurve.Secp256k1 + ).Twice()); + Assert.AreEqual(new ECPoint( + new(BigInteger.Parse("89565891926547004231252920425935692360644145829622209833684329913297188986597"), ECCurve.Secp256k1), + new(BigInteger.Parse("12158399299693830322967808612713398636155367887041628176798871954788371653930"), ECCurve.Secp256k1), + ECCurve.Secp256k1 + ), ECCurve.Secp256k1.G.Twice()); + } +} + +#nullable disable diff --git a/tests/Neo.UnitTests/Cryptography/UT_Base58.cs b/tests/Neo.UnitTests/Cryptography/UT_Base58.cs new file mode 100644 index 0000000000..4f972b9fe6 --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_Base58.cs @@ -0,0 +1,71 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Base58.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_Base58 +{ + [TestMethod] + public void TestEncodeDecode() + { + var bitcoinTest = new Dictionary() + { + // Tests from https://github.com/bitcoin/bitcoin/blob/46fc4d1a24c88e797d6080336e3828e45e39c3fd/src/test/data/base58_encode_decode.json + {"", ""}, + {"61", "2g"}, + {"626262", "a3gV"}, + {"636363", "aPEr"}, + {"73696d706c792061206c6f6e6720737472696e67", "2cFupjhnEsSn59qHXstmK2ffpLv2"}, + {"00eb15231dfceb60925886b67d065299925915aeb172c06647", "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"}, + {"516b6fcd0f", "ABnLTmg"}, + {"bf4f89001e670274dd", "3SEo3LWLoPntC"}, + {"572e4794", "3EFU7m"}, + {"ecac89cad93923c02321", "EJDM8drfXA6uyA"}, + {"10c8511e", "Rt5zm"}, + {"00000000000000000000", "1111111111"}, + { + "000111d38e5fc9071ffcd20b4a763cc9ae4f252bb4e48fd66a835e252ada93ff480d6dd43dc62a641155a5", + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + }, + { + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2" + + "c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758" + + "595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283848" + + "5868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1" + + "b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcddd" + + "edfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + "1cWB5HCBdLjAuqGGReWE3R3CguuwSjw6RHn39s2yuDRTS5NsBgNiFpWgAnEx6VQi8csexkgYw3mdYrMHr8x9i7aEw" + + "P8kZ7vccXWqKDvGv3u1GxFKPuAkn8JCPPGDMf3vMMnbzm6Nh9zh1gcNsMvH3ZNLmP5fSG6DGbbi2tuwMWPthr4boW" + + "wCxf7ewSgNQeacyozhKDDQQ1qL5fQFUW52QKUZDZ5fw3KXNQJMcNTcaB723LchjeKun7MuGW5qyCBZYzA1KjofN1g" + + "YBV3NqyhQJ3Ns746GNuf9N2pQPmHz4xpnSrrfCvy6TVVz5d4PdrjeshsWQwpZsZGzvbdAdN8MKV5QsBDY" + }, + // Extra tests + {"00", "1"}, + {"00010203040506070809", "1kA3B2yGe2z4"}, + }; + + foreach (var entry in bitcoinTest) + { + Assert.AreEqual(entry.Value, Base58.Encode(entry.Key.HexToBytes())); + CollectionAssert.AreEqual(entry.Key.HexToBytes(), Base58.Decode(entry.Value)); + } + + var invalidBase58 = new string[] { "0", "O", "I", "l", "+", "/" }; + foreach (var s in invalidBase58) + { + var action = new Action(() => Base58.Decode(s)); + Assert.ThrowsExactly(action); + } + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_BloomFilter.cs b/tests/Neo.UnitTests/Cryptography/UT_BloomFilter.cs new file mode 100644 index 0000000000..6e41befc17 --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_BloomFilter.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_BloomFilter.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_BloomFilter +{ + [TestMethod] + public void TestAddCheck() + { + int m = 7, n = 10; + uint nTweak = 123456; + byte[] elements = { 0, 1, 2, 3, 4 }; + var filter = new BloomFilter(m, n, nTweak); + filter.Add(elements); + Assert.IsTrue(filter.Check(elements)); + byte[] anotherElements = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + Assert.IsFalse(filter.Check(anotherElements)); + } + + [TestMethod] + public void TestBloomFIlterConstructorGetKMTweak() + { + int m = -7, n = 10; + uint nTweak = 123456; + Assert.ThrowsExactly(() => new BloomFilter(m, n, nTweak)); + Assert.ThrowsExactly(() => new BloomFilter(m, n, nTweak, new byte[] { 0, 1, 2, 3, 4 })); + + m = 7; + n = -10; + Assert.ThrowsExactly(() => new BloomFilter(m, n, nTweak)); + + n = 10; + var filter = new BloomFilter(m, n, nTweak); + Assert.AreEqual(m, filter.M); + Assert.AreEqual(n, filter.K); + Assert.AreEqual(nTweak, filter.Tweak); + + byte[] shorterElements = { 0, 1, 2, 3, 4 }; + filter = new BloomFilter(m, n, nTweak, shorterElements); + Assert.AreEqual(m, filter.M); + Assert.AreEqual(n, filter.K); + Assert.AreEqual(nTweak, filter.Tweak); + + byte[] longerElements = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + filter = new BloomFilter(m, n, nTweak, longerElements); + Assert.AreEqual(m, filter.M); + Assert.AreEqual(n, filter.K); + Assert.AreEqual(nTweak, filter.Tweak); + } + + [TestMethod] + public void TestGetBits() + { + int m = 7, n = 10; + uint nTweak = 123456; + var filter = new BloomFilter(m, n, nTweak); + byte[] result = new byte[m]; + filter.GetBits(result); + foreach (byte value in result) + Assert.AreEqual(0, value); + } + + [TestMethod] + public void TestInvalidArguments() + { + uint nTweak = 123456; + Assert.ThrowsExactly(() => new BloomFilter(0, 3, nTweak)); + Assert.ThrowsExactly(() => new BloomFilter(3, 0, nTweak)); + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs b/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs new file mode 100644 index 0000000000..441df1de41 --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs @@ -0,0 +1,241 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Crypto.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.Wallets; +using System.Security.Cryptography; +using System.Text; +using ECCurve = Neo.Cryptography.ECC.ECCurve; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_Crypto +{ + private KeyPair _key = null!; + + public static KeyPair GenerateKey(int privateKeyLength) + { + var privateKey = new byte[privateKeyLength]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(privateKey); + } + return new KeyPair(privateKey); + } + + public static KeyPair GenerateCertainKey(int privateKeyLength) + { + var privateKey = new byte[privateKeyLength]; + for (var i = 0; i < privateKeyLength; i++) + { + privateKey[i] = (byte)((byte)i % byte.MaxValue); + } + return new KeyPair(privateKey); + } + + [TestInitialize] + public void TestSetup() + { + _key = GenerateKey(32); + } + + [TestMethod] + public void TestVerifySignature() + { + var message = Encoding.Default.GetBytes("HelloWorld"); + var signature = Crypto.Sign(message, _key.PrivateKey); + Assert.IsTrue(Crypto.VerifySignature(message, signature, _key.PublicKey)); + + var wrongKey2 = new byte[36]; + var wrongKey = new byte[33]; + wrongKey[0] = 0x02; + Assert.IsFalse(Crypto.VerifySignature(message, signature, wrongKey, ECCurve.Secp256r1)); + + wrongKey[0] = 0x03; + for (int i = 1; i < 33; i++) wrongKey[i] = byte.MaxValue; + Assert.ThrowsExactly(() => _ = Crypto.VerifySignature(message, signature, wrongKey, ECCurve.Secp256r1)); + + Assert.ThrowsExactly(() => _ = Crypto.VerifySignature(message, signature, wrongKey2, ECCurve.Secp256r1)); + } + + [TestMethod] + public void TestSecp256k1() + { + byte[] privkey = "7177f0d04c79fa0b8c91fe90c1cf1d44772d1fba6e5eb9b281a22cd3aafb51fe".HexToBytes(); + byte[] message = "2d46a712699bae19a634563d74d04cc2da497b841456da270dccb75ac2f7c4e7".HexToBytes(); + var signature = Crypto.Sign(message, privkey, ECCurve.Secp256k1); + + byte[] pubKey = ("04" + "fd0a8c1ce5ae5570fdd46e7599c16b175bf0ebdfe9c178f1ab848fb16dac74a5" + + "d301b0534c7bcf1b3760881f0c420d17084907edd771e1c9c8e941bbf6ff9108").HexToBytes(); + Assert.IsTrue(Crypto.VerifySignature(message, signature, pubKey, ECCurve.Secp256k1)); + + message = Encoding.Default.GetBytes("world"); + signature = Crypto.Sign(message, privkey, ECCurve.Secp256k1); + + Assert.IsTrue(Crypto.VerifySignature(message, signature, pubKey, ECCurve.Secp256k1)); + + message = Encoding.Default.GetBytes("中文"); + signature = ("b8cba1ff42304d74d083e87706058f59cdd4f755b995926d2cd80a734c5a3c37" + + "e4583bfd4339ac762c1c91eee3782660a6baf62cd29e407eccd3da3e9de55a02").HexToBytes(); + pubKey = "03661b86d54eb3a8e7ea2399e0db36ab65753f95fff661da53ae0121278b881ad0".HexToBytes(); + + Assert.IsTrue(Crypto.VerifySignature(message, signature, pubKey, ECCurve.Secp256k1)); + } + + [TestMethod] + public void TestECRecover() + { + // Test case 1 + var message1 = ("5c868fedb8026979ebd26f1ba07c27eedf4ff6d10443505a96ecaf21ba8c4f09" + + "37b3cd23ffdc3dd429d4cd1905fb8dbcceeff1350020e18b58d2ba70887baa3a" + + "9b783ad30d3fbf210331cdd7df8d77defa398cdacdfc2e359c7ba4cae46bb744" + + "01deb417f8b912a1aa966aeeba9c39c7dd22479ae2b30719dca2f2206c5eb4b7").HexToBytes(); + var messageHash1 = "5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0".HexToBytes(); + + var signature1 = ("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d092" + + "0d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b78801").HexToBytes(); + var expectedPubKey1 = "034a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5".HexToBytes(); + + var recoveredKey1 = Crypto.ECRecover(signature1, messageHash1); + CollectionAssert.AreEqual(expectedPubKey1, recoveredKey1.EncodePoint(true)); + + // Test case 2 + var message2 = ("17cd4a74d724d55355b6fb2b0759ca095298e3fd1856b87ca1cb2df540905802" + + "2736d21be071d820b16dfc441be97fbcea5df787edc886e759475469e2128b22" + + "f26b82ca993be6695ab190e673285d561d3b6d42fcc1edd6d12db12dcda0823e" + + "9d6079e7bc5ff54cd452dad308d52a15ce9c7edd6ef3dad6a27becd8e001e80f").HexToBytes(); + var messageHash2 = "586052916fb6f746e1d417766cceffbe1baf95579bab67ad49addaaa6e798862".HexToBytes(); + + var signature2 = ("4e0ea79d4a476276e4b067facdec7460d2c98c8a65326a6e5c998fd7c6506114" + + "0e45aea5034af973410e65cf97651b3f2b976e3fc79c6a93065ed7cb69a2ab5a01").HexToBytes(); + var expectedPubKey2 = "02dbf1f4092deb3cfd4246b2011f7b24840bc5dbedae02f28471ce5b3bfbf06e71".HexToBytes(); + + var recoveredKey2 = Crypto.ECRecover(signature2, messageHash2); + CollectionAssert.AreEqual(expectedPubKey2, recoveredKey2.EncodePoint(true)); + + // Test case 3 - recovery param 0 + var message3 = ("db0d31717b04802adbbae1997487da8773440923c09b869e12a57c36dda34af1" + + "1b8897f266cd81c02a762c6b74ea6aaf45aaa3c52867eb8f270f5092a36b498f" + + "88b65b2ebda24afe675da6f25379d1e194d093e7a2f66e450568dbdffebff97c" + + "4597a00c96a5be9ba26deefcca8761c1354429622c8db269d6a0ec0cc7a8585c").HexToBytes(); + var messageHash3 = "c36d0ecf4bfd178835c97aae7585f6a87de7dfa23cc927944f99a8d60feff68b".HexToBytes(); + + var signature3 = ("f25b86e1d8a11d72475b3ed273b0781c7d7f6f9e1dae0dd5d3ee9b84f3fab891" + + "63d9c4e1391de077244583e9a6e3d8e8e1f236a3bf5963735353b93b1a3ba93500").HexToBytes(); + var expectedPubKey3 = "03414549fd05bfb7803ae507ff86b99becd36f8d66037a7f5ba612792841d42eb9".HexToBytes(); + + var recoveredKey3 = Crypto.ECRecover(signature3, messageHash3); + CollectionAssert.AreEqual(expectedPubKey3, recoveredKey3.EncodePoint(true)); + + // Test invalid cases + var invalidSignature = new byte[65]; + Assert.ThrowsExactly(() => _ = Crypto.ECRecover(invalidSignature, messageHash1)); + + // Test with invalid recovery value + var invalidRecoverySignature = signature1.ToArray(); + invalidRecoverySignature[64] = 29; // Invalid recovery value + Assert.ThrowsExactly(() => _ = Crypto.ECRecover(invalidRecoverySignature, messageHash1)); + + // Test with wrong message hash + var recoveredWrongHash = Crypto.ECRecover(signature1, messageHash2); + CollectionAssert.AreNotEquivalent(expectedPubKey1, recoveredWrongHash.EncodePoint(true)); + } + + [TestMethod] + public void TestERC2098() + { + // Test from https://eips.ethereum.org/EIPS/eip-2098 + + // Private Key: 0x1234567890123456789012345678901234567890123456789012345678901234 + // Message: "Hello World" + // Signature: + // r: 0x68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90 + // s: 0x7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064 + // v: 27 + + var privateKey = "1234567890123456789012345678901234567890123456789012345678901234".HexToBytes(); + var expectedPubKey1 = (ECCurve.Secp256k1.G * privateKey); + + Console.WriteLine($"Expected PubKey: {expectedPubKey1.ToArray().ToHexString()}"); + var message1 = Encoding.UTF8.GetBytes("Hello World"); + var messageHash1 = new byte[] { 0x19 } + .Concat(Encoding.UTF8.GetBytes($"Ethereum Signed Message:\n{message1.Length}")) + .Concat(message1) + .ToArray() + .Keccak256(); + Console.WriteLine($"Message Hash: {Convert.ToHexString(messageHash1)}"); + + // Signature values from EIP-2098 test case + var r = "68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90".HexToBytes(); + var s = "7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064".HexToBytes(); + var signature1 = new byte[65]; + Array.Copy(r, 0, signature1, 0, 32); + Array.Copy(s, 0, signature1, 32, 32); + signature1[64] = 27; + + Console.WriteLine($"r: {Convert.ToHexString(signature1.Take(32).ToArray())}"); + Console.WriteLine($"s: {Convert.ToHexString(signature1.Skip(32).Take(32).ToArray())}"); + Console.WriteLine($"yParity: {(signature1[32] & 0x80) != 0}"); + + var recoveredKey1 = Crypto.ECRecover(signature1, messageHash1); + CollectionAssert.AreEqual(expectedPubKey1.EncodePoint(true), recoveredKey1.EncodePoint(true)); + + var sig = "68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90".HexToBytes() + .Concat("7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064".HexToBytes()) + .Concat(new byte[] { 0x1B }) + .ToArray(); + + var pubKey = Crypto.ECRecover(sig, messageHash1); + + Console.WriteLine($"Recovered PubKey: {pubKey.EncodePoint(true).ToHexString()}"); + Console.WriteLine($"Recovered PubKey: {recoveredKey1.EncodePoint(true).ToHexString()}"); + CollectionAssert.AreEqual(expectedPubKey1.EncodePoint(true), pubKey.EncodePoint(true)); + + // Private Key: 0x1234567890123456789012345678901234567890123456789012345678901234 + // Message: "It's a small(er) world" + // Signature: + // r: 0x9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76 + // s: 0x139c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793 + // v: 28 + + var message2Body = Encoding.UTF8.GetBytes("It's a small(er) world"); + var message2 = new byte[] { 0x19 } + .Concat(Encoding.UTF8.GetBytes($"Ethereum Signed Message:\n{message2Body.Length}")) + .Concat(message2Body) + .ToArray(); + var messageHash2 = message2.Keccak256(); + Console.WriteLine($"\nMessage Hash 2: {Convert.ToHexString(messageHash2)}"); + + // Second test case from EIP-2098 + var r2 = "9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76".HexToBytes(); + var s2 = "939c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793".HexToBytes(); + var signature2 = new byte[64]; + + Array.Copy(r2, 0, signature2, 0, 32); + Array.Copy(s2, 0, signature2, 32, 32); + + Console.WriteLine($"r: {Convert.ToHexString(signature2.Take(32).ToArray())}"); + Console.WriteLine($"s: {Convert.ToHexString(signature2.Skip(32).Take(32).ToArray())}"); + Console.WriteLine($"yParity: {(signature2[31] & 0x80) != 0}"); + + var recoveredKey2 = Crypto.ECRecover(signature2, messageHash2); + CollectionAssert.AreEqual(expectedPubKey1.ToArray(), recoveredKey2.EncodePoint(true)); + + // Normal signature without recovery param + var verifySig = "9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76".HexToBytes() + .Concat("139c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793".HexToBytes()) + .ToArray(); + + Assert.IsTrue(Crypto.VerifySignature(message2, verifySig, recoveredKey2, Neo.Cryptography.HashAlgorithm.Keccak256)); + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_Cryptography_Helper.cs b/tests/Neo.UnitTests/Cryptography/UT_Cryptography_Helper.cs new file mode 100644 index 0000000000..41406f3bef --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_Cryptography_Helper.cs @@ -0,0 +1,225 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Cryptography_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.Cryptography; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.Wallets; +using Neo.Wallets.NEP6; +using System.Text; +using Helper = Neo.Cryptography.Helper; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_Cryptography_Helper +{ + [TestMethod] + public void TestBase58CheckDecode() + { + string input = "3vQB7B6MrGQZaxCuFg4oh"; + var result = input.Base58CheckDecode(); + byte[] helloWorld = { 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 }; + CollectionAssert.AreEqual(helloWorld, result); + + input = "3v"; + var action = () => input.Base58CheckDecode(); + Assert.ThrowsExactly(action); + + input = "3vQB7B6MrGQZaxCuFg4og"; + action = () => input.Base58CheckDecode(); + Assert.ThrowsExactly(action); + Assert.ThrowsExactly(() => _ = string.Empty.Base58CheckDecode()); + } + + [TestMethod] + public void TestMurmurReadOnlySpan() + { + ReadOnlySpan input = "Hello, world!"u8; + byte[] input2 = input.ToArray(); + Assert.AreEqual(input2.Murmur32(0), input.Murmur32(0)); + CollectionAssert.AreEqual(input2.Murmur128(0), input.Murmur128(0)); + } + + [TestMethod] + public void TestSha256() + { + var value = Encoding.ASCII.GetBytes("hello world"); + var result = value.Sha256(0, value.Length); + Assert.AreEqual("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", result.ToHexString()); + CollectionAssert.AreEqual(result, value.Sha256()); + CollectionAssert.AreEqual(result, ((Span)value).Sha256()); + CollectionAssert.AreEqual(result, ((ReadOnlySpan)value).Sha256()); + } + + [TestMethod] + public void TestSha512() + { + var value = Encoding.ASCII.GetBytes("hello world"); + var result = value.Sha512(0, value.Length); + Assert.AreEqual("309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f", result.ToHexString()); + CollectionAssert.AreEqual(result, value.Sha512()); + CollectionAssert.AreEqual(result, ((Span)value).Sha512()); + CollectionAssert.AreEqual(result, ((ReadOnlySpan)value).Sha512()); + } + + [TestMethod] + public void TestKeccak256() + { + var input = "Hello, world!"u8.ToArray(); + var result = input.Keccak256(); + Assert.AreEqual("b6e16d27ac5ab427a7f68900ac5559ce272dc6c37c82b3e052246c82244c50e4", result.ToHexString()); + CollectionAssert.AreEqual(result, ((Span)input).Keccak256()); + CollectionAssert.AreEqual(result, ((ReadOnlySpan)input).Keccak256()); + } + + [TestMethod] + public void TestRIPEMD160() + { + ReadOnlySpan value = Encoding.ASCII.GetBytes("hello world"); + var result = value.RIPEMD160(); + Assert.AreEqual("98c615784ccb5fe5936fbc0cbe9dfdb408d92f0f", result.ToHexString()); + } + + [TestMethod] + public void TestAESEncryptAndDecrypt() + { + var wallet = new NEP6Wallet("", "1", TestProtocolSettings.Default); + wallet.CreateAccount(); + + var account = wallet.GetAccounts().ToArray()[0]; + var key = account.GetKey()!; + var random = new Random(); + var nonce = new byte[12]; + random.NextBytes(nonce); + + var cypher = Helper.AES256Encrypt(Encoding.UTF8.GetBytes("hello world"), key.PrivateKey, nonce); + var m = Helper.AES256Decrypt(cypher, key.PrivateKey); + var message2 = Encoding.UTF8.GetString(m); + Assert.AreEqual("hello world", message2); + } + + [TestMethod] + public void TestEcdhEncryptAndDecrypt() + { + var wallet = new NEP6Wallet("", "1", ProtocolSettings.Default); + wallet.CreateAccount(); + wallet.CreateAccount(); + + var account1 = wallet.GetAccounts().ToArray()[0]; + var key1 = account1.GetKey()!; + var account2 = wallet.GetAccounts().ToArray()[1]; + var key2 = account2.GetKey()!; + Console.WriteLine($"Account:{1},privatekey:{key1.PrivateKey.ToHexString()},publicKey:{key1.PublicKey.ToArray().ToHexString()}"); + Console.WriteLine($"Account:{2},privatekey:{key2.PrivateKey.ToHexString()},publicKey:{key2.PublicKey.ToArray().ToHexString()}"); + + var secret1 = Helper.ECDHDeriveKey(key1, key2.PublicKey); + var secret2 = Helper.ECDHDeriveKey(key2, key1.PublicKey); + Assert.AreEqual(secret1.ToHexString(), secret2.ToHexString()); + + var message = Encoding.ASCII.GetBytes("hello world"); + var random = new Random(); + var nonce = new byte[12]; + random.NextBytes(nonce); + + var cypher = message.AES256Encrypt(secret1, nonce); + cypher.AES256Decrypt(secret2); + Assert.AreEqual("hello world", Encoding.ASCII.GetString(cypher.AES256Decrypt(secret2))); + + Assert.ThrowsExactly(() => Helper.AES256Decrypt(new byte[11], key1.PrivateKey)); + Assert.ThrowsExactly(() => Helper.AES256Decrypt(new byte[11 + 16], key1.PrivateKey)); + } + + [TestMethod] + public void TestTest() + { + int m = 7, n = 10; + uint nTweak = 123456; + var filter = new BloomFilter(m, n, nTweak); + var tx = new Transaction() + { + Script = TestUtils.GetByteArray(32, 0x42), + SystemFee = 4200000000, + Signers = [new() { Account = Array.Empty().ToScriptHash() }], + Attributes = [], + Witnesses = [Witness.Empty] + }; + Assert.IsFalse(filter.Test(tx)); + filter.Add(tx.Witnesses[0].ScriptHash.ToArray()); + Assert.IsTrue(filter.Test(tx)); + filter.Add(tx.Hash.ToArray()); + Assert.IsTrue(filter.Test(tx)); + } + + [TestMethod] + public void TestSha3_512() + { + var data = "hello world"u8; + var hash = data.Sha3_512(); + var expected = "840006653e9ac9e95117a15c915caab81662918e925de9e004f774ff82d7079a40d4d27b1b372657c61d46d470304c88c788b3a4527ad074d1dccbee5dbaa99a"; + Assert.AreEqual(expected, hash.ToHexString()); + + hash = data.ToArray().Sha3_512(); + Assert.AreEqual(expected, hash.ToHexString()); + } + + [TestMethod] + public void TestSha3_256() + { + var data = "hello world"u8; + var hash = data.Sha3_256(); + var expected = "644bcc7e564373040999aac89e7622f3ca71fba1d972fd94a31c3bfbf24e3938"; + Assert.AreEqual(expected, hash.ToHexString()); + + hash = data.ToArray().Sha3_256(); + Assert.AreEqual(expected, hash.ToHexString()); + } + + [TestMethod] + public void TestBlake2b_512() + { + var data = "hello world"u8; + var hash = data.Blake2b_512(); + var expected = "021ced8799296ceca557832ab941a50b4a11f83478cf141f51f933f653ab9fbcc05a037cddbed06e309bf334942c4e58cdf1a46e237911ccd7fcf9787cbc7fd0"; + Assert.AreEqual(expected, hash.ToHexString()); + + hash = data.ToArray().Blake2b_512(); + Assert.AreEqual(expected, hash.ToHexString()); + + var salt = "0123456789abcdef"u8; + expected = "d986f099932b14a65ebc5a6fb1b8bff8d05b6924a4ff74d4972949b880c1f74b5ab263357f332726d98fac3cabeacf415099f1a2a9b97b66cd989ca865539640"; + hash = data.Blake2b_512(salt.ToArray()); + Assert.AreEqual(expected, hash.ToHexString()); + + Assert.ThrowsExactly(() => "abc"u8.Blake2b_512(new byte[15])); + Assert.ThrowsExactly(() => "abc"u8.Blake2b_512(new byte[17])); + } + + [TestMethod] + public void TestBlake2b_256() + { + var data = "hello world"u8; + var hash = data.Blake2b_256(); + var expected = "256c83b297114d201b30179f3f0ef0cace9783622da5974326b436178aeef610"; + Assert.AreEqual(expected, hash.ToHexString()); + + hash = data.ToArray().Blake2b_256(); + Assert.AreEqual(expected, hash.ToHexString()); + + var salt = "0123456789abcdef"u8; + hash = data.Blake2b_256(salt.ToArray()); + Assert.AreEqual("779c5f2194a9c2c03e73e3ffcf3e1508dd83cb85cd861029415ab961a755cc4e", hash.ToHexString()); + + Assert.ThrowsExactly(() => "abc"u8.Blake2b_256(new byte[15])); + Assert.ThrowsExactly(() => "abc"u8.Blake2b_256(new byte[17])); + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_Ed25519.cs b/tests/Neo.UnitTests/Cryptography/UT_Ed25519.cs new file mode 100644 index 0000000000..f7adf2cdb3 --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_Ed25519.cs @@ -0,0 +1,148 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Ed25519.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 System.Text; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_Ed25519 +{ + [TestMethod] + public void TestGenerateKeyPair() + { + byte[] keyPair = Ed25519.GenerateKeyPair(); + Assert.IsNotNull(keyPair); + Assert.HasCount(32, keyPair); + } + + [TestMethod] + public void TestGetPublicKey() + { + byte[] privateKey = Ed25519.GenerateKeyPair(); + byte[] publicKey = Ed25519.GetPublicKey(privateKey); + Assert.IsNotNull(publicKey); + Assert.HasCount(Ed25519.PublicKeySize, publicKey); + } + + [TestMethod] + public void TestSignAndVerify() + { + byte[] privateKey = Ed25519.GenerateKeyPair(); + byte[] publicKey = Ed25519.GetPublicKey(privateKey); + byte[] message = Encoding.UTF8.GetBytes("Hello, Neo!"); + + byte[] signature = Ed25519.Sign(privateKey, message); + Assert.IsNotNull(signature); + Assert.HasCount(Ed25519.SignatureSize, signature); + + bool isValid = Ed25519.Verify(publicKey, message, signature); + Assert.IsTrue(isValid); + } + + [TestMethod] + public void TestFailedVerify() + { + byte[] privateKey = Ed25519.GenerateKeyPair(); + byte[] publicKey = Ed25519.GetPublicKey(privateKey); + byte[] message = Encoding.UTF8.GetBytes("Hello, Neo!"); + + byte[] signature = Ed25519.Sign(privateKey, message); + + // Tamper with the message + byte[] tamperedMessage = Encoding.UTF8.GetBytes("Hello, Neo?"); + + bool isValid = Ed25519.Verify(publicKey, tamperedMessage, signature); + Assert.IsFalse(isValid); + + // Tamper with the signature + byte[] tamperedSignature = new byte[signature.Length]; + Array.Copy(signature, tamperedSignature, signature.Length); + tamperedSignature[0] ^= 0x01; // Flip one bit + + isValid = Ed25519.Verify(publicKey, message, tamperedSignature); + Assert.IsFalse(isValid); + + // Use wrong public key + byte[] wrongPrivateKey = Ed25519.GenerateKeyPair(); + byte[] wrongPublicKey = Ed25519.GetPublicKey(wrongPrivateKey); + + isValid = Ed25519.Verify(wrongPublicKey, message, signature); + Assert.IsFalse(isValid); + } + + [TestMethod] + public void TestInvalidPrivateKeySize() + { + byte[] invalidPrivateKey = new byte[31]; // Invalid size + Assert.ThrowsExactly(() => Ed25519.GetPublicKey(invalidPrivateKey), "Invalid private key size*"); + } + + [TestMethod] + public void TestInvalidSignatureSize() + { + byte[] message = Encoding.UTF8.GetBytes("Test message"); + byte[] invalidSignature = new byte[63]; // Invalid size + byte[] publicKey = new byte[Ed25519.PublicKeySize]; + Assert.ThrowsExactly(() => Ed25519.Verify(publicKey, message, invalidSignature), "Invalid signature size*"); + } + + [TestMethod] + public void TestInvalidPublicKeySize() + { + byte[] message = Encoding.UTF8.GetBytes("Test message"); + byte[] signature = new byte[Ed25519.SignatureSize]; + byte[] invalidPublicKey = new byte[31]; // Invalid size + Assert.ThrowsExactly(() => Ed25519.Verify(invalidPublicKey, message, signature), "Invalid public key size*"); + } + + // Test vectors from RFC 8032 (https://datatracker.ietf.org/doc/html/rfc8032) + // Section 7.1. Test Vectors for Ed25519 + + [TestMethod] + public void TestVectorCase1() + { + byte[] privateKey = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60".HexToBytes(); + byte[] publicKey = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a".HexToBytes(); + byte[] message = Array.Empty(); + byte[] signature = ("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155" + + "5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b").HexToBytes(); + + CollectionAssert.AreEqual(publicKey, Ed25519.GetPublicKey(privateKey)); + CollectionAssert.AreEqual(signature, Ed25519.Sign(privateKey, message)); + } + + [TestMethod] + public void TestVectorCase2() + { + byte[] privateKey = "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb".HexToBytes(); + byte[] publicKey = "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c".HexToBytes(); + byte[] message = Encoding.UTF8.GetBytes("r"); + byte[] signature = ("92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da" + + "085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00").HexToBytes(); + + CollectionAssert.AreEqual(publicKey, Ed25519.GetPublicKey(privateKey)); + CollectionAssert.AreEqual(signature, Ed25519.Sign(privateKey, message)); + } + + [TestMethod] + public void TestVectorCase3() + { + byte[] privateKey = "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7".HexToBytes(); + byte[] publicKey = "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025".HexToBytes(); + byte[] signature = ("6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac" + + "18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a").HexToBytes(); + byte[] message = "af82".HexToBytes(); + CollectionAssert.AreEqual(publicKey, Ed25519.GetPublicKey(privateKey)); + CollectionAssert.AreEqual(signature, Ed25519.Sign(privateKey, message)); + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_MerkleTree.cs b/tests/Neo.UnitTests/Cryptography/UT_MerkleTree.cs new file mode 100644 index 0000000000..630f353197 --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_MerkleTree.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MerkleTree.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using System.Collections; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_MerkleTree +{ + public static UInt256 GetByteArrayHash(byte[] bytes) + { + ArgumentNullException.ThrowIfNull(bytes, nameof(bytes)); + + var hash = new UInt256(Crypto.Hash256(bytes)); + return hash; + } + + [TestMethod] + public void TestBuildAndDepthFirstSearch() + { + byte[] array1 = { 0x01 }; + var hash1 = GetByteArrayHash(array1); + + byte[] array2 = { 0x02 }; + var hash2 = GetByteArrayHash(array2); + + byte[] array3 = { 0x03 }; + var hash3 = GetByteArrayHash(array3); + + UInt256[] hashes = { hash1, hash2, hash3 }; + var tree = new MerkleTree(hashes); + var hashArray = tree.ToHashArray(); + Assert.AreEqual(hash1, hashArray[0]); + Assert.AreEqual(hash2, hashArray[1]); + Assert.AreEqual(hash3, hashArray[2]); + Assert.AreEqual(hash3, hashArray[3]); + + var rootHash = MerkleTree.ComputeRoot(hashes); + var hash4 = Crypto.Hash256(hash1.ToArray().Concat(hash2.ToArray()).ToArray()); + var hash5 = Crypto.Hash256(hash3.ToArray().Concat(hash3.ToArray()).ToArray()); + var result = new UInt256(Crypto.Hash256(hash4.ToArray().Concat(hash5.ToArray()).ToArray())); + Assert.AreEqual(result, rootHash); + } + + [TestMethod] + public void TestTrim() + { + byte[] array1 = { 0x01 }; + var hash1 = GetByteArrayHash(array1); + + byte[] array2 = { 0x02 }; + var hash2 = GetByteArrayHash(array2); + + byte[] array3 = { 0x03 }; + var hash3 = GetByteArrayHash(array3); + + UInt256[] hashes = { hash1, hash2, hash3 }; + var tree = new MerkleTree(hashes); + + bool[] boolArray = { false, false, false }; + var bitArray = new BitArray(boolArray); + tree.Trim(bitArray); + var hashArray = tree.ToHashArray(); + + Assert.HasCount(1, hashArray); + var hash4 = Crypto.Hash256(hash1.ToArray().Concat(hash2.ToArray()).ToArray()); + var hash5 = Crypto.Hash256(hash3.ToArray().Concat(hash3.ToArray()).ToArray()); + var result = new UInt256(Crypto.Hash256(hash4.ToArray().Concat(hash5.ToArray()).ToArray())); + Assert.AreEqual(result, hashArray[0]); + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_MerkleTreeNode.cs b/tests/Neo.UnitTests/Cryptography/UT_MerkleTreeNode.cs new file mode 100644 index 0000000000..fe6c07c0e5 --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_MerkleTreeNode.cs @@ -0,0 +1,59 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MerkleTreeNode.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 System.Text; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_MerkleTreeNode +{ + private readonly MerkleTreeNode node = new(); + + [TestInitialize] + public void TestSetup() + { + node.Hash = null; + node.Parent = null; + node.LeftChild = null; + node.RightChild = null; + } + + [TestMethod] + public void TestConstructor() + { + byte[] byteArray = Encoding.ASCII.GetBytes("hello world"); + var hash = new UInt256(Crypto.Hash256(byteArray)); + node.Hash = hash; + + Assert.AreEqual(hash, node.Hash); + Assert.IsNull(node.Parent); + Assert.IsNull(node.LeftChild); + Assert.IsNull(node.RightChild); + } + + [TestMethod] + public void TestGetIsLeaf() + { + Assert.IsTrue(node.IsLeaf); + + var child = new MerkleTreeNode(); + node.LeftChild = child; + Assert.IsFalse(node.IsLeaf); + } + + [TestMethod] + public void TestGetIsRoot() + { + Assert.IsTrue(node.IsRoot); + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_Murmur128.cs b/tests/Neo.UnitTests/Cryptography/UT_Murmur128.cs new file mode 100644 index 0000000000..72c98d33e7 --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_Murmur128.cs @@ -0,0 +1,81 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Murmur128.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using System.Text; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_Murmur128 +{ + [TestMethod] + public void TestHashCore() + { + byte[] array = Encoding.ASCII.GetBytes("hello"); + Assert.AreEqual("0bc59d0ad25fde2982ed65af61227a0e", array.Murmur128(123u).ToHexString()); + + array = Encoding.ASCII.GetBytes("world"); + Assert.AreEqual("3d3810fed480472bd214a14023bb407f", array.Murmur128(123u).ToHexString()); + + array = Encoding.ASCII.GetBytes("hello world"); + Assert.AreEqual("e0a0632d4f51302c55e3b3e48d28795d", array.Murmur128(123u).ToHexString()); + + array = "718f952132679baa9c5c2aa0d329fd2a".HexToBytes(); + Assert.AreEqual("9b4aa747ff0cf4e41b3d96251551c8ae", array.Murmur128(123u).ToHexString()); + } + + [TestMethod] + public void TestComputeHash128() + { + var murmur128 = new Murmur128(123u); + var buffer = murmur128.ComputeHash("hello world"u8.ToArray()); + Assert.AreEqual("e0a0632d4f51302c55e3b3e48d28795d", buffer.ToHexString()); + + murmur128.Reset(); + murmur128.Append("hello "u8.ToArray()); + murmur128.Append("world"u8.ToArray()); + buffer = murmur128.GetCurrentHash(); + Assert.AreEqual("e0a0632d4f51302c55e3b3e48d28795d", buffer.ToHexString()); + + murmur128.Reset(); + murmur128.Append("hello worldhello world"u8.ToArray()); + buffer = murmur128.GetCurrentHash(); + Assert.AreEqual("76f870485d4e69f8302d4b3fad28fd39", buffer.ToHexString()); + + murmur128.Reset(); + murmur128.Append("hello world"u8.ToArray()); + murmur128.Append("hello world"u8.ToArray()); + buffer = murmur128.GetCurrentHash(); + Assert.AreEqual("76f870485d4e69f8302d4b3fad28fd39", buffer.ToHexString()); + + murmur128.Reset(); + murmur128.Append("hello worldhello "u8.ToArray()); + murmur128.Append("world"u8.ToArray()); + buffer = murmur128.GetCurrentHash(); + Assert.AreEqual("76f870485d4e69f8302d4b3fad28fd39", buffer.ToHexString()); + } + + [TestMethod] + public void TestAppend() + { + var buffer = RandomNumberFactory.NextBytes(RandomNumberFactory.NextInt32(2, 2048)); + for (int i = 0; i < 100; i++) + { + int split = RandomNumberFactory.NextInt32(1, buffer.Length - 1); + var murmur128 = new Murmur128(123u); + murmur128.Append(buffer.AsSpan(0, split)); + murmur128.Append(buffer.AsSpan(split)); + Assert.AreEqual(murmur128.GetCurrentHash().ToHexString(), buffer.Murmur128(123u).ToHexString()); + } + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_Murmur32.cs b/tests/Neo.UnitTests/Cryptography/UT_Murmur32.cs new file mode 100644 index 0000000000..c74fb2e77c --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_Murmur32.cs @@ -0,0 +1,88 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Murmur32.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using System.Buffers.Binary; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_Murmur32 +{ + [TestMethod] + public void TestHashToUInt32() + { + byte[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1 }; + Assert.AreEqual(378574820u, array.Murmur32(10u)); + } + + [TestMethod] + public void TestComputeHash() + { + var murmur3 = new Murmur32(10u); + var data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1 }; + var buffer = murmur3.ComputeHash(data); + var hash = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + Assert.AreEqual(378574820u, hash); + } + + [TestMethod] + public void TestComputeHashUInt32() + { + var murmur3 = new Murmur32(10u); + var hash = murmur3.ComputeHashUInt32("hello worldhello world"u8.ToArray()); + Assert.AreEqual(60539726u, hash); + + hash = murmur3.ComputeHashUInt32("he"u8.ToArray()); + Assert.AreEqual(972873329u, hash); + } + + [TestMethod] + public void TestAppend() + { + var murmur3 = new Murmur32(10u); + murmur3.Append("h"u8.ToArray()); + murmur3.Append("e"u8.ToArray()); + Assert.AreEqual(972873329u, murmur3.GetCurrentHashUInt32()); + + murmur3.Reset(); + murmur3.Append("hello world"u8.ToArray()); + murmur3.Append("hello world"u8.ToArray()); + Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32()); + + murmur3.Reset(); + murmur3.Append("hello worldh"u8.ToArray()); + murmur3.Append("ello world"u8.ToArray()); + Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32()); + + murmur3.Reset(); + murmur3.Append("hello worldhello world"u8.ToArray()); + murmur3.Append(""u8.ToArray()); + Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32()); + + murmur3.Reset(); + murmur3.Append(""u8.ToArray()); + murmur3.Append("hello worldhello world"u8.ToArray()); + Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32()); + + // random data, random split + var data = RandomNumberFactory.NextBytes(RandomNumberFactory.NextInt32(2, 2048)); + for (int i = 0; i < 100; i++) + { + var split = RandomNumberFactory.NextInt32(1, data.Length - 1); + murmur3.Reset(); + murmur3.Append(data.AsSpan(0, split)); + murmur3.Append(data.AsSpan(split)); + Assert.AreEqual(data.Murmur32(10u), murmur3.GetCurrentHashUInt32()); + } + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_SCrypt.cs b/tests/Neo.UnitTests/Cryptography/UT_SCrypt.cs new file mode 100644 index 0000000000..5f839c9982 --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_SCrypt.cs @@ -0,0 +1,27 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_SCrypt.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Org.BouncyCastle.Crypto.Generators; + +namespace Neo.UnitTests.Cryptography; + +[TestClass] +public class UT_SCrypt +{ + [TestMethod] + public void DeriveKeyTest() + { + int N = 32, r = 2, p = 2; + + var derivedkey = SCrypt.Generate(new byte[] { 0x01, 0x02, 0x03 }, new byte[] { 0x04, 0x05, 0x06 }, N, r, p, 64).ToHexString(); + Assert.AreEqual("b6274d3a81892c24335ab46a08ec16d040ac00c5943b212099a44b76a9b8102631ab988fa07fb35357cee7b0e3910098c0774c0e97399997676d890b2bf2bb25", derivedkey); + } +} diff --git a/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs b/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs new file mode 100644 index 0000000000..79fb68ab49 --- /dev/null +++ b/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs @@ -0,0 +1,177 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// NativeContractExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Extensions.VM; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.Extensions; + +public static class NativeContractExtensions +{ + /// + /// Deploy a contract to the blockchain. + /// + /// The snapshot used for deploying the contract. + /// The address of the contract deployer. + /// The file of the contract to be deployed. + /// The manifest of the contract to be deployed. + /// The gas fee to spend for deploying the contract in the unit of datoshi, 1 datoshi = 1e-8 GAS. + /// + /// + public static ContractState DeployContract(this DataCache snapshot, UInt160 sender, byte[] nefFile, byte[] manifest, long datoshi = 200_00000000) + { + var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", nefFile, manifest, null); + + var engine = ApplicationEngine.Create(TriggerType.Application, + sender != null ? new Transaction() { Signers = [new() { Account = sender }], Attributes = [], Witnesses = null! } : null, + snapshot, settings: TestProtocolSettings.Default, gas: datoshi); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() != VMState.HALT) + { + Exception exception = engine.FaultException!; + while (exception.InnerException != null) exception = exception.InnerException; + throw exception; + } + + var ret = (ContractState)RuntimeHelpers.GetUninitializedObject(typeof(ContractState)); + ((IInteroperable)ret).FromStackItem(engine.ResultStack.Pop()); + return ret; + } + + public static void UpdateContract(this DataCache snapshot, UInt160 callingScriptHash, byte[] nefFile, byte[] manifest) + { + var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.ContractManagement.Hash, "update", nefFile, manifest, null); + + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + // Fake calling script hash + if (callingScriptHash != null) + { + engine.CurrentContext!.GetState().NativeCallingScriptHash = callingScriptHash; + engine.CurrentContext.GetState().ScriptHash = callingScriptHash; + } + + if (engine.Execute() != VMState.HALT) + { + Exception exception = engine.FaultException!; + while (exception.InnerException != null) exception = exception.InnerException; + throw exception; + } + } + + public static void DestroyContract(this DataCache snapshot, UInt160 callingScriptHash) + { + var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.ContractManagement.Hash, "destroy"); + + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + // Fake calling script hash + if (callingScriptHash != null) + { + engine.CurrentContext!.GetState().NativeCallingScriptHash = callingScriptHash; + engine.CurrentContext.GetState().ScriptHash = callingScriptHash; + } + + if (engine.Execute() != VMState.HALT) + { + Exception exception = engine.FaultException!; + while (exception.InnerException != null) exception = exception.InnerException; + throw exception; + } + } + + public static void AddContract(this DataCache snapshot, UInt160 hash, ContractState state) + { + //key: hash, value: ContractState + var key = new KeyBuilder(NativeContract.ContractManagement.Id, 8).Add(hash); + snapshot.Add(key, new StorageItem(state)); + //key: id, value: hash + var key2 = new KeyBuilder(NativeContract.ContractManagement.Id, 12).Add(state.Id); + if (!snapshot.Contains(key2)) snapshot.Add(key2, new StorageItem(hash.ToArray())); + } + + public static void DeleteContract(this DataCache snapshot, UInt160 hash) + { + //key: hash, value: ContractState + var key = new KeyBuilder(NativeContract.ContractManagement.Id, 8).Add(hash); + var value = snapshot.TryGet(key)?.GetInteroperable(); + snapshot.Delete(key); + if (value != null) + { + //key: id, value: hash + var key2 = new KeyBuilder(NativeContract.ContractManagement.Id, 12).Add(value.Id); + snapshot.Delete(key2); + } + } + + public static StackItem? Call(this NativeContract contract, DataCache snapshot, string method, params ContractParameter[] args) + { + return Call(contract, snapshot, null, null, method, null, args); + } + + public static StackItem? Call(this NativeContract contract, DataCache snapshot, string method, ApplicationEngine.OnNotifyEvent onNotify, params ContractParameter[] args) + { + return Call(contract, snapshot, null, null, method, onNotify, args); + } + + public static StackItem? Call( + this NativeContract contract, DataCache snapshot, IVerifiable container, + Block persistingBlock, string method, params ContractParameter[] args + ) + { + return Call(contract, snapshot, container, persistingBlock, method, null, args); + } + + public static StackItem? Call( + this NativeContract contract, DataCache snapshot, IVerifiable? container, + Block? persistingBlock, string method, ApplicationEngine.OnNotifyEvent? onNotify, params ContractParameter[] args) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, container, snapshot, persistingBlock, settings: TestProtocolSettings.Default); + + if (onNotify != null) + { + engine.Notify += onNotify; + } + + return Call(contract, engine, method, args); + } + + public static StackItem? Call(this NativeContract contract, ApplicationEngine engine, string method, params ContractParameter[] args) + { + using var script = new ScriptBuilder(); + script.EmitDynamicCall(contract.Hash, method, args); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() != VMState.HALT) + { + Exception exception = engine.FaultException!; + while (exception.InnerException != null) exception = exception.InnerException; + throw exception; + } + + if (0 < engine.ResultStack.Count) + return engine.ResultStack.Pop(); + return null; + } +} diff --git a/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs b/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs new file mode 100644 index 0000000000..abfdefa12a --- /dev/null +++ b/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs @@ -0,0 +1,160 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Nep17NativeContractExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.VM; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; +using Boolean = Neo.VM.Types.Boolean; + +namespace Neo.UnitTests.Extensions; + +public static class Nep17NativeContractExtensions +{ + internal class ManualWitness : IVerifiable + { + private readonly UInt160[] _hashForVerify; + + public int Size => 0; + + public Witness[] Witnesses { get; set; } = null!; + + public ManualWitness(params UInt160[] hashForVerify) + { + _hashForVerify = hashForVerify; + } + + public void Deserialize(ref MemoryReader reader) { } + + public void DeserializeUnsigned(ref MemoryReader reader) { } + + public UInt160[] GetScriptHashesForVerifying(IReadOnlyStore? snapshot = null) => _hashForVerify; + + public void Serialize(BinaryWriter writer) { } + + public void SerializeUnsigned(BinaryWriter writer) { } + } + + public static bool Transfer(this NativeContract contract, DataCache snapshot, byte[]? from, byte[]? to, BigInteger amount, bool signFrom, Block persistingBlock) + { + return Transfer(contract, snapshot, from, to, amount, signFrom, persistingBlock, null); + } + + public static bool Transfer(this NativeContract contract, DataCache snapshot, byte[]? from, byte[]? to, BigInteger amount, bool signFrom, Block persistingBlock, object? data) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, + new ManualWitness(signFrom ? [new UInt160(from)] : []), snapshot, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(contract.Hash, "transfer", from, to, amount, data); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() == VMState.FAULT) + { + throw engine.FaultException!; + } + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetBoolean(); + } + + public static bool TransferWithTransaction(this NativeContract contract, DataCache snapshot, byte[] from, byte[] to, BigInteger amount, bool signFrom, Block persistingBlock, object data) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Transaction() { Signers = signFrom ? [new() { Account = new(from), Scopes = WitnessScope.Global }] : [], Attributes = [], Witnesses = null! }, + snapshot, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(contract.Hash, "transfer", from, to, amount, data); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() == VMState.FAULT) + { + throw engine.FaultException!; + } + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetBoolean(); + } + + public static BigInteger TotalSupply(this NativeContract contract, DataCache snapshot) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(contract.Hash, "totalSupply"); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetInteger(); + } + + public static BigInteger BalanceOf(this NativeContract contract, DataCache snapshot, byte[] account) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(contract.Hash, "balanceOf", account); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetInteger(); + } + + public static BigInteger Decimals(this NativeContract contract, DataCache snapshot) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(contract.Hash, "decimals"); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetInteger(); + } + + public static string Symbol(this NativeContract contract, DataCache snapshot) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(contract.Hash, "symbol"); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetString()!; + } +} diff --git a/tests/Neo.UnitTests/Extensions/UT_ContractStateExtensions.cs b/tests/Neo.UnitTests/Extensions/UT_ContractStateExtensions.cs new file mode 100644 index 0000000000..f888cebc71 --- /dev/null +++ b/tests/Neo.UnitTests/Extensions/UT_ContractStateExtensions.cs @@ -0,0 +1,57 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractStateExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Extensions.SmartContract; +using Neo.SmartContract.Native; + +namespace Neo.UnitTests.Extensions; + +[TestClass] +public class UT_ContractStateExtensions +{ + private NeoSystem _system = null!; + + [TestInitialize] + public void Initialize() + { + _system = TestBlockchain.GetSystem(); + } + + [TestMethod] + public void TestGetStorage() + { + var contractStorage = NativeContract.ContractManagement.FindContractStorage(_system.StoreView, NativeContract.NEO.Id); + Assert.IsNotNull(contractStorage); + + var neoContract = NativeContract.ContractManagement.GetContractById(_system.StoreView, NativeContract.NEO.Id); + Assert.IsNotNull(neoContract); + + contractStorage = neoContract.FindStorage(_system.StoreView); + + Assert.IsNotNull(contractStorage); + + contractStorage = neoContract.FindStorage(_system.StoreView, [20]); + + Assert.IsNotNull(contractStorage); + + UInt160 address = "0x9f8f056a53e39585c7bb52886418c7bed83d126b"; + var item = neoContract.GetStorage(_system.StoreView, [20, .. address.ToArray()]); + + Assert.IsNotNull(item); + Assert.AreEqual(100_000_000, item.GetInteroperable().Balance); + + // Ensure GetInteroperableClone don't change nothing + + item.GetInteroperableClone().Balance = 123; + Assert.AreEqual(100_000_000, item.GetInteroperable().Balance); + } +} diff --git a/tests/Neo.UnitTests/Extensions/UT_GasTokenExtensions.cs b/tests/Neo.UnitTests/Extensions/UT_GasTokenExtensions.cs new file mode 100644 index 0000000000..e5ce86cb3c --- /dev/null +++ b/tests/Neo.UnitTests/Extensions/UT_GasTokenExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_GasTokenExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.SmartContract; +using Neo.SmartContract.Native; + +namespace Neo.UnitTests.Extensions; + +[TestClass] +public class UT_GasTokenExtensions +{ + private NeoSystem _system = null!; + + [TestInitialize] + public void Initialize() + { + _system = TestBlockchain.GetSystem(); + } + + [TestMethod] + public void TestGetAccounts() + { + UInt160 expected = "0x9f8f056a53e39585c7bb52886418c7bed83d126b"; + + var accounts = NativeContract.GAS.GetAccounts(_system.StoreView); + var (address, balance) = accounts.FirstOrDefault(); + + Assert.AreEqual(expected, address); + Assert.AreEqual(5200000000000000, balance); + } +} diff --git a/tests/Neo.UnitTests/Extensions/UT_NeoTokenExtensions.cs b/tests/Neo.UnitTests/Extensions/UT_NeoTokenExtensions.cs new file mode 100644 index 0000000000..5e8412df58 --- /dev/null +++ b/tests/Neo.UnitTests/Extensions/UT_NeoTokenExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NeoTokenExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.SmartContract; +using Neo.SmartContract.Native; + +namespace Neo.UnitTests.Extensions; + +[TestClass] +public class UT_NeoTokenExtensions +{ + private NeoSystem _system = null!; + + [TestInitialize] + public void Initialize() + { + _system = TestBlockchain.GetSystem(); + } + + [TestMethod] + public void TestGetAccounts() + { + UInt160 expected = "0x9f8f056a53e39585c7bb52886418c7bed83d126b"; + + var accounts = NativeContract.NEO.GetAccounts(_system.StoreView); + var (address, balance) = accounts.FirstOrDefault(); + + Assert.AreEqual(expected, address); + Assert.AreEqual(100000000, balance); + } +} diff --git a/tests/Neo.UnitTests/GasTests/Fixtures/StdLib.json b/tests/Neo.UnitTests/GasTests/Fixtures/StdLib.json new file mode 100644 index 0000000000..6255549683 --- /dev/null +++ b/tests/Neo.UnitTests/GasTests/Fixtures/StdLib.json @@ -0,0 +1,15 @@ +[ + { + + "Execute": [ + { + "Script": "ERHAHwwEaXRvYQwUwO85zuDk6SXGwqBqeeFEDdhvzqxBYn1bUg==", + "Fee": 1167960 + }, + { + "Script": "DAExEcAfDARhdG9pDBTA7znO4OTpJcbCoGp54UQN2G/OrEFifVtS", + "Fee": 1047210 + } + ] + } +] diff --git a/tests/Neo.UnitTests/GasTests/GasFixturesTests.cs b/tests/Neo.UnitTests/GasTests/GasFixturesTests.cs new file mode 100644 index 0000000000..409004f998 --- /dev/null +++ b/tests/Neo.UnitTests/GasTests/GasFixturesTests.cs @@ -0,0 +1,97 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// GasFixturesTests.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Native; +using Neo.UnitTests.Extensions; +using Newtonsoft.Json; + +namespace Neo.UnitTests.GasTests; + +[TestClass] +public class GasFixturesTests +{ + [TestMethod] + public void StdLibTest() + { + TestFixture("./GasTests/Fixtures/StdLib.json"); + } + + public static void TestFixture(string file) + { + var pathFile = Path.GetFullPath(file); + var json = File.ReadAllText(pathFile); + + var store = TestBlockchain.GetTestSnapshotCache(); + var fixtures = JsonConvert.DeserializeObject(json)!; + + foreach (var fixture in fixtures) + { + var snapshot = store.CloneCache(); + + AssertFixture(fixture, snapshot); + } + } + + public static void AssertFixture(GasTestFixture fixture, DataCache snapshot) + { + // Set state + + if (fixture.PreExecution?.Storage != null) + { + foreach (var preStore in fixture.PreExecution.Storage) + { + var key = new StorageKey(Convert.FromBase64String(preStore.Key)); + var value = Convert.FromBase64String(preStore.Value); + + snapshot.Add(key, value); + } + } + + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = null!, + Index = 1, + NextConsensus = null!, + Witness = null! + }, + Transactions = null! + }; + + // Signature + + List signatures = []; + + if (fixture.Signature != null) + { + if (fixture.Signature.SignedByCommittee) + { + signatures.Add(NativeContract.NEO.GetCommitteeAddress(snapshot)); + } + } + + foreach (var execute in fixture.Execute) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness([.. signatures]), snapshot, + persistingBlock, settings: TestProtocolSettings.Default); + + engine.LoadScript(execute.Script); + Assert.AreEqual(execute.State, engine.Execute()); + Assert.AreEqual(execute.Fee, engine.FeeConsumed); + } + } +} diff --git a/tests/Neo.UnitTests/GasTests/GasTestFixture.cs b/tests/Neo.UnitTests/GasTests/GasTestFixture.cs new file mode 100644 index 0000000000..2dde5cf55f --- /dev/null +++ b/tests/Neo.UnitTests/GasTests/GasTestFixture.cs @@ -0,0 +1,43 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// GasTestFixture.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; +using System.Numerics; + +#nullable enable + +namespace Neo.UnitTests.GasTests; + +public class GasTestFixture +{ + public class SignatureData + { + public bool SignedByCommittee { get; set; } = false; + } + + public class PreExecutionData + { + public Dictionary Storage { get; set; } = []; + } + + public class NeoExecution + { + public byte[] Script { get; set; } = []; + public BigInteger Fee { get; set; } = BigInteger.Zero; + public VMState State { get; set; } = VMState.HALT; + } + + public SignatureData? Signature { get; set; } = null; + public PreExecutionData? PreExecution { get; set; } = null; + public List Execute { get; set; } = []; +} + +#nullable disable diff --git a/tests/Neo.UnitTests/GasTests/GenerateGasFixturesTests.cs b/tests/Neo.UnitTests/GasTests/GenerateGasFixturesTests.cs new file mode 100644 index 0000000000..4d3dadc23c --- /dev/null +++ b/tests/Neo.UnitTests/GasTests/GenerateGasFixturesTests.cs @@ -0,0 +1,47 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// GenerateGasFixturesTests.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.VM; +using Neo.SmartContract.Native; +using Neo.VM; +using Newtonsoft.Json; + +namespace Neo.UnitTests.GasTests; + +[TestClass] +public class GenerateGasFixturesTests +{ + [TestMethod] + public void StdLibTest() + { + var fixture = new GasTestFixture() + { + Execute = + [ + new () + { + // itoa + Script = new ScriptBuilder().EmitDynamicCall(NativeContract.StdLib.Hash, "itoa", [1]).ToArray(), + Fee = 1167960 + }, + new () + { + // atoi + Script = new ScriptBuilder().EmitDynamicCall(NativeContract.StdLib.Hash, "atoi", ["1"]).ToArray(), + Fee = 1047210 + } + ] + }; + + var json = JsonConvert.SerializeObject(fixture); + Assert.IsNotNull(json); + } +} diff --git a/tests/Neo.UnitTests/IO/Caching/UT_Cache.cs b/tests/Neo.UnitTests/IO/Caching/UT_Cache.cs new file mode 100644 index 0000000000..fa8b2baabb --- /dev/null +++ b/tests/Neo.UnitTests/IO/Caching/UT_Cache.cs @@ -0,0 +1,250 @@ +// Copyright (C) 2015-2025 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 Neo.IO.Caching; +using System.Collections; + +namespace Neo.UnitTests.IO.Caching; + +class MyCache : Cache +{ + public MyCache(int maxCapacity) : base(maxCapacity) { } + + protected override int GetKeyForItem(string item) + { + return item.GetHashCode(); + } + + protected override void OnAccess(CacheItem item) { } + + public IEnumerator MyGetEnumerator() + { + IEnumerable enumerable = this; + return enumerable.GetEnumerator(); + } +} + +class CacheDisposableEntry : IDisposable +{ + public int Key { get; set; } + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } +} + +class MyDisposableCache : Cache +{ + public MyDisposableCache(int maxCapacity) : base(maxCapacity) { } + + protected override int GetKeyForItem(CacheDisposableEntry item) + { + return item.Key; + } + + protected override void OnAccess(CacheItem item) { } + + public IEnumerator MyGetEnumerator() + { + IEnumerable enumerable = this; + return enumerable.GetEnumerator(); + } +} + +[TestClass] +public class UT_Cache +{ + MyCache cache = null!; + readonly int maxCapacity = 4; + + [TestInitialize] + public void Init() + { + cache = new MyCache(maxCapacity); + } + + [TestMethod] + public void TestCount() + { + Assert.IsEmpty(cache); + + cache.Add("hello"); + cache.Add("world"); + Assert.HasCount(2, cache); + + cache.Remove("hello"); + Assert.HasCount(1, cache); + } + + [TestMethod] + public void TestIsReadOnly() + { + Assert.IsFalse(cache.IsReadOnly); + } + + [TestMethod] + public void TestAddAndAddInternal() + { + cache.Add("hello"); + Assert.Contains("hello", cache); + Assert.DoesNotContain("world", cache); + cache.Add("hello"); + Assert.HasCount(1, cache); + } + + [TestMethod] + public void TestAddRange() + { + string[] range = { "hello", "world" }; + cache.AddRange(range); + Assert.HasCount(2, cache); + Assert.Contains("hello", cache); + Assert.Contains("world", cache); + Assert.DoesNotContain("non exist string", cache); + } + + [TestMethod] + public void TestClear() + { + cache.Add("hello"); + cache.Add("world"); + Assert.HasCount(2, cache); + cache.Clear(); + Assert.IsEmpty(cache); + } + + [TestMethod] + public void TestContainsKey() + { + cache.Add("hello"); + Assert.Contains("hello", cache); + Assert.DoesNotContain("world", cache); + } + + [TestMethod] + public void TestContainsValue() + { + cache.Add("hello"); + Assert.Contains("hello", cache); + Assert.DoesNotContain("world", cache); + } + + [TestMethod] + public void TestCopyTo() + { + cache.Add("hello"); + cache.Add("world"); + string[] temp = new string[2]; + + Action action = () => cache.CopyTo(temp, -1); + Assert.ThrowsExactly(() => action()); + + action = () => cache.CopyTo(temp, 1); + Assert.ThrowsExactly(() => action()); + + cache.CopyTo(temp, 0); + Assert.AreEqual("hello", temp[0]); + Assert.AreEqual("world", temp[1]); + } + + [TestMethod] + public void TestRemoveKey() + { + cache.Add("hello"); + Assert.IsTrue(cache.Remove("hello".GetHashCode())); + Assert.IsFalse(cache.Remove("world".GetHashCode())); + Assert.DoesNotContain("hello", cache); + } + + [TestMethod] + public void TestRemoveDisposableKey() + { + var entry = new CacheDisposableEntry() { Key = 1 }; + var dcache = new MyDisposableCache(100) + { + entry + }; + + Assert.IsFalse(entry.IsDisposed); + Assert.IsTrue(dcache.Remove(entry.Key)); + Assert.IsFalse(dcache.Remove(entry.Key)); + Assert.IsTrue(entry.IsDisposed); + } + + [TestMethod] + public void TestRemoveValue() + { + cache.Add("hello"); + Assert.IsTrue(cache.Remove("hello")); + Assert.IsFalse(cache.Remove("world")); + Assert.DoesNotContain("hello", cache); + } + + [TestMethod] + public void TestTryGet() + { + cache.Add("hello"); + Assert.IsTrue(cache.TryGet("hello".GetHashCode(), out string? output)); + Assert.AreEqual("hello", output); + Assert.IsFalse(cache.TryGet("world".GetHashCode(), out string? output2)); + Assert.IsNull(output2); + } + + [TestMethod] + public void TestArrayIndexAccess() + { + cache.Add("hello"); + cache.Add("world"); + Assert.AreEqual("hello", cache["hello".GetHashCode()]); + Assert.AreEqual("world", cache["world".GetHashCode()]); + Assert.ThrowsExactly(() => cache["non exist string".GetHashCode()]); + } + + [TestMethod] + public void TestGetEnumerator() + { + cache.Add("hello"); + cache.Add("world"); + int i = 0; + foreach (string item in cache) + { + if (i == 0) Assert.AreEqual("hello", item); + if (i == 1) Assert.AreEqual("world", item); + i++; + } + Assert.AreEqual(2, i); + Assert.IsNotNull(cache.MyGetEnumerator()); + } + + [TestMethod] + public void TestOverMaxCapacity() + { + int i = 1; + cache = new MyCache(maxCapacity); + for (; i <= maxCapacity; i++) + { + cache.Add(i.ToString()); + } + cache.Add(i.ToString()); // The first one will be deleted + Assert.AreEqual(maxCapacity, cache.Count); + Assert.Contains((maxCapacity + 1).ToString(), cache); + } + + [TestMethod] + public void TestDispose() + { + cache.Add("hello"); + cache.Add("world"); + cache.Dispose(); + } +} diff --git a/tests/Neo.UnitTests/IO/Caching/UT_ECPointCache.cs b/tests/Neo.UnitTests/IO/Caching/UT_ECPointCache.cs new file mode 100644 index 0000000000..3cdcd152fe --- /dev/null +++ b/tests/Neo.UnitTests/IO/Caching/UT_ECPointCache.cs @@ -0,0 +1,35 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ECPointCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; + +namespace Neo.UnitTests.IO.Caching; + +[TestClass] +public class UT_ECPointCache +{ + ECPointCache relayCache = null!; + + [TestInitialize] + public void SetUp() + { + relayCache = new ECPointCache(10); + } + + [TestMethod] + public void TestGetKeyForItem() + { + relayCache.Add(ECCurve.Secp256r1.G); + Assert.Contains(ECCurve.Secp256r1.G, relayCache); + Assert.IsTrue(relayCache.TryGet(ECCurve.Secp256r1.G.EncodePoint(true), out _)); + } +} diff --git a/tests/Neo.UnitTests/IO/Caching/UT_HashSetCache.cs b/tests/Neo.UnitTests/IO/Caching/UT_HashSetCache.cs new file mode 100644 index 0000000000..d21a71903b --- /dev/null +++ b/tests/Neo.UnitTests/IO/Caching/UT_HashSetCache.cs @@ -0,0 +1,147 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_HashSetCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; +using System.Collections; + +namespace Neo.UnitTests.IO.Caching; + +[TestClass] +public class UT_HashSetCache +{ + [TestMethod] + public void TestHashSetCache() + { + var bucket = new HashSetCache(100); + for (var i = 1; i <= 100; i++) + { + Assert.IsTrue(bucket.TryAdd(i)); + Assert.IsFalse(bucket.TryAdd(i)); + } + Assert.HasCount(100, bucket); + + var sum = 0; + foreach (var ele in bucket) + { + sum += ele; + } + Assert.AreEqual(5050, sum); + + bucket.TryAdd(101); + Assert.HasCount(100, bucket); + + var items = new int[10]; + var value = 11; + for (var i = 0; i < 10; i++) + { + items[i] = value; + value += 2; + } + bucket.ExceptWith(items); + Assert.HasCount(90, bucket); + + Assert.DoesNotContain(13, bucket); + Assert.Contains(50, bucket); + } + + [TestMethod] + public void TestConstructor() + { + Assert.ThrowsExactly(() => new HashSetCache(-1)); + } + + [TestMethod] + public void TestAdd() + { + var key1 = Enumerable.Repeat((byte)1, 32).ToArray(); + var a = new UInt256(key1); + + var key2 = Enumerable.Repeat((byte)1, 31).Append((byte)2).ToArray(); + var b = new UInt256(key2); + + var set = new HashSetCache(1); + Assert.IsTrue(set.TryAdd(a)); + Assert.IsTrue(set.TryAdd(b)); + CollectionAssert.AreEqual(set.ToArray(), new UInt256[] { b }); + } + + [TestMethod] + public void TestCopyTo() + { + var key1 = Enumerable.Repeat((byte)1, 32).ToArray(); + var a = new UInt256(key1); + + var key2 = Enumerable.Repeat((byte)1, 31).Append((byte)2).ToArray(); + var b = new UInt256(key2); + + var set = new HashSetCache(1); + Assert.IsTrue(set.TryAdd(a)); + Assert.IsTrue(set.TryAdd(b)); + + var array = new UInt256[1]; + set.CopyTo(array, 0); + + CollectionAssert.AreEqual(array, new UInt256[] { b }); + } + + [TestMethod] + public void TestGetEnumerator() + { + var key1 = Enumerable.Repeat((byte)1, 32).ToArray(); + var a = new UInt256(key1); + + var key2 = Enumerable.Repeat((byte)1, 31).Append((byte)2).ToArray(); + var b = new UInt256(key2); + + var set = new HashSetCache(1); + set.TryAdd(a); + set.Add(b); + IEnumerable ie = set; + Assert.IsNotNull(ie.GetEnumerator()); + } + + [TestMethod] + public void TestExceptWith() + { + var key1 = Enumerable.Repeat((byte)1, 32).ToArray(); + var a = new UInt256(key1); + + var key2 = Enumerable.Repeat((byte)1, 31).Append((byte)2).ToArray(); + var b = new UInt256(key2); + + var key3 = Enumerable.Repeat((byte)1, 31).Append((byte)3).ToArray(); + var c = new UInt256(key3); + + var set = new HashSetCache(10); + set.TryAdd(a); + set.TryAdd(b); + set.TryAdd(c); + set.ExceptWith([b, c]); + CollectionAssert.AreEqual(set.ToArray(), new UInt256[] { a }); + + set.Remove(a); + CollectionAssert.AreEqual(set.ToArray(), Array.Empty()); + + set = new HashSetCache(10); + set.TryAdd(a); + set.TryAdd(b); + set.TryAdd(c); + set.ExceptWith([a]); + CollectionAssert.AreEqual(set.ToArray(), new UInt256[] { b, c }); + + set = new HashSetCache(10); + set.TryAdd(a); + set.TryAdd(b); + set.TryAdd(c); + set.ExceptWith([c]); + CollectionAssert.AreEqual(set.ToArray(), new UInt256[] { a, b }); + } +} diff --git a/tests/Neo.UnitTests/IO/Caching/UT_IndexedQueue.cs b/tests/Neo.UnitTests/IO/Caching/UT_IndexedQueue.cs new file mode 100644 index 0000000000..6347cfd7f3 --- /dev/null +++ b/tests/Neo.UnitTests/IO/Caching/UT_IndexedQueue.cs @@ -0,0 +1,116 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_IndexedQueue.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; + +namespace Neo.UnitTests.IO.Caching; + +[TestClass] +public class UT_IndexedQueue +{ + [TestMethod] + public void TestDefault() + { + var queue = new IndexedQueue(10); + Assert.IsEmpty(queue); + + queue = new IndexedQueue(); + Assert.IsEmpty(queue); + queue.TrimExcess(); + Assert.IsEmpty(queue); + + queue = new IndexedQueue(Array.Empty()); + Assert.IsEmpty(queue); + Assert.IsFalse(queue.TryPeek(out var a)); + Assert.AreEqual(0, a); + Assert.IsFalse(queue.TryDequeue(out a)); + Assert.AreEqual(0, a); + + Assert.ThrowsExactly(() => _ = queue.Peek()); + Assert.ThrowsExactly(() => _ = queue.Dequeue()); + Assert.ThrowsExactly(() => _ = _ = queue[-1]); + Assert.ThrowsExactly(() => _ = queue[-1] = 1); + Assert.ThrowsExactly(() => _ = _ = queue[1]); + Assert.ThrowsExactly(() => _ = queue[1] = 1); + Assert.ThrowsExactly(() => _ = new IndexedQueue(-1)); + } + + [TestMethod] + public void TestQueue() + { + var queue = new IndexedQueue([1, 2, 3]); + Assert.HasCount(3, queue); + + queue.Enqueue(4); + Assert.HasCount(4, queue); + Assert.AreEqual(1, queue.Peek()); + Assert.IsTrue(queue.TryPeek(out var a)); + Assert.AreEqual(1, a); + + Assert.AreEqual(1, queue[0]); + Assert.AreEqual(2, queue[1]); + Assert.AreEqual(3, queue[2]); + Assert.AreEqual(1, queue.Dequeue()); + Assert.AreEqual(2, queue.Dequeue()); + Assert.AreEqual(3, queue.Dequeue()); + queue[0] = 5; + Assert.IsTrue(queue.TryDequeue(out a)); + Assert.AreEqual(5, a); + + queue.Enqueue(4); + queue.Clear(); + Assert.IsEmpty(queue); + } + + [TestMethod] + public void TestEnumerator() + { + int[] arr = new int[3] { 1, 2, 3 }; + var queue = new IndexedQueue(arr); + + Assert.IsTrue(arr.SequenceEqual(queue)); + } + + [TestMethod] + public void TestCopyTo() + { + int[] arr = new int[3]; + var queue = new IndexedQueue([1, 2, 3]); + + Assert.ThrowsExactly(() => queue.CopyTo(arr, -1)); + Assert.ThrowsExactly(() => queue.CopyTo(arr, 2)); + + queue.CopyTo(arr, 0); + + Assert.AreEqual(1, arr[0]); + Assert.AreEqual(2, arr[1]); + Assert.AreEqual(3, arr[2]); + + arr = queue.ToArray(); + + Assert.AreEqual(1, arr[0]); + Assert.AreEqual(2, arr[1]); + Assert.AreEqual(3, arr[2]); + } + + [TestMethod] + public void TestQueueClass() + { + var q = new IndexedQueue([1, 2]); + var item = q.Dequeue(); + Assert.AreEqual(1, item); + + item = q.Dequeue(); + Assert.AreEqual(2, item); + + Assert.ThrowsExactly(() => q.Dequeue()); + } +} diff --git a/tests/Neo.UnitTests/IO/Caching/UT_KeyedCollectionSlim.cs b/tests/Neo.UnitTests/IO/Caching/UT_KeyedCollectionSlim.cs new file mode 100644 index 0000000000..c1fd1732a1 --- /dev/null +++ b/tests/Neo.UnitTests/IO/Caching/UT_KeyedCollectionSlim.cs @@ -0,0 +1,104 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_KeyedCollectionSlim.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; + +namespace Neo.UnitTests.IO.Caching; + +[TestClass] +public class UT_KeyedCollectionSlim +{ + [TestMethod] + public void Add_ShouldAddItem() + { + var collection = new TestKeyedCollectionSlim(); + var item = new TestItem { Id = 1, Name = "Item1" }; + var ok = collection.TryAdd(item); + Assert.IsTrue(ok); + Assert.HasCount(1, collection); + Assert.Contains(item, collection); + Assert.AreEqual(item, collection.FirstOrDefault); + } + + [TestMethod] + public void AddTest() + { + // Arrange + var collection = new TestKeyedCollectionSlim(); + var item1 = new TestItem { Id = 1, Name = "Item1" }; + var item2 = new TestItem { Id = 1, Name = "Item2" }; // Same ID as item1 + + var ok = collection.TryAdd(item1); + Assert.IsTrue(ok); + + ok = collection.TryAdd(item2); + Assert.IsFalse(ok); + + collection.Clear(); + Assert.IsEmpty(collection); + Assert.IsNull(collection.FirstOrDefault); + } + + [TestMethod] + public void Remove_ShouldRemoveItem() + { + // Arrange + var collection = new TestKeyedCollectionSlim(); + var item = new TestItem { Id = 1, Name = "Item1" }; + collection.TryAdd(item); + + // Act + var ok = collection.Remove(1); + + // Assert + Assert.IsTrue(ok); + Assert.IsEmpty(collection); + Assert.DoesNotContain(item, collection); + } + + [TestMethod] + public void RemoveFirst_ShouldRemoveFirstItem() + { + // Arrange + var collection = new TestKeyedCollectionSlim(); + var item1 = new TestItem { Id = 1, Name = "Item1" }; + var item2 = new TestItem { Id = 2, Name = "Item2" }; + collection.TryAdd(item1); + collection.TryAdd(item2); + + // Act + Assert.IsTrue(collection.RemoveFirst()); + + // Assert + Assert.HasCount(1, collection); + Assert.DoesNotContain(item1, collection); + Assert.Contains(item2, collection); + } + + public class TestItem + { + public int Id { get; set; } + + public required string Name { get; set; } + + public override int GetHashCode() => Id; + + public override bool Equals(object? obj) => obj is TestItem item && Id == item.Id; + } + + internal class TestKeyedCollectionSlim : KeyedCollectionSlim + { + protected override int GetKeyForItem(TestItem item) + { + return item?.Id ?? throw new ArgumentNullException(nameof(item), "Item cannot be null"); + } + } +} diff --git a/tests/Neo.UnitTests/IO/Caching/UT_LRUCache.cs b/tests/Neo.UnitTests/IO/Caching/UT_LRUCache.cs new file mode 100644 index 0000000000..6db983e0a4 --- /dev/null +++ b/tests/Neo.UnitTests/IO/Caching/UT_LRUCache.cs @@ -0,0 +1,70 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_LRUCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; + +namespace Neo.UnitTests.IO.Caching; + +class DemoLRUCache : LRUCache +{ + public DemoLRUCache(int maxCapacity) : base(maxCapacity) { } + + protected override int GetKeyForItem(string item) => item.GetHashCode(); +} + +[TestClass] +public class UT_LRUCache +{ + [TestMethod] + public void TestLRUCache() + { + var cache = new DemoLRUCache(3); + Assert.IsEmpty(cache); + + var key2 = "2".GetHashCode(); + + cache.Add("1"); + cache.Add("2"); + cache.Add("3"); + Assert.HasCount(3, cache); + Assert.Contains("1", cache); + Assert.Contains("2", cache); + Assert.Contains("3", cache); + Assert.DoesNotContain("4", cache); + + var cached = cache[key2]; + Assert.AreEqual("2", cached); + Assert.HasCount(3, cache); + Assert.Contains("1", cache); + Assert.Contains("2", cache); + Assert.Contains("3", cache); + Assert.DoesNotContain("4", cache); + + cache.Add("4"); + Assert.HasCount(3, cache); + Assert.Contains("3", cache); + Assert.Contains("2", cache); + Assert.Contains("4", cache); + Assert.DoesNotContain("1", cache); + + cache.Add("5"); + Assert.HasCount(3, cache); + Assert.DoesNotContain("1", cache); + Assert.Contains("2", cache); + Assert.DoesNotContain("3", cache); + Assert.Contains("4", cache); + Assert.Contains("5", cache); + + cache.Add("6"); + Assert.HasCount(3, cache); + Assert.Contains("5", cache); + } +} diff --git a/tests/Neo.UnitTests/IO/Caching/UT_ReflectionCache.cs b/tests/Neo.UnitTests/IO/Caching/UT_ReflectionCache.cs new file mode 100644 index 0000000000..b76f3ca377 --- /dev/null +++ b/tests/Neo.UnitTests/IO/Caching/UT_ReflectionCache.cs @@ -0,0 +1,84 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ReflectionCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO.Caching; + +namespace Neo.UnitTests.IO.Caching; + +public class TestItem : ISerializable +{ + public int Size => 0; + public void Deserialize(ref MemoryReader reader) { } + public void Serialize(BinaryWriter writer) { } +} + +public class TestItem1 : TestItem { } + +public class TestItem2 : TestItem { } + +public enum MyTestEnum : byte +{ + [ReflectionCache(typeof(TestItem1))] + Item1 = 0x00, + + [ReflectionCache(typeof(TestItem2))] + Item2 = 0x01, +} + +public enum MyEmptyEnum : byte { } + +[TestClass] +public class UT_ReflectionCache +{ + [TestMethod] + public void TestCreateFromEmptyEnum() + { + Assert.AreEqual(0, ReflectionCache.Count); + } + + [TestMethod] + public void TestCreateInstance() + { + object? item1 = ReflectionCache.CreateInstance(MyTestEnum.Item1, null); + Assert.IsTrue(item1 is TestItem1); + + object? item2 = ReflectionCache.CreateInstance(MyTestEnum.Item2, null); + Assert.IsTrue(item2 is TestItem2); + + object? item3 = ReflectionCache.CreateInstance((MyTestEnum)0x02, null); + Assert.IsNull(item3); + } + + [TestMethod] + public void TestCreateSerializable() + { + object? item1 = ReflectionCache.CreateSerializable(MyTestEnum.Item1, ReadOnlyMemory.Empty); + Assert.IsTrue(item1 is TestItem1); + + object? item2 = ReflectionCache.CreateSerializable(MyTestEnum.Item2, ReadOnlyMemory.Empty); + Assert.IsTrue(item2 is TestItem2); + + object? item3 = ReflectionCache.CreateSerializable((MyTestEnum)0x02, ReadOnlyMemory.Empty); + Assert.IsNull(item3); + } + + [TestMethod] + public void TestCreateInstance2() + { + TestItem defaultItem = new TestItem1(); + object? item2 = ReflectionCache.CreateInstance(MyTestEnum.Item2, defaultItem); + Assert.IsTrue(item2 is TestItem2); + + object? item1 = ReflectionCache.CreateInstance((MyTestEnum)0x02, new TestItem1()); + Assert.IsTrue(item1 is TestItem1); + } +} diff --git a/tests/Neo.UnitTests/IO/Caching/UT_RelayCache.cs b/tests/Neo.UnitTests/IO/Caching/UT_RelayCache.cs new file mode 100644 index 0000000000..2a7d03f2ee --- /dev/null +++ b/tests/Neo.UnitTests/IO/Caching/UT_RelayCache.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_RelayCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Caching; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.IO.Caching; + +[TestClass] +public class UT_RelayCache +{ + RelayCache relayCache = null!; + + [TestInitialize] + public void SetUp() + { + relayCache = new RelayCache(10); + } + + [TestMethod] + public void TestGetKeyForItem() + { + var tx = new Transaction() + { + Version = 0, + Nonce = 1, + SystemFee = 0, + NetworkFee = 0, + ValidUntilBlock = 100, + Attributes = Array.Empty(), + Signers = Array.Empty(), + Script = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, + Witnesses = Array.Empty() + }; + relayCache.Add(tx); + Assert.Contains(tx, relayCache); + Assert.IsTrue(relayCache.TryGet(tx.Hash, out IInventory? tmp)); + Assert.IsTrue(tmp is Transaction); + } +} diff --git a/tests/Neo.UnitTests/IO/UT_IOHelper.cs b/tests/Neo.UnitTests/IO/UT_IOHelper.cs new file mode 100644 index 0000000000..144de36682 --- /dev/null +++ b/tests/Neo.UnitTests/IO/UT_IOHelper.cs @@ -0,0 +1,395 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_IOHelper.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.IO; +using System.Text; + +namespace Neo.UnitTests.IO; + +[TestClass] +public class UT_IOHelper +{ + [TestMethod] + public void TestAsSerializableGeneric() + { + var caseArray = Enumerable.Repeat((byte)0x00, 20).ToArray(); + var result = caseArray.AsSerializable(); + Assert.AreEqual(UInt160.Zero, result); + } + + [TestMethod] + public void TestReadFixedBytes() + { + byte[] data = [0x01, 0x02, 0x03, 0x04]; + + // Less data + using (var reader = new BinaryReader(new MemoryStream(data), Encoding.UTF8, false)) + { + var result = reader.ReadFixedBytes(3); + + Assert.AreEqual("010203", result.ToHexString()); + Assert.AreEqual(3, reader.BaseStream.Position); + } + + // Same data + using (var reader = new BinaryReader(new MemoryStream(data), Encoding.UTF8, false)) + { + var result = reader.ReadFixedBytes(4); + + Assert.AreEqual("01020304", result.ToHexString()); + Assert.AreEqual(4, reader.BaseStream.Position); + } + + // More data + using (var reader = new BinaryReader(new MemoryStream(data), Encoding.UTF8, false)) + { + Assert.ThrowsExactly(() => _ = reader.ReadFixedBytes(5)); + Assert.AreEqual(4, reader.BaseStream.Position); + } + } + + [TestMethod] + public void TestNullableArray() + { + var caseArray = new UInt160?[] + { + null, + UInt160.Zero, + new( + [ + 0xAA,0x00,0x00,0x00,0x00, + 0xBB,0x00,0x00,0x00,0x00, + 0xCC,0x00,0x00,0x00,0x00, + 0xDD,0x00,0x00,0x00,0x00 + ]) + }; + + byte[] data; + using var stream = new MemoryStream(); + using var writter = new BinaryWriter(stream); + { + writter.WriteNullableArray(caseArray); + data = stream.ToArray(); + } + + // Read Error + Assert.ThrowsExactly(() => MemoryReaderReadNullableArray(data, 2)); + + // Read 100% + var reader = new MemoryReader(data); + var read = reader.ReadNullableArray(); + CollectionAssert.AreEqual(caseArray, read); + + static void MemoryReaderReadNullableArray(byte[] data, int count) + { + var reader = new MemoryReader(data); + reader.ReadNullableArray(count); + } + } + + [TestMethod] + public void TestAsSerializable() + { + var caseArray = Enumerable.Repeat((byte)0x00, 20).ToArray(); + var result = caseArray.AsSerializable(); + Assert.AreEqual(UInt160.Zero, result); + } + + [TestMethod] + public void TestCompression() + { + var data = new byte[] { 1, 2, 3, 4 }; + var byteArray = data.CompressLz4(); + var result = byteArray.Span.DecompressLz4(byte.MaxValue); + + CollectionAssert.AreEqual(result, data); + + // Compress + + data = new byte[255]; + for (int x = 0; x < data.Length; x++) data[x] = 1; + + byteArray = data.CompressLz4(); + result = byteArray.Span.DecompressLz4(byte.MaxValue); + + Assert.IsLessThan(result.Length, byteArray.Length); + CollectionAssert.AreEqual(result, data); + + // Error max length + + Assert.ThrowsExactly(() => _ = byteArray.Span.DecompressLz4(byte.MaxValue - 1)); + Assert.ThrowsExactly(() => _ = byteArray.Span.DecompressLz4(-1)); + + // Error length + + byte[] data_wrong = byteArray.ToArray(); + data_wrong[0]++; + Assert.ThrowsExactly(() => _ = data_wrong.DecompressLz4(byte.MaxValue)); + } + + [TestMethod] + public void TestAsSerializableArray() + { + byte[] byteArray = new UInt160[] { UInt160.Zero }.ToByteArray(); + UInt160[] result = byteArray.AsSerializableArray(); + Assert.HasCount(1, result); + Assert.AreEqual(UInt160.Zero, result[0]); + } + + [TestMethod] + public void TestReadSerializable() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.Write(UInt160.Zero); + + var reader = new MemoryReader(stream.ToArray()); + var result = reader.ReadSerializable(); + Assert.AreEqual(UInt160.Zero, result); + } + + [TestMethod] + public void TestReadSerializableArray() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.Write([UInt160.Zero]); + + var reader = new MemoryReader(stream.ToArray()); + var resultArray = reader.ReadSerializableArray(); + Assert.HasCount(1, resultArray); + Assert.AreEqual(UInt160.Zero, resultArray[0]); + } + + [TestMethod] + public void TestReadVarBytes() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarBytes([0xAA, 0xAA]); + stream.Seek(0, SeekOrigin.Begin); + + var reader = new BinaryReader(stream); + var byteArray = reader.ReadVarBytes(10); + Assert.AreEqual(Encoding.Default.GetString([0xAA, 0xAA]), Encoding.Default.GetString(byteArray)); + } + + [TestMethod] + public void TestReadVarInt() + { + for (int i = 0; i < 4; i++) + { + if (i == 0) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarInt(0xFFFF); + stream.Seek(0, SeekOrigin.Begin); + + var reader = new BinaryReader(stream); + var result = reader.ReadVarInt(0xFFFF); + Assert.AreEqual((ulong)0xFFFF, result); + } + else if (i == 1) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarInt(0xFFFFFFFF); + stream.Seek(0, SeekOrigin.Begin); + var reader = new BinaryReader(stream); + var result = reader.ReadVarInt(0xFFFFFFFF); + Assert.AreEqual(0xFFFFFFFF, result); + } + else + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarInt(0xFFFFFFFFFF); + stream.Seek(0, SeekOrigin.Begin); + + var reader = new BinaryReader(stream); + Assert.ThrowsExactly(() => reader.ReadVarInt(0xFFFFFFFF)); + } + } + } + + [TestMethod] + public void TestToArray() + { + var byteArray = UInt160.Zero.ToArray(); + Assert.AreEqual(Encoding.Default.GetString(Enumerable.Repeat((byte)0x00, 20).ToArray()), Encoding.Default.GetString(byteArray)); + } + + [TestMethod] + public void TestToByteArrayGeneric() + { + var byteArray = new UInt160[] { UInt160.Zero }.ToByteArray(); + var expected = Enumerable.Repeat((byte)0x00, 21).ToArray(); + expected[0] = 0x01; + Assert.AreEqual(Encoding.Default.GetString(expected), Encoding.Default.GetString(byteArray)); + } + + [TestMethod] + public void TestWrite() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.Write(UInt160.Zero); + stream.Seek(0, SeekOrigin.Begin); + + var byteArray = new byte[stream.Length]; + stream.Read(byteArray, 0, (int)stream.Length); + Assert.AreEqual(Encoding.Default.GetString(Enumerable.Repeat((byte)0x00, 20).ToArray()), Encoding.Default.GetString(byteArray)); + } + + [TestMethod] + public void TestWriteGeneric() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.Write([UInt160.Zero]); + stream.Seek(0, SeekOrigin.Begin); + + var byteArray = new byte[stream.Length]; + stream.Read(byteArray, 0, (int)stream.Length); + + var expected = Enumerable.Repeat((byte)0x00, 21).ToArray(); + expected[0] = 0x01; + Assert.AreEqual(Encoding.Default.GetString(expected), Encoding.Default.GetString(byteArray)); + } + + [TestMethod] + public void TestWriteFixedString() + { + for (int i = 0; i < 5; i++) + { + if (i == 1) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + Assert.ThrowsExactly(() => writer.WriteFixedString("AA", Encoding.UTF8.GetBytes("AA").Length - 1)); + } + else if (i == 2) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + Assert.ThrowsExactly(() => writer.WriteFixedString("拉拉", Encoding.UTF8.GetBytes("拉拉").Length - 1)); + } + else if (i == 3) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteFixedString("AA", Encoding.UTF8.GetBytes("AA").Length + 1); + stream.Seek(0, SeekOrigin.Begin); + + var byteArray = new byte[stream.Length]; + stream.Read(byteArray, 0, (int)stream.Length); + + var expected = new byte[Encoding.UTF8.GetBytes("AA").Length + 1]; + Encoding.UTF8.GetBytes("AA").CopyTo(expected, 0); + Assert.AreEqual(Encoding.Default.GetString(expected), Encoding.Default.GetString(byteArray)); + } + } + } + + [TestMethod] + public void TestWriteVarBytes() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarBytes([0xAA]); + stream.Seek(0, SeekOrigin.Begin); + + var byteArray = new byte[stream.Length]; + stream.Read(byteArray, 0, (int)stream.Length); + Assert.AreEqual(Encoding.Default.GetString([0x01, 0xAA]), Encoding.Default.GetString(byteArray)); + } + + [TestMethod] + public void TestWriteVarInt() + { + for (int i = 0; i < 5; i++) + { + if (i == 0) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + Assert.ThrowsExactly(() => writer.WriteVarInt(-1)); + } + else if (i == 1) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarInt(0xFC); + stream.Seek(0, SeekOrigin.Begin); + + var byteArray = new byte[stream.Length]; + stream.Read(byteArray, 0, (int)stream.Length); + Assert.AreEqual(0xFC, byteArray[0]); + } + else if (i == 2) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarInt(0xFFFF); + stream.Seek(0, SeekOrigin.Begin); + + var byteArray = new byte[stream.Length]; + stream.Read(byteArray, 0, (int)stream.Length); + Assert.AreEqual(0xFD, byteArray[0]); + Assert.AreEqual(Encoding.Default.GetString([0xFF, 0xFF]), + Encoding.Default.GetString(byteArray.Skip(1).Take(byteArray.Length - 1).ToArray())); + } + else if (i == 3) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarInt(0xFFFFFFFF); + stream.Seek(0, SeekOrigin.Begin); + + var byteArray = new byte[stream.Length]; + stream.Read(byteArray, 0, (int)stream.Length); + Assert.AreEqual(0xFE, byteArray[0]); + Assert.AreEqual(0xFFFFFFFF, BitConverter.ToUInt32(byteArray, 1)); + } + else + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarInt(0xAEFFFFFFFF); + stream.Seek(0, SeekOrigin.Begin); + + var byteArray = new byte[stream.Length]; + stream.Read(byteArray, 0, (int)stream.Length); + Assert.AreEqual(0xFF, byteArray[0]); + Assert.AreEqual(Encoding.Default.GetString([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00]), + Encoding.Default.GetString(byteArray.Skip(1).Take(byteArray.Length - 1).ToArray())); + } + } + } + + [TestMethod] + public void TestWriteVarString() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarString("a"); + stream.Seek(0, SeekOrigin.Begin); + + var byteArray = new byte[stream.Length]; + stream.Read(byteArray, 0, (int)stream.Length); + Assert.AreEqual(0x01, byteArray[0]); + Assert.AreEqual(0x61, byteArray[1]); + } +} diff --git a/tests/Neo.UnitTests/IO/UT_MemoryReader.cs b/tests/Neo.UnitTests/IO/UT_MemoryReader.cs new file mode 100644 index 0000000000..d17026eff6 --- /dev/null +++ b/tests/Neo.UnitTests/IO/UT_MemoryReader.cs @@ -0,0 +1,221 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MemoryReader.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using System.Text; + +namespace Neo.UnitTests.IO; + +[TestClass] +public class UT_MemoryReader +{ + [TestMethod] + public void TestReadFixedString() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteFixedString("AA", Encoding.UTF8.GetBytes("AA").Length + 1); + MemoryReader reader = new(stream.ToArray()); + string result = reader.ReadFixedString(Encoding.UTF8.GetBytes("AA").Length + 1); + Assert.AreEqual("AA", result); + } + + [TestMethod] + public void TestReadVarString() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.WriteVarString("AAAAAAA"); + MemoryReader reader = new(stream.ToArray()); + string result = reader.ReadVarString(10); + Assert.AreEqual("AAAAAAA", result); + } + + [TestMethod] + public void TestReadNullableArray() + { + byte[] bs = "0400000000".HexToBytes(); + MemoryReader reader = new(bs); + reader.ReadNullableArray(); + Assert.AreEqual(5, reader.Position); + } + + [TestMethod] + public void TestReadSByte() + { + var values = new sbyte[] { 0, 1, -1, 5, -5, sbyte.MaxValue, sbyte.MinValue }; + foreach (var v in values) + { + var byteArray = new byte[1]; + byteArray[0] = (byte)v; + var reader = new MemoryReader(byteArray); + var n = reader.ReadSByte(); + Assert.AreEqual(v, n); + } + + var values2 = new long[] { (long)int.MaxValue + 1, (long)int.MinValue - 1 }; + foreach (var v in values2) + { + var byteArray = new byte[1]; + byteArray[0] = (byte)v; + var reader = new MemoryReader(byteArray); + var n = reader.ReadSByte(); + Assert.AreEqual((sbyte)v, n); + } + } + + [TestMethod] + public void TestReadInt32() + { + var values = new int[] { 0, 1, -1, 5, -5, int.MaxValue, int.MinValue }; + foreach (var v in values) + { + var bytes = BitConverter.GetBytes(v); + var reader = new MemoryReader(bytes); + var n = reader.ReadInt32(); + Assert.AreEqual(v, n); + } + + var values2 = new long[] { (long)int.MaxValue + 1, (long)int.MinValue - 1 }; + foreach (var v in values2) + { + var bytes = BitConverter.GetBytes(v); + var reader = new MemoryReader(bytes); + var n = reader.ReadInt32(); + Assert.AreEqual((int)v, n); + } + } + + [TestMethod] + public void TestReadUInt64() + { + var values = new ulong[] { 0, 1, 5, ulong.MaxValue, ulong.MinValue }; + foreach (var v in values) + { + var bytes = BitConverter.GetBytes(v); + var reader = new MemoryReader(bytes); + var n = reader.ReadUInt64(); + Assert.AreEqual(v, n); + } + + var values2 = new long[] { long.MinValue, -1, long.MaxValue }; + foreach (var v in values2) + { + var bytes = BitConverter.GetBytes(v); + var reader = new MemoryReader(bytes); + var n = reader.ReadUInt64(); + Assert.AreEqual((ulong)v, n); + } + } + + [TestMethod] + public void TestReadInt16BigEndian() + { + var values = new short[] { short.MinValue, -1, 0, 1, 12345, short.MaxValue }; + foreach (var v in values) + { + var bytes = BitConverter.GetBytes(v); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + var reader = new MemoryReader(bytes); + var n = reader.ReadInt16BigEndian(); + Assert.AreEqual(v, n); + } + } + + [TestMethod] + public void TestReadUInt16BigEndian() + { + var values = new ushort[] { ushort.MinValue, 0, 1, 12345, ushort.MaxValue }; + foreach (var v in values) + { + var bytes = BitConverter.GetBytes(v); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + var reader = new MemoryReader(bytes); + var n = reader.ReadUInt16BigEndian(); + Assert.AreEqual(v, n); + } + } + + [TestMethod] + public void TestReadInt32BigEndian() + { + var values = new int[] { int.MinValue, -1, 0, 1, 12345, int.MaxValue }; + foreach (var v in values) + { + var bytes = BitConverter.GetBytes(v); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + var reader = new MemoryReader(bytes); + var n = reader.ReadInt32BigEndian(); + Assert.AreEqual(v, n); + } + } + + [TestMethod] + public void TestReadUInt32BigEndian() + { + var values = new uint[] { uint.MinValue, 0, 1, 12345, uint.MaxValue }; + foreach (var v in values) + { + var bytes = BitConverter.GetBytes(v); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + var reader = new MemoryReader(bytes); + var n = reader.ReadUInt32BigEndian(); + Assert.AreEqual(v, n); + } + } + + [TestMethod] + public void TestReadInt64BigEndian() + { + var values = new long[] { long.MinValue, int.MinValue, -1, 0, 1, 12345, int.MaxValue, long.MaxValue }; + foreach (var v in values) + { + var bytes = BitConverter.GetBytes(v); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + var reader = new MemoryReader(bytes); + var n = reader.ReadInt64BigEndian(); + Assert.AreEqual(v, n); + } + } + + [TestMethod] + public void TestReadUInt64BigEndian() + { + var values = new ulong[] { ulong.MinValue, 0, 1, 12345, ulong.MaxValue }; + foreach (var v in values) + { + var bytes = BitConverter.GetBytes(v); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + var reader = new MemoryReader(bytes); + var n = reader.ReadUInt64BigEndian(); + Assert.AreEqual(v, n); + } + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_Blockchain.cs b/tests/Neo.UnitTests/Ledger/UT_Blockchain.cs new file mode 100644 index 0000000000..ea2af79481 --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_Blockchain.cs @@ -0,0 +1,187 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_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 Akka.TestKit; +using Akka.TestKit.MsTest; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_Blockchain : TestKit +{ + private NeoSystem _system = null!; + private Transaction txSample = null!; + private TestProbe senderProbe = null!; + + [TestInitialize] + public void Initialize() + { + _system = TestBlockchain.GetSystem(); + senderProbe = CreateTestProbe(); + txSample = new Transaction + { + Attributes = [], + Script = Array.Empty(), + Signers = [new Signer { Account = UInt160.Zero }], + Witnesses = [] + }; + _system.MemPool.TryAdd(txSample, _system.GetSnapshotCache()); + } + + [TestMethod] + public void TestValidTransaction() + { + var snapshot = _system.GetSnapshotCache(); + var walletA = TestUtils.GenerateTestWallet("123"); + var acc = walletA.CreateAccount(); + + // Fake balance + + var key = new KeyBuilder(NativeContract.GAS.Id, 20).Add(acc.ScriptHash); + var entry = snapshot.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 100_000_000 * NativeContract.GAS.Factor; + snapshot.Commit(); + + // Make transaction + + var tx = TestUtils.CreateValidTx(snapshot, walletA, acc.ScriptHash, 0); + + senderProbe.Send(_system.Blockchain, tx); + senderProbe.ExpectMsg(p => p.Result == VerifyResult.Succeed, cancellationToken: CancellationToken.None); + + senderProbe.Send(_system.Blockchain, tx); + senderProbe.ExpectMsg(p => p.Result == VerifyResult.AlreadyInPool, cancellationToken: CancellationToken.None); + } + + [TestMethod] + public void TestInvalidTransaction() + { + var snapshot = _system.GetSnapshotCache(); + var walletA = TestUtils.GenerateTestWallet("123"); + var acc = walletA.CreateAccount(); + + // Fake balance + + var key = new KeyBuilder(NativeContract.GAS.Id, 20).Add(acc.ScriptHash); + var entry = snapshot.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 100_000_000 * NativeContract.GAS.Factor; + snapshot.Commit(); + + // Make transaction + + var tx = TestUtils.CreateValidTx(snapshot, walletA, acc.ScriptHash, 0); + tx.Signers = null!; + + senderProbe.Send(_system.Blockchain, tx); + senderProbe.ExpectMsg(p => p.Result == VerifyResult.Invalid, cancellationToken: CancellationToken.None); + } + + internal static StorageKey CreateStorageKey(byte prefix, byte[]? key = null) + { + byte[] buffer = GC.AllocateUninitializedArray(sizeof(byte) + (key?.Length ?? 0)); + buffer[0] = prefix; + key?.CopyTo(buffer.AsSpan(1)); + return new() + { + Id = NativeContract.NEO.Id, + Key = buffer + }; + } + + + [TestMethod] + public void TestMaliciousOnChainConflict() + { + var snapshot = _system.GetSnapshotCache(); + var walletA = TestUtils.GenerateTestWallet("123"); + var accA = walletA.CreateAccount(); + var walletB = TestUtils.GenerateTestWallet("456"); + var accB = walletB.CreateAccount(); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: _system.Settings, gas: long.MaxValue); + engine.LoadScript(Array.Empty()); + + // Fake balance for accounts A and B. + var key = new KeyBuilder(NativeContract.GAS.Id, 20).Add(accA.ScriptHash); + var entry = snapshot.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 100_000_000 * NativeContract.GAS.Factor; + snapshot.Commit(); + + key = new KeyBuilder(NativeContract.GAS.Id, 20).Add(accB.ScriptHash); + entry = snapshot.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 100_000_000 * NativeContract.GAS.Factor; + snapshot.Commit(); + + // Create transactions: + // tx1 conflicts with tx2 and has the same sender (thus, it's a valid conflict and must prevent tx2 from entering the chain); + // tx2 conflicts with tx3 and has different sender (thus, this conflict is invalid and must not prevent tx3 from entering the chain). + var tx1 = TestUtils.CreateValidTx(snapshot, walletA, accA.ScriptHash, 0); + var tx2 = TestUtils.CreateValidTx(snapshot, walletA, accA.ScriptHash, 1); + var tx3 = TestUtils.CreateValidTx(snapshot, walletB, accB.ScriptHash, 2); + + tx1.Attributes = [new Conflicts() { Hash = tx2.Hash }, new Conflicts() { Hash = tx3.Hash }]; + + // Persist tx1. + var block = new Block + { + Header = new Header() + { + Index = 5, // allow tx1, tx2 and tx3 to fit into MaxValidUntilBlockIncrement. + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero, + Witness = Witness.Empty, + }, + Transactions = [tx1], + }; + byte[] onPersistScript; + using (ScriptBuilder sb = new()) + { + sb.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist); + onPersistScript = sb.ToArray(); + } + using (ApplicationEngine engine2 = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, block, _system.Settings, 0)) + { + engine2.LoadScript(onPersistScript); + if (engine2.Execute() != VMState.HALT) throw engine2.FaultException!; + engine2.SnapshotCache.Commit(); + } + snapshot.Commit(); + + // Run PostPersist to update current block index in native Ledger. + // Relevant current block index is needed for conflict records checks. + byte[] postPersistScript; + using (ScriptBuilder sb = new()) + { + sb.EmitSysCall(ApplicationEngine.System_Contract_NativePostPersist); + postPersistScript = sb.ToArray(); + } + using (ApplicationEngine engine2 = ApplicationEngine.Create(TriggerType.PostPersist, null, snapshot, block, _system.Settings, 0)) + { + engine2.LoadScript(postPersistScript); + if (engine2.Execute() != VMState.HALT) throw engine2.FaultException!; + engine2.SnapshotCache.Commit(); + } + snapshot.Commit(); + + // Add tx2: must fail because valid conflict is alredy on chain (tx1). + senderProbe.Send(_system.Blockchain, tx2); + senderProbe.ExpectMsg(p => p.Result == VerifyResult.HasConflicts, cancellationToken: CancellationToken.None); + + // Add tx3: must succeed because on-chain conflict is invalid (doesn't have proper signer). + senderProbe.Send(_system.Blockchain, tx3); + senderProbe.ExpectMsg(p => p.Result == VerifyResult.Succeed, cancellationToken: CancellationToken.None); + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_HashIndexState.cs b/tests/Neo.UnitTests/Ledger/UT_HashIndexState.cs new file mode 100644 index 0000000000..21a1b077d4 --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_HashIndexState.cs @@ -0,0 +1,46 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_HashIndexState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; +using Neo.VM; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_HashIndexState +{ + HashIndexState origin = null!; + + [TestInitialize] + public void Initialize() + { + origin = new HashIndexState + { + Hash = UInt256.Zero, + Index = 10 + }; + } + + [TestMethod] + public void TestDeserialize() + { + var data = BinarySerializer.Serialize(((IInteroperable)origin).ToStackItem(null), ExecutionEngineLimits.Default); + var reader = new MemoryReader(data); + + HashIndexState dest = new(); + ((IInteroperable)dest).FromStackItem(BinarySerializer.Deserialize(ref reader, ExecutionEngineLimits.Default, null)); + + Assert.AreEqual(origin.Hash, dest.Hash); + Assert.AreEqual(origin.Index, dest.Index); + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_HeaderCache.cs b/tests/Neo.UnitTests/Ledger/UT_HeaderCache.cs new file mode 100644 index 0000000000..4884f2c9e8 --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_HeaderCache.cs @@ -0,0 +1,114 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_HeaderCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_HeaderCache +{ + [TestMethod] + public void TestHeaderCache() + { + var cache = new HeaderCache(); + var header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = null!, + Index = 1, + NextConsensus = null!, + Witness = null! + }; + cache.Add(header); + + var got = cache[1]; + Assert.IsNotNull(got); + Assert.AreEqual((uint)1, got.Index); + + var count = cache.Count; + Assert.AreEqual(1, count); + + var full = cache.Full; + Assert.IsFalse(full); + + var last = cache.Last; + Assert.IsNotNull(last); + Assert.AreEqual((uint)1, last.Index); + + got = cache[2]; + Assert.IsNull(got); + + // enumerate + var enumerator = cache.GetEnumerator(); + Assert.IsTrue(enumerator.MoveNext()); + Assert.AreEqual((uint)1, enumerator.Current.Index); + Assert.IsFalse(enumerator.MoveNext()); + + var removed = cache.TryRemoveFirst(out _); + Assert.IsTrue(removed); + + count = cache.Count; + Assert.AreEqual(0, count); + + full = cache.Full; + Assert.IsFalse(full); + + last = cache.Last; + Assert.IsNull(last); + + got = cache[1]; + Assert.IsNull(got); + } + + [TestMethod] + public void TestHeaderCache_Limit() + { + var cache = new HeaderCache(); + uint capacity = 10000; + + // Fill the cache + for (uint i = 0; i < capacity; i++) + { + cache.Add(new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = null!, + Index = i, + NextConsensus = null!, + Witness = null! + }); + } + + Assert.AreEqual((int)capacity, cache.Count); + Assert.IsTrue(cache.Full); + Assert.AreEqual(capacity - 1, cache.Last!.Index); + + // Try adding one more + cache.Add(new Header + { + PrevHash = null!, + MerkleRoot = null!, + Index = capacity, + NextConsensus = null!, + Witness = null! + }); + + // Verify count did not increase and last item remains the same + Assert.AreEqual((int)capacity, cache.Count); + Assert.IsTrue(cache.Full); + Assert.AreEqual(capacity - 1, cache.Last.Index); + + // Verify the extra item was not added + Assert.IsNull(cache[capacity]); + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_MemoryPool.cs b/tests/Neo.UnitTests/Ledger/UT_MemoryPool.cs new file mode 100644 index 0000000000..5559afcabb --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_MemoryPool.cs @@ -0,0 +1,1006 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MemoryPool.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.MsTest; +using Moq; +using Neo.Cryptography; +using Neo.Factories; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using System.Collections; +using System.Numerics; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_MemoryPool : TestKit +{ + private static NeoSystem _system = null!; + + private const byte Prefix_MaxTransactionsPerBlock = 23; + private const byte Prefix_FeePerByte = 10; + private readonly UInt160 senderAccount = UInt160.Zero; + private MemoryPool _unit = null!; + + [ClassInitialize] + public static void TestSetup(TestContext ctx) + { + _system = TestBlockchain.GetSystem(); + } + + private static DataCache GetSnapshot() + { + return _system.StoreView.CloneCache(); + } + + [TestInitialize] + public void TestSetup() + { + // protect against external changes on TimeProvider + TimeProvider.ResetToDefault(); + + // Create a MemoryPool with capacity of 100 + _unit = new MemoryPool(new NeoSystem(TestProtocolSettings.Default with { MemoryPoolMaxTransactions = 100 }, storageProvider: (string?)null)); + + // Verify capacity equals the amount specified + Assert.AreEqual(100, _unit.Capacity); + + Assert.AreEqual(0, _unit.VerifiedCount); + Assert.AreEqual(0, _unit.UnVerifiedCount); + Assert.IsEmpty(_unit); + } + + private Transaction CreateTransactionWithFee(long fee) + { + var randomBytes = RandomNumberFactory.NextBytes(16); + Mock mock = new(); + mock.Setup(p => p.VerifyStateDependent( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns(VerifyResult.Succeed); + mock.Setup(p => p.VerifyStateIndependent(It.IsAny())).Returns(VerifyResult.Succeed); + mock.Object.Script = randomBytes; + mock.Object.NetworkFee = fee; + mock.Object.Attributes = []; + mock.Object.Signers = [new() { Account = senderAccount, Scopes = WitnessScope.None }]; + mock.Object.Witnesses = [Witness.Empty]; + return mock.Object; + } + + private Transaction CreateTransactionWithFeeAndBalanceVerify(long fee) + { + var randomBytes = RandomNumberFactory.NextBytes(16); + Mock mock = new(); + UInt160 sender = senderAccount; + mock.Setup(p => p.VerifyStateDependent( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((ProtocolSettings settings, DataCache snapshot, TransactionVerificationContext context, IEnumerable conflictsList) + => context.CheckTransaction(mock.Object, conflictsList, snapshot) ? VerifyResult.Succeed : VerifyResult.InsufficientFunds); + + mock.Setup(p => p.VerifyStateIndependent(It.IsAny())).Returns(VerifyResult.Succeed); + mock.Object.Script = randomBytes; + mock.Object.NetworkFee = fee; + mock.Object.Attributes = []; + mock.Object.Signers = [new() { Account = senderAccount, Scopes = WitnessScope.None }]; + mock.Object.Witnesses = [Witness.Empty]; + return mock.Object; + } + + private Transaction CreateTransaction(long fee = -1) + { + if (fee != -1) + return CreateTransactionWithFee(fee); + return CreateTransactionWithFee(RandomNumberFactory.NextInt64(100000, 100000000)); + } + + private void AddTransactions(int count) + { + var snapshot = GetSnapshot(); + for (int i = 0; i < count; i++) + { + var txToAdd = CreateTransaction(); + _unit.TryAdd(txToAdd, snapshot); + } + + Console.WriteLine($"created {count} tx"); + } + + private void AddTransaction(Transaction txToAdd) + { + var snapshot = GetSnapshot(); + _unit.TryAdd(txToAdd, snapshot); + } + + private void AddTransactionsWithBalanceVerify(int count, long fee, DataCache snapshot) + { + for (int i = 0; i < count; i++) + { + var txToAdd = CreateTransactionWithFeeAndBalanceVerify(fee); + _unit.TryAdd(txToAdd, snapshot); + } + + Console.WriteLine($"created {count} tx"); + } + + [TestMethod] + public void CapacityTest() + { + // Add over the capacity items, verify that the verified count increases each time + AddTransactions(101); + + Console.WriteLine($"VerifiedCount: {_unit.VerifiedCount} Count {_unit.SortedTxCount}"); + + Assert.AreEqual(100, _unit.SortedTxCount); + Assert.AreEqual(100, _unit.VerifiedCount); + Assert.AreEqual(0, _unit.UnVerifiedCount); + Assert.HasCount(100, _unit); + } + + [TestMethod] + public void BlockPersistMovesTxToUnverifiedAndReverification() + { + AddTransactions(70); + + Assert.AreEqual(70, _unit.SortedTxCount); + + var block = new Block + { + Header = new Header + { + PrevHash = null!, + MerkleRoot = null!, + NextConsensus = null!, + Witness = null! + }, + Transactions = _unit.GetSortedVerifiedTransactions(10) + .Concat(_unit.GetSortedVerifiedTransactions(5)).ToArray() + }; + _unit.UpdatePoolForBlockPersisted(block, GetSnapshot()); + _unit.InvalidateVerifiedTransactions(); + Assert.AreEqual(0, _unit.SortedTxCount); + Assert.AreEqual(60, _unit.UnverifiedSortedTxCount); + + _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(10, GetSnapshot()); + Assert.AreEqual(10, _unit.SortedTxCount); + Assert.AreEqual(50, _unit.UnverifiedSortedTxCount); + + _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(10, GetSnapshot()); + Assert.AreEqual(20, _unit.SortedTxCount); + Assert.AreEqual(40, _unit.UnverifiedSortedTxCount); + + _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(10, GetSnapshot()); + Assert.AreEqual(30, _unit.SortedTxCount); + Assert.AreEqual(30, _unit.UnverifiedSortedTxCount); + + _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(10, GetSnapshot()); + Assert.AreEqual(40, _unit.SortedTxCount); + Assert.AreEqual(20, _unit.UnverifiedSortedTxCount); + + _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(10, GetSnapshot()); + Assert.AreEqual(50, _unit.SortedTxCount); + Assert.AreEqual(10, _unit.UnverifiedSortedTxCount); + + _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(10, GetSnapshot()); + Assert.AreEqual(60, _unit.SortedTxCount); + Assert.AreEqual(0, _unit.UnverifiedSortedTxCount); + } + + [TestMethod] + public async Task BlockPersistAndReverificationWillAbandonTxAsBalanceTransfered() + { + var snapshot = GetSnapshot(); + BigInteger balance = NativeContract.GAS.BalanceOf(snapshot, senderAccount); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default, gas: long.MaxValue); + engine.LoadScript(Array.Empty()); + await NativeContract.GAS.Burn(engine, UInt160.Zero, balance); + _ = NativeContract.GAS.Mint(engine, UInt160.Zero, 70, true); + + long txFee = 1; + AddTransactionsWithBalanceVerify(70, txFee, engine.SnapshotCache); + + Assert.AreEqual(70, _unit.SortedTxCount); + + var block = new Block + { + Header = new Header + { + PrevHash = null!, + MerkleRoot = null!, + NextConsensus = null!, + Witness = null! + }, + Transactions = _unit.GetSortedVerifiedTransactions(10) + }; + + // Simulate the transfer process in tx by burning the balance + UInt160 sender = block.Transactions[0].Sender; + + ApplicationEngine applicationEngine = ApplicationEngine.Create(TriggerType.All, block, snapshot, block, settings: TestProtocolSettings.Default, gas: (long)balance); + applicationEngine.LoadScript(Array.Empty()); + await NativeContract.GAS.Burn(applicationEngine, sender, NativeContract.GAS.BalanceOf(snapshot, sender)); + _ = NativeContract.GAS.Mint(applicationEngine, sender, txFee * 30, true); // Set the balance to meet 30 txs only + + // Persist block and reverify all the txs in mempool, but half of the txs will be discarded + _unit.UpdatePoolForBlockPersisted(block, applicationEngine.SnapshotCache); + Assert.AreEqual(30, _unit.SortedTxCount); + Assert.AreEqual(0, _unit.UnverifiedSortedTxCount); + + // Revert the balance + await NativeContract.GAS.Burn(applicationEngine, sender, txFee * 30); + _ = NativeContract.GAS.Mint(applicationEngine, sender, balance, true); + } + + [TestMethod] + public async Task UpdatePoolForBlockPersisted_RemoveBlockConflicts() + { + // Arrange: prepare mempooled and in-bock txs conflicting with each other. + long txFee = 1; + var snapshot = GetSnapshot(); + BigInteger balance = NativeContract.GAS.BalanceOf(snapshot, senderAccount); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default, gas: long.MaxValue); + engine.LoadScript(Array.Empty()); + await NativeContract.GAS.Burn(engine, UInt160.Zero, balance); + _ = NativeContract.GAS.Mint(engine, UInt160.Zero, 7, true); // balance enough for 7 mempooled txs + + var mp1 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp1 doesn't conflict with anyone + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(mp1, engine.SnapshotCache)); + var tx1 = CreateTransactionWithFeeAndBalanceVerify(txFee); // but in-block tx1 conflicts with mempooled mp1 => mp1 should be removed from pool after persist + tx1.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp1.Hash } }; + + var mp2 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp1 and mp2 don't conflict with anyone + _unit.TryAdd(mp2, engine.SnapshotCache); + var mp3 = CreateTransactionWithFeeAndBalanceVerify(txFee); + _unit.TryAdd(mp3, engine.SnapshotCache); + var tx2 = CreateTransactionWithFeeAndBalanceVerify(txFee); // in-block tx2 conflicts with mempooled mp2 and mp3 => mp2 and mp3 should be removed from pool after persist + tx2.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp2.Hash }, new Conflicts() { Hash = mp3.Hash } }; + + var tx3 = CreateTransactionWithFeeAndBalanceVerify(txFee); // in-block tx3 doesn't conflict with anyone + var mp4 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp4 conflicts with in-block tx3 => mp4 should be removed from pool after persist + mp4.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = tx3.Hash } }; + _unit.TryAdd(mp4, engine.SnapshotCache); + + var tx4 = CreateTransactionWithFeeAndBalanceVerify(txFee); // in-block tx4 and tx5 don't conflict with anyone + var tx5 = CreateTransactionWithFeeAndBalanceVerify(txFee); + var mp5 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp5 conflicts with in-block tx4 and tx5 => mp5 should be removed from pool after persist + mp5.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = tx4.Hash }, new Conflicts() { Hash = tx5.Hash } }; + _unit.TryAdd(mp5, engine.SnapshotCache); + + var mp6 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp6 doesn't conflict with anyone and noone conflicts with mp6 => mp6 should be left in the pool after persist + _unit.TryAdd(mp6, engine.SnapshotCache); + + Assert.AreEqual(6, _unit.SortedTxCount); + Assert.AreEqual(0, _unit.UnverifiedSortedTxCount); + + var mp7 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp7 doesn't conflict with anyone + var tx6 = CreateTransactionWithFeeAndBalanceVerify(txFee); // in-block tx6 conflicts with mp7, but doesn't include sender of mp7 into signers list => even if tx6 is included into block, mp7 shouldn't be removed from the pool + tx6.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp7.Hash } }; + tx6.Signers = new Signer[] { new() { Account = new UInt160(Crypto.Hash160(new byte[] { 1, 2, 3 })) }, new() { Account = new UInt160(Crypto.Hash160(new byte[] { 4, 5, 6 })) } }; + _unit.TryAdd(mp7, engine.SnapshotCache); + + // Act: persist block and reverify all mempooled txs. + var block = new Block + { + Header = new Header + { + PrevHash = null!, + MerkleRoot = null!, + NextConsensus = null!, + Witness = null! + }, + Transactions = new Transaction[] { tx1, tx2, tx3, tx4, tx5, tx6 }, + }; + _unit.UpdatePoolForBlockPersisted(block, engine.SnapshotCache); + + // Assert: conflicting txs should be removed from the pool; the only mp6 that doesn't conflict with anyone should be left. + Assert.AreEqual(2, _unit.SortedTxCount); + Assert.IsTrue(_unit.GetSortedVerifiedTransactions().Select(tx => tx.Hash).Contains(mp6.Hash)); + Assert.IsTrue(_unit.GetSortedVerifiedTransactions().Select(tx => tx.Hash).Contains(mp7.Hash)); + Assert.AreEqual(0, _unit.UnverifiedSortedTxCount); + + // Cleanup: revert the balance. + await NativeContract.GAS.Burn(engine, UInt160.Zero, txFee * 7); + _ = NativeContract.GAS.Mint(engine, UInt160.Zero, balance, true); + } + + [TestMethod] + public async Task TryAdd_AddRangeOfConflictingTransactions() + { + // Arrange: prepare mempooled txs that have conflicts. + long txFee = 1; + var maliciousSender = new UInt160(Crypto.Hash160(new byte[] { 1, 2, 3 })); + var snapshot = GetSnapshot(); + BigInteger balance = NativeContract.GAS.BalanceOf(snapshot, senderAccount); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default, gas: long.MaxValue); + engine.LoadScript(Array.Empty()); + await NativeContract.GAS.Burn(engine, UInt160.Zero, balance); + _ = NativeContract.GAS.Mint(engine, UInt160.Zero, 100, true); // balance enough for all mempooled txs + _ = NativeContract.GAS.Mint(engine, maliciousSender, 100, true); // balance enough for all mempooled txs + + var mp1 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp1 doesn't conflict with anyone and not in the pool yet + + var mp2_1 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp2_1 conflicts with mp1 and has the same network fee + mp2_1.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp1.Hash } }; + _unit.TryAdd(mp2_1, engine.SnapshotCache); + var mp2_2 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp2_2 also conflicts with mp1 and has the same network fee as mp1 and mp2_1 + mp2_2.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp1.Hash } }; + _unit.TryAdd(mp2_2, engine.SnapshotCache); + + var mp3 = CreateTransactionWithFeeAndBalanceVerify(2 * txFee); // mp3 conflicts with mp1 and has larger network fee + mp3.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp1.Hash } }; + _unit.TryAdd(mp3, engine.SnapshotCache); + + var mp4 = CreateTransactionWithFeeAndBalanceVerify(3 * txFee); // mp4 conflicts with mp3 and has larger network fee + mp4.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp3.Hash } }; + + var malicious = CreateTransactionWithFeeAndBalanceVerify(3 * txFee); // malicious conflicts with mp3 and has larger network fee, but different sender + malicious.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp3.Hash } }; + malicious.Signers = new Signer[] { new() { Account = new UInt160(Crypto.Hash160(new byte[] { 1, 2, 3 })), Scopes = WitnessScope.None } }; + + var mp5 = CreateTransactionWithFeeAndBalanceVerify(2 * txFee); // mp5 conflicts with mp4 and has smaller network fee + mp5.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp4.Hash } }; + + var mp6 = CreateTransactionWithFeeAndBalanceVerify(mp2_1.NetworkFee + mp2_2.NetworkFee + 1); // mp6 conflicts with mp2_1 and mp2_2 and has larger network fee. + mp6.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp2_1.Hash }, new Conflicts() { Hash = mp2_2.Hash } }; + + var mp7 = CreateTransactionWithFeeAndBalanceVerify(txFee * 2 + 1); // mp7 doesn't conflicts with anyone, but mp8, mp9 and mp10malicious has smaller sum network fee and conflict with mp7. + var mp8 = CreateTransactionWithFeeAndBalanceVerify(txFee); + mp8.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp7.Hash } }; + var mp9 = CreateTransactionWithFeeAndBalanceVerify(txFee); + mp9.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp7.Hash } }; + var mp10malicious = CreateTransactionWithFeeAndBalanceVerify(txFee); + mp10malicious.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp7.Hash } }; + mp10malicious.Signers = new Signer[] { new() { Account = maliciousSender, Scopes = WitnessScope.None } }; + + Assert.AreEqual(3, _unit.SortedTxCount); + Assert.AreEqual(0, _unit.UnverifiedSortedTxCount); + + // Act & Assert: try to add conlflicting transactions to the pool. + Assert.AreEqual(VerifyResult.HasConflicts, _unit.TryAdd(mp1, engine.SnapshotCache)); // mp1 conflicts with mp2_1, mp2_2 and mp3 but has lower network fee than mp3 => mp1 fails to be added + Assert.AreEqual(3, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp2_1, mp2_2, mp3 }, _unit.GetVerifiedTransactions().ToList()); + + Assert.AreEqual(VerifyResult.HasConflicts, _unit.TryAdd(malicious, engine.SnapshotCache)); // malicious conflicts with mp3, has larger network fee but malicious (different) sender => mp3 shoould be left in pool + Assert.AreEqual(3, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp2_1, mp2_2, mp3 }, _unit.GetVerifiedTransactions().ToList()); + + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(mp4, engine.SnapshotCache)); // mp4 conflicts with mp3 and has larger network fee => mp3 shoould be removed from pool + Assert.AreEqual(3, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp2_1, mp2_2, mp4 }, _unit.GetVerifiedTransactions().ToList()); + + Assert.AreEqual(VerifyResult.HasConflicts, _unit.TryAdd(mp1, engine.SnapshotCache)); // mp1 conflicts with mp2_1 and mp2_2 and has same network fee => mp2_1 and mp2_2 should be left in pool. + Assert.AreEqual(3, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp2_1, mp2_2, mp4 }, _unit.GetVerifiedTransactions().ToList()); + + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(mp6, engine.SnapshotCache)); // mp6 conflicts with mp2_1 and mp2_2 and has larger network fee than the sum of mp2_1 and mp2_2 fees => mp6 should be added. + Assert.AreEqual(2, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp6, mp4 }, _unit.GetVerifiedTransactions().ToList()); + + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(mp1, engine.SnapshotCache)); // mp1 conflicts with mp2_1 and mp2_2, but they are not in the pool now => mp1 should be added. + Assert.AreEqual(3, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp1, mp6, mp4 }, _unit.GetVerifiedTransactions().ToList()); + + Assert.AreEqual(VerifyResult.HasConflicts, _unit.TryAdd(mp2_1, engine.SnapshotCache)); // mp2_1 conflicts with mp1 and has same network fee => mp2_1 shouldn't be added to the pool. + Assert.AreEqual(3, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp1, mp6, mp4 }, _unit.GetVerifiedTransactions().ToList()); + + Assert.AreEqual(VerifyResult.HasConflicts, _unit.TryAdd(mp5, engine.SnapshotCache)); // mp5 conflicts with mp4 and has smaller network fee => mp5 fails to be added. + Assert.AreEqual(3, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp1, mp6, mp4 }, _unit.GetVerifiedTransactions().ToList()); + + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(mp8, engine.SnapshotCache)); // mp8, mp9 and mp10malicious conflict with mp7, but mo7 is not in the pool yet. + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(mp9, engine.SnapshotCache)); + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(mp10malicious, engine.SnapshotCache)); + Assert.AreEqual(6, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp1, mp6, mp4, mp8, mp9, mp10malicious }, _unit.GetVerifiedTransactions().ToList()); + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(mp7, engine.SnapshotCache)); // mp7 has larger network fee than the sum of mp8 and mp9 fees => should be added to the pool. + Assert.AreEqual(4, _unit.SortedTxCount); + CollectionAssert.IsSubsetOf(new List() { mp1, mp6, mp4, mp7 }, _unit.GetVerifiedTransactions().ToList()); + + // Cleanup: revert the balance. + await NativeContract.GAS.Burn(engine, UInt160.Zero, 100); + _ = NativeContract.GAS.Mint(engine, UInt160.Zero, balance, true); + await NativeContract.GAS.Burn(engine, maliciousSender, 100); + _ = NativeContract.GAS.Mint(engine, maliciousSender, balance, true); + } + + [TestMethod] + public async Task TryRemoveVerified_RemoveVerifiedTxWithConflicts() + { + // Arrange: prepare mempooled txs that have conflicts. + long txFee = 1; + var snapshot = GetSnapshot(); + BigInteger balance = NativeContract.GAS.BalanceOf(snapshot, senderAccount); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default, gas: long.MaxValue); + engine.LoadScript(Array.Empty()); + await NativeContract.GAS.Burn(engine, UInt160.Zero, balance); + _ = NativeContract.GAS.Mint(engine, UInt160.Zero, 100, true); // balance enough for all mempooled txs + + var mp1 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp1 doesn't conflict with anyone and not in the pool yet + + var mp2 = CreateTransactionWithFeeAndBalanceVerify(2 * txFee); // mp2 conflicts with mp1 and has larger same network fee + mp2.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp1.Hash } }; + _unit.TryAdd(mp2, engine.SnapshotCache); + + Assert.AreEqual(1, _unit.SortedTxCount); + Assert.AreEqual(0, _unit.UnverifiedSortedTxCount); + + Assert.AreEqual(VerifyResult.HasConflicts, _unit.TryAdd(mp1, engine.SnapshotCache)); // mp1 conflicts with mp2 but has lower network fee + Assert.AreEqual(1, _unit.SortedTxCount); + CollectionAssert.Contains(_unit.GetVerifiedTransactions().ToList(), mp2); + + // Act & Assert: try to invalidate verified transactions and push conflicting one. + _unit.InvalidateVerifiedTransactions(); + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(mp1, engine.SnapshotCache)); // mp1 conflicts with mp2 but mp2 is not verified anymore + Assert.AreEqual(1, _unit.SortedTxCount); + CollectionAssert.Contains(_unit.GetVerifiedTransactions().ToList(), mp1); + + var tx1 = CreateTransactionWithFeeAndBalanceVerify(txFee); // in-block tx1 doesn't conflict with anyone and is aimed to trigger reverification + var block = new Block + { + Header = new Header + { + PrevHash = null!, + MerkleRoot = null!, + NextConsensus = null!, + Witness = null! + }, + Transactions = new Transaction[] { tx1 }, + }; + _unit.UpdatePoolForBlockPersisted(block, engine.SnapshotCache); + Assert.AreEqual(1, _unit.SortedTxCount); + CollectionAssert.Contains(_unit.GetVerifiedTransactions().ToList(), mp2); // after reverificaion mp2 should be back at verified list; mp1 should be completely kicked off + } + + private static void VerifyTransactionsSortedDescending(IEnumerable transactions) + { + Transaction? lastTransaction = null; + foreach (var tx in transactions) + { + if (lastTransaction != null) + { + if (lastTransaction.FeePerByte == tx.FeePerByte) + { + if (lastTransaction.NetworkFee == tx.NetworkFee) + Assert.IsTrue(lastTransaction.Hash < tx.Hash); + else + Assert.IsGreaterThan(tx.NetworkFee, lastTransaction.NetworkFee); + } + else + { + Assert.IsGreaterThan(tx.FeePerByte, lastTransaction.FeePerByte); + } + } + lastTransaction = tx; + } + } + + [TestMethod] + public void VerifySortOrderAndThatHighetFeeTransactionsAreReverifiedFirst() + { + AddTransactions(100); + + var sortedVerifiedTxs = _unit.GetSortedVerifiedTransactions(); + // verify all 100 transactions are returned in sorted order + Assert.HasCount(100, sortedVerifiedTxs); + VerifyTransactionsSortedDescending(sortedVerifiedTxs); + + // move all to unverified + var block = new Block + { + Header = new Header + { + PrevHash = null!, + MerkleRoot = null!, + NextConsensus = null!, + Witness = null! + }, + Transactions = Array.Empty() + }; + _unit.UpdatePoolForBlockPersisted(block, GetSnapshot()); + _unit.InvalidateVerifiedTransactions(); + Assert.AreEqual(0, _unit.SortedTxCount); + Assert.AreEqual(100, _unit.UnverifiedSortedTxCount); + + // We can verify the order they are re-verified by reverifying 2 at a time + while (_unit.UnVerifiedCount > 0) + { + _unit.GetVerifiedAndUnverifiedTransactions(out var sortedVerifiedTransactions, out var sortedUnverifiedTransactions); + Assert.AreEqual(0, sortedVerifiedTransactions.Count()); + var sortedUnverifiedArray = sortedUnverifiedTransactions.ToArray(); + VerifyTransactionsSortedDescending(sortedUnverifiedArray); + var maxTransaction = sortedUnverifiedArray.First(); + var minTransaction = sortedUnverifiedArray.Last(); + + // reverify 1 high priority and 1 low priority transaction + _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(1, GetSnapshot()); + var verifiedTxs = _unit.GetSortedVerifiedTransactions(); + Assert.HasCount(1, verifiedTxs); + Assert.AreEqual(maxTransaction, verifiedTxs[0]); + var blockWith2Tx = new Block + { + Header = new Header + { + PrevHash = null!, + MerkleRoot = null!, + NextConsensus = null!, + Witness = null! + }, + Transactions = new[] { maxTransaction, minTransaction } + }; + // verify and remove the 2 transactions from the verified pool + _unit.UpdatePoolForBlockPersisted(blockWith2Tx, GetSnapshot()); + _unit.InvalidateVerifiedTransactions(); + Assert.AreEqual(0, _unit.SortedTxCount); + } + Assert.AreEqual(0, _unit.UnverifiedSortedTxCount); + } + + void VerifyCapacityThresholdForAttemptingToAddATransaction() + { + var sortedVerified = _unit.GetSortedVerifiedTransactions(); + var txBarelyWontFit = CreateTransactionWithFee(sortedVerified.Last().NetworkFee - 1); + Assert.IsFalse(_unit.CanTransactionFitInPool(txBarelyWontFit)); + var txBarelyFits = CreateTransactionWithFee(sortedVerified.Last().NetworkFee + 1); + Assert.IsTrue(_unit.CanTransactionFitInPool(txBarelyFits)); + } + + [TestMethod] + public void VerifyCanTransactionFitInPoolWorksAsIntended() + { + AddTransactions(100); + VerifyCapacityThresholdForAttemptingToAddATransaction(); + AddTransactions(50); + VerifyCapacityThresholdForAttemptingToAddATransaction(); + AddTransactions(50); + VerifyCapacityThresholdForAttemptingToAddATransaction(); + } + + [TestMethod] + public void CapacityTestWithUnverifiedHighProirtyTransactions() + { + // Verify that unverified high priority transactions will not be pushed out of the queue by incoming + // low priority transactions + + // Fill pool with high priority transactions + AddTransactions(99); + + // move all to unverified + var block = new Block + { + Header = new Header + { + PrevHash = null!, + MerkleRoot = null!, + NextConsensus = null!, + Witness = null! + }, + Transactions = Array.Empty() + }; + _unit.UpdatePoolForBlockPersisted(block, GetSnapshot()); + + Assert.IsTrue(_unit.CanTransactionFitInPool(CreateTransaction())); + AddTransactions(1); + Assert.IsFalse(_unit.CanTransactionFitInPool(CreateTransactionWithFee(0))); + } + + [TestMethod] + public void TestInvalidateAll() + { + AddTransactions(30); + + Assert.AreEqual(0, _unit.UnverifiedSortedTxCount); + Assert.AreEqual(30, _unit.SortedTxCount); + _unit.InvalidateAllTransactions(); + Assert.AreEqual(30, _unit.UnverifiedSortedTxCount); + Assert.AreEqual(0, _unit.SortedTxCount); + } + + [TestMethod] + public void TestContainsKey() + { + var snapshot = GetSnapshot(); + AddTransactions(10); + + var txToAdd = CreateTransaction(); + _unit.TryAdd(txToAdd, snapshot); + Assert.IsTrue(_unit.ContainsKey(txToAdd.Hash)); + _unit.InvalidateVerifiedTransactions(); + Assert.IsTrue(_unit.ContainsKey(txToAdd.Hash)); + } + + [TestMethod] + public void TestGetEnumerator() + { + AddTransactions(10); + _unit.InvalidateVerifiedTransactions(); + IEnumerator enumerator = _unit.GetEnumerator(); + foreach (Transaction tx in _unit) + { + enumerator.MoveNext(); + Assert.IsTrue(ReferenceEquals(tx, enumerator.Current)); + } + } + + [TestMethod] + public void TestIEnumerableGetEnumerator() + { + AddTransactions(10); + _unit.InvalidateVerifiedTransactions(); + IEnumerable enumerable = _unit; + var enumerator = enumerable.GetEnumerator(); + foreach (Transaction tx in _unit) + { + enumerator.MoveNext(); + Assert.IsTrue(ReferenceEquals(tx, enumerator.Current)); + } + } + + [TestMethod] + public void TestGetVerifiedTransactions() + { + var snapshot = GetSnapshot(); + var tx1 = CreateTransaction(); + var tx2 = CreateTransaction(); + _unit.TryAdd(tx1, snapshot); + _unit.InvalidateVerifiedTransactions(); + _unit.TryAdd(tx2, snapshot); + IEnumerable enumerable = _unit.GetVerifiedTransactions(); + Assert.AreEqual(1, enumerable.Count()); + var enumerator = enumerable.GetEnumerator(); + enumerator.MoveNext(); + Assert.AreEqual(tx2, enumerator.Current); + } + + [TestMethod] + public void TestReVerifyTopUnverifiedTransactionsIfNeeded() + { + _unit = new MemoryPool(new NeoSystem(TestProtocolSettings.Default with { MemoryPoolMaxTransactions = 600 }, storageProvider: (string?)null)); + + AddTransaction(CreateTransaction(100000001)); + AddTransaction(CreateTransaction(100000001)); + AddTransaction(CreateTransaction(100000001)); + AddTransaction(CreateTransaction(1)); + Assert.AreEqual(4, _unit.VerifiedCount); + Assert.AreEqual(0, _unit.UnVerifiedCount); + + _unit.InvalidateVerifiedTransactions(); + Assert.AreEqual(0, _unit.VerifiedCount); + Assert.AreEqual(4, _unit.UnVerifiedCount); + + AddTransactions(511); // Max per block currently is 512 + Assert.AreEqual(511, _unit.VerifiedCount); + Assert.AreEqual(4, _unit.UnVerifiedCount); + + var result = _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(1, GetSnapshot()); + Assert.IsTrue(result); + Assert.AreEqual(512, _unit.VerifiedCount); + Assert.AreEqual(3, _unit.UnVerifiedCount); + + result = _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(2, GetSnapshot()); + Assert.IsTrue(result); + Assert.AreEqual(514, _unit.VerifiedCount); + Assert.AreEqual(1, _unit.UnVerifiedCount); + + result = _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(3, GetSnapshot()); + Assert.IsFalse(result); + Assert.AreEqual(515, _unit.VerifiedCount); + Assert.AreEqual(0, _unit.UnVerifiedCount); + } + + [TestMethod] + public void TestTryAdd() + { + var snapshot = GetSnapshot(); + var tx1 = CreateTransaction(); + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(tx1, snapshot)); + Assert.AreNotEqual(VerifyResult.Succeed, _unit.TryAdd(tx1, snapshot)); + } + + [TestMethod] + public void TestTryGetValue() + { + var snapshot = GetSnapshot(); + var tx1 = CreateTransaction(); + _unit.TryAdd(tx1, snapshot); + Assert.IsTrue(_unit.TryGetValue(tx1.Hash, out Transaction? tx)); + Assert.AreEqual(tx1, tx); + + _unit.InvalidateVerifiedTransactions(); + Assert.IsTrue(_unit.TryGetValue(tx1.Hash, out tx)); + Assert.AreEqual(tx1, tx); + + var tx2 = CreateTransaction(); + Assert.IsFalse(_unit.TryGetValue(tx2.Hash, out _)); + } + + [TestMethod] + public void TestUpdatePoolForBlockPersisted() + { + var snapshot = GetSnapshot(); + byte[] transactionsPerBlock = { 0x18, 0x00, 0x00, 0x00 }; // 24 + byte[] feePerByte = { 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00 }; // 1048576 + StorageItem item1 = new() + { + Value = transactionsPerBlock + }; + StorageItem item2 = new() + { + Value = feePerByte + }; + var key1 = CreateStorageKey(NativeContract.Policy.Id, Prefix_MaxTransactionsPerBlock); + var key2 = CreateStorageKey(NativeContract.Policy.Id, Prefix_FeePerByte); + snapshot.Add(key1, item1); + snapshot.Add(key2, item2); + + var tx1 = CreateTransaction(); + var tx2 = CreateTransaction(); + Transaction[] transactions = { tx1, tx2 }; + _unit.TryAdd(tx1, snapshot); + + var block = new Block + { + Header = new Header + { + PrevHash = null!, + MerkleRoot = null!, + NextConsensus = null!, + Witness = null! + }, + Transactions = transactions + }; + + Assert.AreEqual(0, _unit.UnVerifiedCount); + Assert.AreEqual(1, _unit.VerifiedCount); + + _unit.UpdatePoolForBlockPersisted(block, snapshot); + + Assert.AreEqual(0, _unit.UnVerifiedCount); + Assert.AreEqual(0, _unit.VerifiedCount); + } + + [TestMethod] + public void TestTryRemoveUnVerified() + { + AddTransactions(32); + Assert.AreEqual(32, _unit.SortedTxCount); + + var txs = _unit.GetSortedVerifiedTransactions(); + _unit.InvalidateVerifiedTransactions(); + + Assert.AreEqual(0, _unit.SortedTxCount); + + foreach (var tx in txs) + { + _unit.TryRemoveUnVerified(tx.Hash, out _); + } + + Assert.AreEqual(0, _unit.UnVerifiedCount); + } + + public static StorageKey CreateStorageKey(int id, byte prefix, byte[]? key = null) + { + byte[] buffer = GC.AllocateUninitializedArray(sizeof(byte) + (key?.Length ?? 0)); + buffer[0] = prefix; + key?.CopyTo(buffer.AsSpan(1)); + return new() + { + Id = id, + Key = buffer + }; + } + + [TestMethod] + public void TestTransactionAddedEvent() + { + // Arrange + bool eventRaised = false; + Transaction? capturedTx = null; + _unit.TransactionAdded += (sender, tx) => + { + eventRaised = true; + capturedTx = tx; + }; + + var tx = CreateTransaction(); + var snapshot = GetSnapshot(); + + // Act + _unit.TryAdd(tx, snapshot); + + // Assert + Assert.IsTrue(eventRaised, "TransactionAdded event should be raised"); + Assert.AreEqual(tx, capturedTx, "Transaction in event should match the added transaction"); + } + + [TestMethod] + public void TestTransactionRemovedEvent() + { + // Arrange + bool eventRaised = false; + TransactionRemovedEventArgs? capturedArgs = null; + _unit.TransactionRemoved += (sender, args) => + { + eventRaised = true; + capturedArgs = args; + }; + + // Add transactions to fill the pool to capacity + AddTransactions(_unit.Capacity); + + // Add one more to trigger capacity exceeded removal + var txToAdd = CreateTransaction(long.MaxValue); // High fee to ensure it gets added + var snapshot = GetSnapshot(); + + // Act + _unit.TryAdd(txToAdd, snapshot); + + // Assert + Assert.IsTrue(eventRaised, "TransactionRemoved event should be raised"); + Assert.IsNotNull(capturedArgs, "TransactionRemovedEventArgs should not be null"); + Assert.IsGreaterThan(0, capturedArgs.Transactions.Count, "Removed transactions should be included"); + Assert.AreEqual(TransactionRemovalReason.CapacityExceeded, capturedArgs?.Reason, + "Removal reason should be CapacityExceeded"); + } + + [TestMethod] + public void TestGetSortedVerifiedTransactionsWithCount() + { + // Arrange + AddTransactions(50); + + // Act - Get subset of transactions + var transactions10 = _unit.GetSortedVerifiedTransactions(10); + var transactions20 = _unit.GetSortedVerifiedTransactions(20); + var transactionsAll = _unit.GetSortedVerifiedTransactions(); + + // Assert + Assert.HasCount(10, transactions10, "Should return exactly 10 transactions"); + Assert.HasCount(20, transactions20, "Should return exactly 20 transactions"); + Assert.HasCount(50, transactionsAll, "Should return all transactions"); + + // Verify they are in the right order (highest fee first) + VerifyTransactionsSortedDescending(transactions10); + VerifyTransactionsSortedDescending(transactions20); + VerifyTransactionsSortedDescending(transactionsAll); + + // Verify the first 10 transactions in both sets are the same + for (int i = 0; i < 10; i++) + { + Assert.AreEqual(transactions10[i], transactions20[i], + "The first 10 transactions should be the same in both result sets"); + } + } + + [TestMethod] + public void TestComplexConflictScenario() + { + // Arrange + var snapshot = GetSnapshot(); + + // Create a chain of conflicting transactions + var tx1 = CreateTransaction(100000); + var tx2 = CreateTransaction(200000); + var tx3 = CreateTransaction(150000); + var tx4 = CreateTransaction(300000); + + // Set up conflicts: tx2 conflicts with tx1, tx3 conflicts with tx2, tx4 conflicts with tx3 + tx2.Attributes = [new Conflicts() { Hash = tx1.Hash }]; + tx3.Attributes = [new Conflicts() { Hash = tx2.Hash }]; + tx4.Attributes = [new Conflicts() { Hash = tx3.Hash }]; + + // Act & Assert - Add transactions in specific order to test conflict resolution + + // Add tx1 first + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(tx1, snapshot), "tx1 should be added successfully"); + Assert.HasCount(1, _unit, "Pool should contain 1 transaction"); + Assert.IsTrue(_unit.ContainsKey(tx1.Hash), "Pool should contain tx1"); + + // Add tx2 which conflicts with tx1 but has higher fee + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(tx2, snapshot), "tx2 should be added successfully"); + Assert.HasCount(1, _unit, "Pool should still contain 1 transaction (tx2 replaced tx1)"); + Assert.IsTrue(_unit.ContainsKey(tx2.Hash), "Pool should contain tx2"); + Assert.IsFalse(_unit.ContainsKey(tx1.Hash), "Pool should no longer contain tx1"); + + // Add tx3 which conflicts with tx2 but has lower fee + Assert.AreEqual(VerifyResult.HasConflicts, _unit.TryAdd(tx3, snapshot), "tx3 should not be added due to conflicts"); + Assert.HasCount(1, _unit, "Pool should still contain 1 transaction"); + Assert.IsTrue(_unit.ContainsKey(tx2.Hash), "Pool should still contain tx2"); + Assert.IsFalse(_unit.ContainsKey(tx3.Hash), "Pool should not contain tx3"); + + // Add tx4 which conflicts with tx3 (which is not in the pool) + Assert.AreEqual(VerifyResult.Succeed, _unit.TryAdd(tx4, snapshot), "tx4 should be added successfully"); + Assert.HasCount(2, _unit, "Pool should contain 2 transactions"); + Assert.IsTrue(_unit.ContainsKey(tx2.Hash), "Pool should contain tx2"); + Assert.IsTrue(_unit.ContainsKey(tx4.Hash), "Pool should contain tx4"); + } + + [TestMethod] + public void TestMultipleConflictsManagement() + { + // Arrange + var snapshot = GetSnapshot(); + + // Create a transaction with multiple conflicts + var tx1 = CreateTransaction(100000); + var tx2 = CreateTransaction(100000); + var tx3 = CreateTransaction(100000); + + // Create a transaction that conflicts with all three + var txMultiConflict = CreateTransaction(350000); // Higher fee than all three combined + txMultiConflict.Attributes = + [ + new Conflicts() { Hash = tx1.Hash }, + new Conflicts() { Hash = tx2.Hash }, + new Conflicts() { Hash = tx3.Hash } + ]; + + // Act + _unit.TryAdd(tx1, snapshot); + _unit.TryAdd(tx2, snapshot); + _unit.TryAdd(tx3, snapshot); + + Assert.HasCount(3, _unit, "Should have 3 transactions in the pool"); + + // Add the transaction with multiple conflicts + var result = _unit.TryAdd(txMultiConflict, snapshot); + + // Assert + Assert.AreEqual(VerifyResult.Succeed, result, "Transaction with multiple conflicts should be added"); + Assert.HasCount(1, _unit, "Should have 1 transaction in the pool after conflicts resolved"); + Assert.IsTrue(_unit.ContainsKey(txMultiConflict.Hash), "Pool should contain the transaction with higher fee"); + Assert.IsFalse(_unit.ContainsKey(tx1.Hash), "Pool should not contain tx1"); + Assert.IsFalse(_unit.ContainsKey(tx2.Hash), "Pool should not contain tx2"); + Assert.IsFalse(_unit.ContainsKey(tx3.Hash), "Pool should not contain tx3"); + } + + [TestMethod] + public void TestReverificationBehavior() + { + // Arrange + _unit = new MemoryPool(new NeoSystem(TestProtocolSettings.Default with { MemoryPoolMaxTransactions = 1000 }, storageProvider: (string?)null)); + + // Add transactions to the pool + AddTransactions(100); + + // Invalidate all verified transactions to move them to unverified + _unit.InvalidateVerifiedTransactions(); + Assert.AreEqual(0, _unit.VerifiedCount); + Assert.AreEqual(100, _unit.UnVerifiedCount); + + // Act + var snapshot = GetSnapshot(); + + // First batch - should reverify some transactions + _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(20, snapshot); + var verifiedInFirstBatch = _unit.VerifiedCount; + + // Second batch - should reverify more transactions + _unit.ReVerifyTopUnverifiedTransactionsIfNeeded(30, snapshot); + var verifiedInSecondBatch = _unit.VerifiedCount - verifiedInFirstBatch; + + // Assert + Assert.IsGreaterThan(0, verifiedInFirstBatch, "First batch should reverify some transactions"); + Assert.IsGreaterThan(0, verifiedInSecondBatch, "Second batch should reverify additional transactions"); + Assert.IsLessThan(100, _unit.VerifiedCount, "Not all transactions should be reverified in just two batches"); + + // Verify that transactions are reverified in fee order (highest fee first) + var verifiedTxs = _unit.GetSortedVerifiedTransactions(); + VerifyTransactionsSortedDescending(verifiedTxs); + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_PoolItem.cs b/tests/Neo.UnitTests/Ledger/UT_PoolItem.cs new file mode 100644 index 0000000000..172468e5f5 --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_PoolItem.cs @@ -0,0 +1,156 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_PoolItem.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Moq; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_PoolItem +{ + private static readonly Random TestRandom = new(1337); // use fixed seed for guaranteed determinism + + [TestInitialize] + public void TestSetup() + { + var timeValues = new[] { + new DateTime(1968, 06, 01, 0, 0, 1, DateTimeKind.Utc), + }; + + var timeMock = new Mock(); + timeMock.SetupGet(tp => tp.UtcNow).Returns(() => timeValues[0]) + .Callback(() => timeValues[0] = timeValues[0].Add(TimeSpan.FromSeconds(1))); + TimeProvider.Current = timeMock.Object; + } + + [TestCleanup] + public void TestCleanup() + { + // important to leave TimeProvider correct + TimeProvider.ResetToDefault(); + } + + [TestMethod] + public void PoolItem_CompareTo_Fee() + { + int size1 = 51; + int netFeeDatoshi1 = 1; + var tx1 = GenerateTx(netFeeDatoshi1, size1); + int size2 = 51; + int netFeeDatoshi2 = 2; + var tx2 = GenerateTx(netFeeDatoshi2, size2); + + var pitem1 = new PoolItem(tx1); + var pitem2 = new PoolItem(tx2); + + Console.WriteLine($"item1 time {pitem1.Timestamp} item2 time {pitem2.Timestamp}"); + // pitem1 < pitem2 (fee) => -1 + Assert.AreEqual(-1, pitem1.CompareTo(pitem2)); + // pitem2 > pitem1 (fee) => 1 + Assert.AreEqual(1, pitem2.CompareTo(pitem1)); + } + + [TestMethod] + public void PoolItem_CompareTo_Hash() + { + int sizeFixed = 51; + int netFeeDatoshiFixed = 1; + + var tx1 = GenerateTxWithFirstByteOfHashGreaterThanOrEqualTo(0x80, netFeeDatoshiFixed, sizeFixed); + var tx2 = GenerateTxWithFirstByteOfHashLessThanOrEqualTo(0x79, netFeeDatoshiFixed, sizeFixed); + + tx1.Attributes = new TransactionAttribute[] { new HighPriorityAttribute() }; + + var pitem1 = new PoolItem(tx1); + var pitem2 = new PoolItem(tx2); + + // Different priority + Assert.AreEqual(-1, pitem2.CompareTo(pitem1)); + + // Bulk test + for (int testRuns = 0; testRuns < 30; testRuns++) + { + tx1 = GenerateTxWithFirstByteOfHashGreaterThanOrEqualTo(0x80, netFeeDatoshiFixed, sizeFixed); + tx2 = GenerateTxWithFirstByteOfHashLessThanOrEqualTo(0x79, netFeeDatoshiFixed, sizeFixed); + + pitem1 = new PoolItem(tx1); + pitem2 = new PoolItem(tx2); + + // pitem2.tx.Hash < pitem1.tx.Hash => 1 descending order + Assert.AreEqual(1, pitem2.CompareTo(pitem1)); + + // pitem2.tx.Hash > pitem1.tx.Hash => -1 descending order + Assert.AreEqual(-1, pitem1.CompareTo(pitem2)); + } + } + + [TestMethod] + public void PoolItem_CompareTo_Equals() + { + int sizeFixed = 500; + int netFeeDatoshiFixed = 10; + var tx = GenerateTx(netFeeDatoshiFixed, sizeFixed, new byte[] { 0x13, 0x37 }); + + var pitem1 = new PoolItem(tx); + var pitem2 = new PoolItem(tx); + + // pitem1 == pitem2 (fee) => 0 + Assert.AreEqual(0, pitem1.CompareTo(pitem2)); + Assert.AreEqual(0, pitem2.CompareTo(pitem1)); + Assert.AreEqual(1, pitem2.CompareTo((PoolItem?)null)); + } + + public static Transaction GenerateTxWithFirstByteOfHashGreaterThanOrEqualTo(byte firstHashByte, long networkFee, int size) + { + Transaction tx; + do + { + tx = GenerateTx(networkFee, size); + } while (tx.Hash < new UInt256(TestUtils.GetByteArray(32, firstHashByte))); + + return tx; + } + + public static Transaction GenerateTxWithFirstByteOfHashLessThanOrEqualTo(byte firstHashByte, long networkFee, int size) + { + Transaction tx; + do + { + tx = GenerateTx(networkFee, size); + } while (tx.Hash > new UInt256(TestUtils.GetByteArray(32, firstHashByte))); + + return tx; + } + + // Generate Transaction with different sizes and prices + public static Transaction GenerateTx(long networkFee, int size, byte[]? overrideScriptBytes = null) + { + var tx = new Transaction + { + Nonce = (uint)TestRandom.Next(), + Script = overrideScriptBytes ?? ReadOnlyMemory.Empty, + NetworkFee = networkFee, + Attributes = [], + Signers = [], + Witnesses = [Witness.Empty] + }; + + Assert.IsEmpty(tx.Attributes); + Assert.IsEmpty(tx.Signers); + + int diff = size - tx.Size; + if (diff < 0) throw new ArgumentException($"The size({size}) cannot be less than the Transaction.Size({tx.Size})."); + if (diff > 0) tx.Witnesses[0].VerificationScript = new byte[diff]; + return tx; + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_StorageItem.cs b/tests/Neo.UnitTests/Ledger/UT_StorageItem.cs new file mode 100644 index 0000000000..448283a348 --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_StorageItem.cs @@ -0,0 +1,119 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_StorageItem.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Text; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_StorageItem +{ + StorageItem uut = null!; + + [TestInitialize] + public void TestSetup() + { + uut = new StorageItem(); + } + + [TestMethod] + public void Value_Get() + { + Assert.IsTrue(uut.Value.IsEmpty); + } + + [TestMethod] + public void Value_Set() + { + byte[] val = new byte[] { 0x42, 0x32 }; + uut.Value = val; + Assert.AreEqual(2, uut.Value.Length); + Assert.AreEqual(val[0], uut.Value.Span[0]); + Assert.AreEqual(val[1], uut.Value.Span[1]); + } + + [TestMethod] + public void Size_Get() + { + uut.Value = TestUtils.GetByteArray(10, 0x42); + Assert.AreEqual(11, uut.Size); // 1 + 10 + } + + [TestMethod] + public void Size_Get_Larger() + { + uut.Value = TestUtils.GetByteArray(88, 0x42); + Assert.AreEqual(89, uut.Size); // 1 + 88 + } + + [TestMethod] + public void Clone() + { + uut.Value = TestUtils.GetByteArray(10, 0x42); + + StorageItem newSi = uut.Clone(); + var span = newSi.Value.Span; + Assert.AreEqual(10, span.Length); + Assert.AreEqual(0x42, span[0]); + for (int i = 1; i < 10; i++) + { + Assert.AreEqual(0x20, span[i]); + } + } + + [TestMethod] + public void Deserialize() + { + byte[] data = new byte[] { 66, 32, 32, 32, 32, 32, 32, 32, 32, 32 }; + MemoryReader reader = new(data); + uut.Deserialize(ref reader); + var span = uut.Value.Span; + Assert.AreEqual(10, span.Length); + Assert.AreEqual(0x42, span[0]); + for (int i = 1; i < 10; i++) + { + Assert.AreEqual(0x20, span[i]); + } + } + + [TestMethod] + public void Serialize() + { + uut.Value = TestUtils.GetByteArray(10, 0x42); + + byte[] data; + using (var stream = new MemoryStream()) + { + using var writer = new BinaryWriter(stream, Encoding.ASCII, true); + uut.Serialize(writer); + data = stream.ToArray(); + } + + byte[] requiredData = new byte[] { 66, 32, 32, 32, 32, 32, 32, 32, 32, 32 }; + + Assert.HasCount(requiredData.Length, data); + for (int i = 0; i < requiredData.Length; i++) + { + Assert.AreEqual(requiredData[i], data[i]); + } + } + + [TestMethod] + public void TestFromReplica() + { + uut.Value = TestUtils.GetByteArray(10, 0x42); + var dest = new StorageItem(); + dest.FromReplica(uut); + CollectionAssert.AreEqual(uut.Value.ToArray(), dest.Value.ToArray()); + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs b/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs new file mode 100644 index 0000000000..fe323d3548 --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs @@ -0,0 +1,115 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_StorageKey.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_StorageKey +{ + [TestMethod] + public void Id_Get() + { + var uut = new StorageKey { Id = 1, Key = new byte[] { 0x01 } }; + Assert.AreEqual(1, uut.Id); + } + + [TestMethod] + public void Id_Set() + { + int val = 1; + StorageKey uut = new() { Id = val }; + Assert.AreEqual(val, uut.Id); + } + + [TestMethod] + public void Key_Set() + { + byte[] val = new byte[] { 0x42, 0x32 }; + StorageKey uut = new() { Key = val }; + Assert.AreEqual(2, uut.Key.Length); + Assert.AreEqual(val[0], uut.Key.Span[0]); + Assert.AreEqual(val[1], uut.Key.Span[1]); + } + + [TestMethod] + public void Equals_SameObj() + { + StorageKey uut = new(); + Assert.IsTrue(uut.Equals(uut)); + } + + [TestMethod] + public void Equals_Null() + { + StorageKey uut = new(); + Assert.IsFalse(uut.Equals(null)); + } + + [TestMethod] + public void Equals_SameHash_SameKey() + { + int val = 0x42000000; + byte[] keyVal = TestUtils.GetByteArray(10, 0x42); + var newSk = new StorageKey + { + Id = val, + Key = keyVal + }; + StorageKey uut = new() { Id = val, Key = keyVal }; + Assert.IsTrue(uut.Equals(newSk)); + } + + [TestMethod] + public void Equals_DiffHash_SameKey() + { + int val = 0x42000000; + byte[] keyVal = TestUtils.GetByteArray(10, 0x42); + var newSk = new StorageKey + { + Id = val, + Key = keyVal + }; + StorageKey uut = new() { Id = 0x78000000, Key = keyVal }; + Assert.IsFalse(uut.Equals(newSk)); + } + + [TestMethod] + public void Equals_SameHash_DiffKey() + { + int val = 0x42000000; + byte[] keyVal = TestUtils.GetByteArray(10, 0x42); + var newSk = new StorageKey + { + Id = val, + Key = keyVal + }; + StorageKey uut = new() { Id = val, Key = TestUtils.GetByteArray(10, 0x88) }; + Assert.IsFalse(uut.Equals(newSk)); + } + + [TestMethod] + public void GetHashCode_Get() + { + var data = TestUtils.GetByteArray(10, 0x42); + StorageKey uut = new() { Id = 0x42000000, Key = data }; + Assert.AreEqual(HashCode.Combine(0x42000000, data.XxHash3_32()), uut.GetHashCode()); + } + + [TestMethod] + public void Equals_Obj() + { + StorageKey uut = new(); + Assert.IsFalse(uut.Equals(1u)); + Assert.IsTrue(uut.Equals((object)uut)); + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_TransactionState.cs b/tests/Neo.UnitTests/Ledger/UT_TransactionState.cs new file mode 100644 index 0000000000..65ecc347e6 --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_TransactionState.cs @@ -0,0 +1,126 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_TransactionState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using Array = System.Array; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_TransactionState +{ + private TransactionState _origin = null!; + private TransactionState _originTrimmed = null!; + + [TestInitialize] + public void Initialize() + { + _origin = new TransactionState + { + BlockIndex = 1, + Transaction = new Transaction() + { + Nonce = RandomNumberFactory.NextUInt32(), + Attributes = [], + Script = new byte[] { (byte)OpCode.PUSH1 }, + Signers = [new() { Account = UInt160.Zero }], + Witnesses = [Witness.Empty] + } + }; + _originTrimmed = new TransactionState + { + BlockIndex = 1, + }; + } + + [TestMethod] + public void TestDeserialize() + { + var data = BinarySerializer.Serialize(((IInteroperable)_origin).ToStackItem(null), ExecutionEngineLimits.Default); + var reader = new MemoryReader(data); + + TransactionState dest = new(); + ((IInteroperable)dest).FromStackItem(BinarySerializer.Deserialize(ref reader, ExecutionEngineLimits.Default, null)); + + Assert.AreEqual(_origin.BlockIndex, dest.BlockIndex); + Assert.AreEqual(_origin.Transaction!.Hash, dest.Transaction!.Hash); + Assert.IsNotNull(dest.Transaction); + } + + [TestMethod] + public void TestClone() + { + var clone = (TransactionState)((IInteroperable)_origin).Clone(); + CollectionAssert.AreEqual( + BinarySerializer.Serialize(((IInteroperable)clone).ToStackItem(null), ExecutionEngineLimits.Default), + BinarySerializer.Serialize((_origin as IInteroperable).ToStackItem(null), ExecutionEngineLimits.Default) + ); + clone.Transaction!.Nonce++; + Assert.AreNotEqual(clone.Transaction.Nonce, _origin.Transaction!.Nonce); + CollectionAssert.AreNotEqual( + BinarySerializer.Serialize((clone as IInteroperable).ToStackItem(null), ExecutionEngineLimits.Default), + BinarySerializer.Serialize((_origin as IInteroperable).ToStackItem(null), ExecutionEngineLimits.Default) + ); + } + + [TestMethod] + public void AvoidReplicaBug() + { + var replica = new TransactionState(); + (replica as IInteroperable).FromReplica(_origin); + Assert.AreEqual(replica.Transaction!.Nonce, _origin.Transaction!.Nonce); + CollectionAssert.AreEqual( + ((Struct)((IInteroperable)replica).ToStackItem(null))[1].GetSpan().ToArray(), + ((Struct)((IInteroperable)_origin).ToStackItem(null))[1].GetSpan().ToArray()); + + var newOrigin = new TransactionState + { + BlockIndex = 2, + Transaction = new Transaction() + { + Nonce = RandomNumberFactory.NextUInt32(), + NetworkFee = _origin.Transaction.NetworkFee++, // more fee + Attributes = [], + Script = new byte[] { (byte)OpCode.PUSH1 }, + Signers = [new() { Account = UInt160.Zero }], + Witnesses = [ new Witness() { + InvocationScript=Array.Empty(), + VerificationScript=Array.Empty() + } ] + } + }; + (replica as IInteroperable).FromReplica(newOrigin); + Assert.AreEqual(replica.Transaction.Nonce, newOrigin.Transaction.Nonce); + Assert.AreEqual(replica.Transaction.NetworkFee, newOrigin.Transaction.NetworkFee); + CollectionAssert.AreEqual( + ((Struct)((IInteroperable)replica).ToStackItem(null))[1].GetSpan().ToArray(), + ((Struct)((IInteroperable)newOrigin).ToStackItem(null))[1].GetSpan().ToArray()); + } + + [TestMethod] + public void TestDeserializeTrimmed() + { + var data = BinarySerializer.Serialize(((IInteroperable)_originTrimmed).ToStackItem(null), ExecutionEngineLimits.Default); + var reader = new MemoryReader(data); + + TransactionState dest = new(); + ((IInteroperable)dest).FromStackItem(BinarySerializer.Deserialize(ref reader, ExecutionEngineLimits.Default, null)); + + Assert.AreEqual(_originTrimmed.BlockIndex, dest.BlockIndex); + Assert.IsNull(dest.Transaction); + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs b/tests/Neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs new file mode 100644 index 0000000000..93ce63039b --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs @@ -0,0 +1,111 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_TransactionVerificationContext.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Moq; +using Neo.Factories; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using System.Numerics; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_TransactionVerificationContext +{ + private static Transaction CreateTransactionWithFee(long networkFee, long systemFee) + { + var randomBytes = RandomNumberFactory.NextBytes(16); + Mock mock = new(); + mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())).Returns(VerifyResult.Succeed); + mock.Setup(p => p.VerifyStateIndependent(It.IsAny())).Returns(VerifyResult.Succeed); + mock.Object.Script = randomBytes; + mock.Object.NetworkFee = networkFee; + mock.Object.SystemFee = systemFee; + mock.Object.Signers = [new() { Account = UInt160.Zero }]; + mock.Object.Attributes = []; + mock.Object.Witnesses = [Witness.Empty]; + return mock.Object; + } + + [TestMethod] + public async Task TestDuplicateOracle() + { + // Fake balance + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default, gas: long.MaxValue); + BigInteger balance = NativeContract.GAS.BalanceOf(snapshotCache, UInt160.Zero); + await NativeContract.GAS.Burn(engine, UInt160.Zero, balance); + _ = NativeContract.GAS.Mint(engine, UInt160.Zero, 8, false); + + // Test + TransactionVerificationContext verificationContext = new(); + var tx = CreateTransactionWithFee(1, 2); + tx.Attributes = [new OracleResponse() { Code = OracleResponseCode.ConsensusUnreachable, Id = 1, Result = Array.Empty() }]; + var conflicts = new List(); + Assert.IsTrue(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + verificationContext.AddTransaction(tx); + + tx = CreateTransactionWithFee(2, 1); + tx.Attributes = [new OracleResponse() { Code = OracleResponseCode.ConsensusUnreachable, Id = 1, Result = Array.Empty() }]; + Assert.IsFalse(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + } + + [TestMethod] + public async Task TestTransactionSenderFee() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default, gas: long.MaxValue); + BigInteger balance = NativeContract.GAS.BalanceOf(snapshotCache, UInt160.Zero); + await NativeContract.GAS.Burn(engine, UInt160.Zero, balance); + _ = NativeContract.GAS.Mint(engine, UInt160.Zero, 8, true); + + TransactionVerificationContext verificationContext = new(); + var tx = CreateTransactionWithFee(1, 2); + var conflicts = new List(); + Assert.IsTrue(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + verificationContext.AddTransaction(tx); + Assert.IsTrue(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + verificationContext.AddTransaction(tx); + Assert.IsFalse(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + verificationContext.RemoveTransaction(tx); + Assert.IsTrue(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + verificationContext.AddTransaction(tx); + Assert.IsFalse(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + } + + [TestMethod] + public async Task TestTransactionSenderFeeWithConflicts() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default, gas: long.MaxValue); + BigInteger balance = NativeContract.GAS.BalanceOf(snapshotCache, UInt160.Zero); + await NativeContract.GAS.Burn(engine, UInt160.Zero, balance); + _ = NativeContract.GAS.Mint(engine, UInt160.Zero, 3 + 3 + 1, true); // balance is enough for 2 transactions and 1 GAS is left. + + TransactionVerificationContext verificationContext = new(); + var tx = CreateTransactionWithFee(1, 2); + var conflictingTx = CreateTransactionWithFee(1, 1); // costs 2 GAS + + var conflicts = new List(); + Assert.IsTrue(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + verificationContext.AddTransaction(tx); + Assert.IsTrue(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + verificationContext.AddTransaction(tx); + Assert.IsFalse(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); + + conflicts.Add(conflictingTx); + Assert.IsTrue(verificationContext.CheckTransaction(tx, conflicts, snapshotCache)); // 1 GAS is left on the balance + 2 GAS is free after conflicts removal => enough for one more trasnaction. + } +} diff --git a/tests/Neo.UnitTests/Ledger/UT_TrimmedBlock.cs b/tests/Neo.UnitTests/Ledger/UT_TrimmedBlock.cs new file mode 100644 index 0000000000..76afd81fd8 --- /dev/null +++ b/tests/Neo.UnitTests/Ledger/UT_TrimmedBlock.cs @@ -0,0 +1,133 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_TrimmedBlock.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.Ledger; + +[TestClass] +public class UT_TrimmedBlock +{ + public static TrimmedBlock GetTrimmedBlockWithNoTransaction() + { + return new TrimmedBlock + { + Header = new Header + { + MerkleRoot = UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff02"), + PrevHash = UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff01"), + Timestamp = new DateTime(1988, 06, 01, 0, 0, 0, DateTimeKind.Utc).ToTimestamp(), + Index = 1, + NextConsensus = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01"), + Witness = new Witness + { + InvocationScript = ReadOnlyMemory.Empty, + VerificationScript = new[] { (byte)OpCode.PUSH1 } + }, + }, + Hashes = [] + }; + } + + [TestMethod] + public void TestGetBlock() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var tx1 = TestUtils.GetTransaction(UInt160.Zero); + tx1.Script = new byte[] { 0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01 }; + var state1 = new TransactionState + { + Transaction = tx1, + BlockIndex = 1 + }; + var tx2 = TestUtils.GetTransaction(UInt160.Zero); + tx2.Script = new byte[] { 0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x02 }; + var state2 = new TransactionState + { + Transaction = tx2, + BlockIndex = 1 + }; + TestUtils.TransactionAdd(snapshotCache, state1, state2); + + TrimmedBlock tblock = GetTrimmedBlockWithNoTransaction(); + tblock.Hashes = new UInt256[] { tx1.Hash, tx2.Hash }; + TestUtils.BlocksAdd(snapshotCache, tblock.Hash, tblock); + + Block block = NativeContract.Ledger.GetBlock(snapshotCache, tblock.Hash)!; + + Assert.AreEqual((uint)1, block.Index); + Assert.AreEqual(UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff02"), block.MerkleRoot); + Assert.HasCount(2, block.Transactions); + Assert.AreEqual(tx1.Hash, block.Transactions[0].Hash); + Assert.AreEqual(tblock.Header.Witness.InvocationScript.Span.ToHexString(), block.Witness.InvocationScript.Span.ToHexString()); + Assert.AreEqual(tblock.Header.Witness.VerificationScript.Span.ToHexString(), block.Witness.VerificationScript.Span.ToHexString()); + } + + [TestMethod] + public void TestClone() + { + var block = GetTrimmedBlockWithNoTransaction(); + var clone = (TrimmedBlock)((IInteroperable)block).Clone(); + CollectionAssert.AreEqual( + BinarySerializer.Serialize(((IInteroperable)clone).ToStackItem(null), ExecutionEngineLimits.Default), + BinarySerializer.Serialize(((IInteroperable)block).ToStackItem(null), ExecutionEngineLimits.Default)); + clone.Header.Index++; + Assert.AreNotEqual(clone.Header.Index, block.Header.Index); + CollectionAssert.AreNotEqual( + BinarySerializer.Serialize(((IInteroperable)clone).ToStackItem(null), ExecutionEngineLimits.Default), + BinarySerializer.Serialize(((IInteroperable)block).ToStackItem(null), ExecutionEngineLimits.Default)); + } + + [TestMethod] + public void TestGetHeader() + { + TrimmedBlock tblock = GetTrimmedBlockWithNoTransaction(); + Header header = tblock.Header; + Assert.AreEqual(UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff01"), header.PrevHash); + Assert.AreEqual(UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff02"), header.MerkleRoot); + } + + [TestMethod] + public void TestGetSize() + { + TrimmedBlock tblock = GetTrimmedBlockWithNoTransaction(); + tblock.Hashes = new UInt256[] { TestUtils.GetTransaction(UInt160.Zero).Hash }; + Assert.AreEqual(146, tblock.Size); // 138 + 8 + } + + [TestMethod] + public void TestDeserialize() + { + TrimmedBlock tblock = GetTrimmedBlockWithNoTransaction(); + tblock.Hashes = new UInt256[] { TestUtils.GetTransaction(UInt160.Zero).Hash }; + var newBlock = (TrimmedBlock)RuntimeHelpers.GetUninitializedObject(typeof(TrimmedBlock)); + using (MemoryStream ms = new(1024)) + using (BinaryWriter writer = new(ms)) + { + tblock.Serialize(writer); + MemoryReader reader = new(ms.ToArray()); + newBlock.Deserialize(ref reader); + } + Assert.HasCount(newBlock.Hashes.Length, tblock.Hashes); + Assert.AreEqual(newBlock.Header.ToJson(ProtocolSettings.Default).ToString(), tblock.Header.ToJson(TestProtocolSettings.Default).ToString()); + } +} diff --git a/tests/Neo.UnitTests/Neo.UnitTests.csproj b/tests/Neo.UnitTests/Neo.UnitTests.csproj new file mode 100644 index 0000000000..31c8bd194b --- /dev/null +++ b/tests/Neo.UnitTests/Neo.UnitTests.csproj @@ -0,0 +1,26 @@ + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest + + + + + + + + + diff --git a/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_ArchivalNodeCapability.cs b/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_ArchivalNodeCapability.cs new file mode 100644 index 0000000000..51c864d54d --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_ArchivalNodeCapability.cs @@ -0,0 +1,53 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ArchivalNodeCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Capabilities; + +namespace Neo.UnitTests.Network.P2P.Capabilities; + +[TestClass] +public class UT_ArchivalNodeCapability +{ + [TestMethod] + public void Size_Get() + { + var test = new ArchivalNodeCapability(); + Assert.AreEqual(2, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = new ArchivalNodeCapability(); + var buffer = test.ToArray(); + + var br = new MemoryReader(buffer); + var clone = (ArchivalNodeCapability)NodeCapability.DeserializeFrom(ref br); + + Assert.AreEqual(test.Type, clone.Type); + buffer[1] = 0x01; + br = new MemoryReader(buffer); + + var exceptionHappened = false; + // CS8175 prevents from using Assert.ThrowsException here + try + { + NodeCapability.DeserializeFrom(ref br); + } + catch (FormatException) + { + exceptionHappened = true; + } + Assert.IsTrue(exceptionHappened); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_FullNodeCapability.cs b/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_FullNodeCapability.cs new file mode 100644 index 0000000000..4cc89b913f --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_FullNodeCapability.cs @@ -0,0 +1,42 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_FullNodeCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Capabilities; + +namespace Neo.UnitTests.Network.P2P.Capabilities; + +[TestClass] +public class UT_FullNodeCapability +{ + [TestMethod] + public void Size_Get() + { + var test = new FullNodeCapability() { StartHeight = 1 }; + Assert.AreEqual(5, test.Size); + + test = new FullNodeCapability(2); + Assert.AreEqual(5, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = new FullNodeCapability() { StartHeight = uint.MaxValue }; + var buffer = test.ToArray(); + + var br = new MemoryReader(buffer); + var clone = (FullNodeCapability)NodeCapability.DeserializeFrom(ref br); + + Assert.AreEqual(test.StartHeight, clone.StartHeight); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_ServerCapability.cs b/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_ServerCapability.cs new file mode 100644 index 0000000000..b772399518 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_ServerCapability.cs @@ -0,0 +1,77 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ServerCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Capabilities; + +namespace Neo.UnitTests.Network.P2P.Capabilities; + +[TestClass] +public class UT_ServerCapability +{ + [TestMethod] + public void Size_Get() + { + var test = new ServerCapability(NodeCapabilityType.TcpServer) { Port = 1 }; + Assert.AreEqual(3, test.Size); + +#pragma warning disable CS0618 // Type or member is obsolete + test = new ServerCapability(NodeCapabilityType.WsServer) { Port = 2 }; +#pragma warning restore CS0618 // Type or member is obsolete + Assert.AreEqual(3, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { +#pragma warning disable CS0618 // Type or member is obsolete + var test = new ServerCapability(NodeCapabilityType.WsServer) { Port = 2 }; +#pragma warning restore CS0618 // Type or member is obsolete + var buffer = test.ToArray(); + + var br = new MemoryReader(buffer); + var clone = (ServerCapability)NodeCapability.DeserializeFrom(ref br); + + Assert.AreEqual(test.Port, clone.Port); + Assert.AreEqual(test.Type, clone.Type); + +#pragma warning disable CS0618 // Type or member is obsolete + clone = new ServerCapability(NodeCapabilityType.WsServer, 123); +#pragma warning restore CS0618 // Type or member is obsolete + br = new MemoryReader(buffer); + ((ISerializable)clone).Deserialize(ref br); + + Assert.AreEqual(test.Port, clone.Port); + Assert.AreEqual(test.Type, clone.Type); + + clone = new ServerCapability(NodeCapabilityType.TcpServer, 123); + + Assert.ThrowsExactly(() => MemoryReaderDeserialize(buffer, clone)); + Assert.ThrowsExactly(() => new ServerCapability(NodeCapabilityType.FullNode)); + + // Wrong type + buffer[0] = 0xFF; + Assert.ThrowsExactly(() => MemoryReaderDeserializeFrom(buffer)); + + static void MemoryReaderDeserialize(byte[] buffer, ISerializable obj) + { + var reader = new MemoryReader(buffer); + obj.Deserialize(ref reader); + } + + static void MemoryReaderDeserializeFrom(byte[] buffer) + { + var reader = new MemoryReader(buffer); + NodeCapability.DeserializeFrom(ref reader); + } + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_UnknownCapability.cs b/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_UnknownCapability.cs new file mode 100644 index 0000000000..001a97fceb --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Capabilities/UT_UnknownCapability.cs @@ -0,0 +1,32 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_UnknownCapability.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Capabilities; + +namespace Neo.UnitTests.Network.P2P.Capabilities; + +[TestClass] +public class UT_UnknownCapability +{ + [TestMethod] + public void DeserializeUnknown() + { + var buffer = new byte[] { 0xff, 0x03, 0x01, 0x02, 0x03 }; // Type 0xff, three bytes of data. + + var br = new MemoryReader(buffer); + var capab = (NodeCapability)NodeCapability.DeserializeFrom(ref br); + + Assert.IsTrue(capab is UnknownCapability); + CollectionAssert.AreEqual(buffer, capab.ToArray()); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_AddrPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_AddrPayload.cs new file mode 100644 index 0000000000..0277149b74 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_AddrPayload.cs @@ -0,0 +1,46 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_AddrPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using System.Net; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_AddrPayload +{ + [TestMethod] + public void Size_Get() + { + var test = new AddrPayload() { AddressList = [] }; + Assert.AreEqual(1, test.Size); + + test = AddrPayload.Create([new NetworkAddressWithTime() { Address = IPAddress.Any, Capabilities = [], Timestamp = 1 }]); + Assert.AreEqual(22, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = AddrPayload.Create([new NetworkAddressWithTime() + { + Address = IPAddress.Any, + Capabilities = [], + Timestamp = 1 + }]); + var clone = test.ToArray().AsSerializable(); + CollectionAssert.AreEqual(test.AddressList.Select(u => u.EndPoint).ToArray(), clone.AddressList.Select(u => u.EndPoint).ToArray()); + + Assert.ThrowsExactly(() => _ = new AddrPayload() { AddressList = [] }.ToArray().AsSerializable()); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Block.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Block.cs new file mode 100644 index 0000000000..f011a2fe74 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Block.cs @@ -0,0 +1,199 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Block.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_Block +{ + private static readonly string s_blockHex = + "0000000000000000000000000000000000000000000000000000000000000000000000006c23be5d326" + + "79baa9c5c2aa0d329fd2a2441d7875d0f34d42f58f70428fbbbb9493ed0e58f01000000000000000000" + + "00000000000000000000000000000000000000000000000000000100011101000000000000000000000" + + "0000000000000000000000000000001000000000000000000000000000000000000000001000112010000"; + + private static ApplicationEngine GetEngine(bool hasContainer = false, bool hasSnapshot = false, + bool hasBlock = false, bool addScript = true, long gas = 20_00000000) + { + var system = TestBlockchain.GetSystem(); + var tx = hasContainer ? TestUtils.GetTransaction(UInt160.Zero) : null; + var snapshotCache = hasSnapshot ? system.GetTestSnapshotCache() : null; + var block = hasBlock ? new Block + { + Header = new Header + { + PrevHash = null!, + MerkleRoot = null!, + NextConsensus = null!, + Witness = null! + }, + Transactions = null! + } : null; + var engine = ApplicationEngine.Create(TriggerType.Application, + tx, snapshotCache!, block, system.Settings, gas: gas); + if (addScript) engine.LoadScript(new byte[] { 0x01 }); + return engine; + } + + [TestMethod] + public void Header_Get() + { + var uut = TestUtils.MakeBlock(null, UInt256.Zero, 0); + Assert.IsNotNull(uut.Header); + Assert.AreEqual(UInt256.Zero, uut.Header.PrevHash); + } + + [TestMethod] + public void Size_Get() + { + var uut = TestUtils.MakeBlock(null, UInt256.Zero, 0); + // header 4 + 32 + 32 + 8 + 4 + 1 + 20 + 4 + // tx 1 + Assert.AreEqual(114, uut.Size); // 106 + nonce + } + + [TestMethod] + public void Size_Get_1_Transaction() + { + var uut = TestUtils.MakeBlock(null, UInt256.Zero, 1); + uut.Transactions = + [ + TestUtils.GetTransaction(UInt160.Zero) + ]; + + Assert.AreEqual(167, uut.Size); // 159 + nonce + } + + [TestMethod] + public void Size_Get_3_Transaction() + { + var uut = TestUtils.MakeBlock(null, UInt256.Zero, 3); + uut.Transactions = + [ + TestUtils.GetTransaction(UInt160.Zero), + TestUtils.GetTransaction(UInt160.Zero), + TestUtils.GetTransaction(UInt160.Zero) + ]; + + Assert.AreEqual(273, uut.Size); // 265 + nonce + } + + [TestMethod] + public void Serialize() + { + var uut = TestUtils.MakeBlock(null, UInt256.Zero, 1); + Assert.AreEqual(s_blockHex, uut.ToArray().ToHexString()); + } + + [TestMethod] + public void Deserialize() + { + var uut = TestUtils.MakeBlock(null, UInt256.Zero, 1); + MemoryReader reader = new(s_blockHex.HexToBytes()); + uut.Deserialize(ref reader); + var merkRoot = uut.MerkleRoot; + + Assert.AreEqual(merkRoot, uut.MerkleRoot); + } + + [TestMethod] + public void Equals_SameObj() + { + var uut = (Block)RuntimeHelpers.GetUninitializedObject(typeof(Block)); + Assert.IsTrue(uut.Equals(uut)); + + var obj = uut as object; + Assert.IsTrue(uut.Equals(obj)); + } + + [TestMethod] + public void TestGetHashCode() + { + var snapshot = GetEngine(true, true).SnapshotCache; + Assert.AreEqual(-626492395, NativeContract.Ledger.GetBlock(snapshot, 0)!.GetHashCode()); + } + + [TestMethod] + public void Equals_DiffObj() + { + var prevHash = new UInt256(TestUtils.GetByteArray(32, 0x42)); + var block = TestUtils.MakeBlock(null, UInt256.Zero, 1); + var uut = TestUtils.MakeBlock(null, prevHash, 0); + + Assert.IsFalse(uut.Equals(block)); + } + + [TestMethod] + public void Equals_Null() + { + var uut = (Block)RuntimeHelpers.GetUninitializedObject(typeof(Block)); + Assert.IsFalse(uut.Equals(null)); + } + + [TestMethod] + public void Equals_SameHash() + { + var prevHash = new UInt256(TestUtils.GetByteArray(32, 0x42)); + var block = TestUtils.MakeBlock(null, prevHash, 1); + var uut = TestUtils.MakeBlock(null, prevHash, 1); + Assert.IsTrue(uut.Equals(block)); + } + + [TestMethod] + public void ToJson() + { + var uut = TestUtils.MakeBlock(null, UInt256.Zero, 1); + var jObj = uut.ToJson(TestProtocolSettings.Default); + Assert.IsNotNull(jObj); + Assert.AreEqual("0x942065e93848732c2e7844061fa92d20c5d9dc0bc71d420a1ea71b3431fc21b4", jObj["hash"]!.AsString()); + Assert.AreEqual(167, jObj["size"]!.AsNumber()); // 159 + nonce + Assert.AreEqual(0, jObj["version"]!.AsNumber()); + Assert.AreEqual("0x0000000000000000000000000000000000000000000000000000000000000000", jObj["previousblockhash"]!.AsString()); + Assert.AreEqual("0xb9bbfb2804f7582fd4340f5d87d741242afd29d3a02a5c9caa9b67325dbe236c", jObj["merkleroot"]!.AsString()); + Assert.AreEqual(uut.Header.Timestamp, jObj["time"]!.AsNumber()); + Assert.AreEqual(uut.Header.Nonce.ToString("X16"), jObj["nonce"]!.AsString()); + Assert.AreEqual(uut.Header.Index, jObj["index"]!.AsNumber()); + Assert.AreEqual("NKuyBkoGdZZSLyPbJEetheRhMjeznFZszf", jObj["nextconsensus"]!.AsString()); + + var scObj = (JObject)jObj["witnesses"]![0]!; + Assert.AreEqual("", scObj["invocation"]!.AsString()); + Assert.AreEqual("EQ==", scObj["verification"]!.AsString()); + + Assert.IsNotNull(jObj["tx"]); + var txObj = (JObject)jObj["tx"]![0]!; + Assert.AreEqual("0xb9bbfb2804f7582fd4340f5d87d741242afd29d3a02a5c9caa9b67325dbe236c", txObj["hash"]!.AsString()); + Assert.AreEqual(53, txObj["size"]!.AsNumber()); + Assert.AreEqual(0, txObj["version"]!.AsNumber()); + Assert.IsEmpty((JArray)txObj["attributes"]!); + Assert.AreEqual("0", txObj["netfee"]!.AsString()); + } + + [TestMethod] + public void Witness() + { + IVerifiable item = new Block() + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = [] + }; + Assert.HasCount(1, item.Witnesses); + void Actual() => item.Witnesses = []; + Assert.ThrowsExactly(Actual); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Conflicts.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Conflicts.cs new file mode 100644 index 0000000000..352935f2d3 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Conflicts.cs @@ -0,0 +1,113 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Conflicts.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.UnitTests.Ledger; +using Neo.VM; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_Conflicts +{ + private const byte Prefix_Transaction = 11; + private static readonly UInt256 _u = new(new byte[32] { + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01 + }); + + private static Conflicts CreateConflictsPayload() + { + return new Conflicts() { Hash = _u }; + } + + [TestMethod] + public void Size_Get() + { + var test = CreateConflictsPayload(); + Assert.AreEqual(1 + 32, test.Size); + } + + [TestMethod] + public void ToJson() + { + var test = CreateConflictsPayload(); + var json = test.ToJson().ToString(); + Assert.AreEqual(@"{""type"":""Conflicts"",""hash"":""0x0101010101010101010101010101010101010101010101010101010101010101""}", json); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = CreateConflictsPayload(); + + var clone = test.ToArray().AsSerializable(); + Assert.AreEqual(clone.Type, test.Type); + + // As transactionAttribute + byte[] buffer = test.ToArray(); + var reader = new MemoryReader(buffer); + clone = (Conflicts)TransactionAttribute.DeserializeFrom(ref reader); + Assert.AreEqual(clone.Type, test.Type); + + // Wrong type + buffer[0] = 0xff; + Assert.ThrowsExactly(() => MemoryReaderDeserializeFrom(buffer)); + + static void MemoryReaderDeserializeFrom(byte[] buffer) + { + var reader = new MemoryReader(buffer); + TransactionAttribute.DeserializeFrom(ref reader); + } + } + + [TestMethod] + public void Verify() + { + var test = CreateConflictsPayload(); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var key = UT_MemoryPool.CreateStorageKey(NativeContract.Ledger.Id, Prefix_Transaction, _u.ToArray()); + + // Conflicting transaction is in the Conflicts attribute of some other on-chain transaction. + var tx = new Transaction() + { + Script = new byte[] { (byte)OpCode.RET }, + Witnesses = [Witness.Empty], + Signers = [new Signer() { Account = UInt160.Zero }], + Attributes = [] + }; + var conflict = new TransactionState(); + snapshotCache.Add(key, new StorageItem(conflict)); + Assert.IsTrue(test.Verify(snapshotCache, tx)); + + // Conflicting transaction is on-chain. + snapshotCache.Delete(key); + conflict = new TransactionState + { + BlockIndex = 123, + Transaction = tx, + State = VMState.NONE + }; + snapshotCache.Add(key, new StorageItem(conflict)); + Assert.IsFalse(test.Verify(snapshotCache, tx)); + + // There's no conflicting transaction at all. + snapshotCache.Delete(key); + Assert.IsTrue(test.Verify(snapshotCache, tx)); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_ExtensiblePayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_ExtensiblePayload.cs new file mode 100644 index 0000000000..baa94ddbba --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_ExtensiblePayload.cs @@ -0,0 +1,73 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ExtensiblePayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.VM; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_ExtensiblePayload +{ + [TestMethod] + public void Size_Get() + { + var test = new ExtensiblePayload() + { + Sender = Array.Empty().ToScriptHash(), + Category = "123", + Data = new byte[] { 1, 2, 3 }, + Witness = new() + { + InvocationScript = new byte[] { 3, 5, 6 }, + VerificationScript = ReadOnlyMemory.Empty + } + }; + Assert.AreEqual(42, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = new ExtensiblePayload() + { + Category = "123", + ValidBlockStart = 456, + ValidBlockEnd = 789, + Sender = Array.Empty().ToScriptHash(), + Data = new byte[] { 1, 2, 3 }, + Witness = new() + { + InvocationScript = new byte[] { (byte)OpCode.PUSH1, (byte)OpCode.PUSH2, (byte)OpCode.PUSH3 }, + VerificationScript = ReadOnlyMemory.Empty + } + }; + var clone = test.ToArray().AsSerializable(); + + Assert.AreEqual(test.Sender, clone.Witness.ScriptHash); + Assert.AreEqual(test.Hash, clone.Hash); + Assert.AreEqual(test.ValidBlockStart, clone.ValidBlockStart); + Assert.AreEqual(test.ValidBlockEnd, clone.ValidBlockEnd); + Assert.AreEqual(test.Category, clone.Category); + } + + [TestMethod] + public void Witness() + { + IVerifiable item = (ExtensiblePayload)RuntimeHelpers.GetUninitializedObject(typeof(ExtensiblePayload)); + item.Witnesses = [new()]; + Assert.HasCount(1, item.Witnesses); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_FilterAddPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_FilterAddPayload.cs new file mode 100644 index 0000000000..7d0691a6d2 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_FilterAddPayload.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_FilterAddPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_FilterAddPayload +{ + [TestMethod] + public void Size_Get() + { + var test = new FilterAddPayload() { Data = ReadOnlyMemory.Empty }; + Assert.AreEqual(1, test.Size); + + test = new FilterAddPayload() { Data = new byte[] { 1, 2, 3 } }; + Assert.AreEqual(4, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = new FilterAddPayload() { Data = new byte[] { 1, 2, 3 } }; + var clone = test.ToArray().AsSerializable(); + + Assert.IsTrue(test.Data.Span.SequenceEqual(clone.Data.Span)); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_FilterLoadPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_FilterLoadPayload.cs new file mode 100644 index 0000000000..4a70d950fc --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_FilterLoadPayload.cs @@ -0,0 +1,44 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_FilterLoadPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_FilterLoadPayload +{ + [TestMethod] + public void Size_Get() + { + var test = new FilterLoadPayload() { Filter = Array.Empty(), K = 1, Tweak = uint.MaxValue }; + Assert.AreEqual(6, test.Size); + + test = FilterLoadPayload.Create(new BloomFilter(8, 10, 123456)); + Assert.AreEqual(7, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = FilterLoadPayload.Create(new BloomFilter(8, 10, 123456)); + var clone = test.ToArray().AsSerializable(); + + CollectionAssert.AreEqual(test.Filter.ToArray(), clone.Filter.ToArray()); + Assert.AreEqual(test.K, clone.K); + Assert.AreEqual(test.Tweak, clone.Tweak); + + Assert.ThrowsExactly(() => _ = new FilterLoadPayload() { Filter = Array.Empty(), K = 51, Tweak = uint.MaxValue }.ToArray().AsSerializable()); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_GetBlockByIndexPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_GetBlockByIndexPayload.cs new file mode 100644 index 0000000000..dffbadbeab --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_GetBlockByIndexPayload.cs @@ -0,0 +1,49 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_GetBlockByIndexPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_GetBlockByIndexPayload +{ + [TestMethod] + public void Size_Get() + { + var test = new GetBlockByIndexPayload() { Count = 5, IndexStart = 5 }; + Assert.AreEqual(6, test.Size); + + test = GetBlockByIndexPayload.Create(1, short.MaxValue); + Assert.AreEqual(6, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = new GetBlockByIndexPayload() { Count = -1, IndexStart = int.MaxValue }; + var clone = test.ToArray().AsSerializable(); + + Assert.AreEqual(test.Count, clone.Count); + Assert.AreEqual(test.IndexStart, clone.IndexStart); + + test = new GetBlockByIndexPayload() { Count = -2, IndexStart = int.MaxValue }; + Assert.ThrowsExactly(() => _ = test.ToArray().AsSerializable()); + + test = new GetBlockByIndexPayload() { Count = 0, IndexStart = int.MaxValue }; + Assert.ThrowsExactly(() => _ = test.ToArray().AsSerializable()); + + test = new GetBlockByIndexPayload() { Count = HeadersPayload.MaxHeadersCount + 1, IndexStart = int.MaxValue }; + Assert.ThrowsExactly(() => _ = test.ToArray().AsSerializable()); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_GetBlocksPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_GetBlocksPayload.cs new file mode 100644 index 0000000000..e7dba73b90 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_GetBlocksPayload.cs @@ -0,0 +1,45 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_GetBlocksPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_GetBlocksPayload +{ + [TestMethod] + public void Size_Get() + { + var test = new GetBlocksPayload() { Count = 5, HashStart = UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff01") }; + Assert.AreEqual(34, test.Size); + + test = new GetBlocksPayload() { Count = 1, HashStart = UInt256.Zero }; + Assert.AreEqual(34, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = GetBlocksPayload.Create(UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff01"), 5); + var clone = test.ToArray().AsSerializable(); + + Assert.AreEqual(test.Count, clone.Count); + Assert.AreEqual(test.HashStart, clone.HashStart); + Assert.AreEqual(5, clone.Count); + Assert.AreEqual("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff01", clone.HashStart.ToString()); + + Assert.ThrowsExactly(() => _ = GetBlocksPayload.Create(UInt256.Zero, -2).ToArray().AsSerializable()); + Assert.ThrowsExactly(() => _ = GetBlocksPayload.Create(UInt256.Zero, 0).ToArray().AsSerializable()); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Header.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Header.cs new file mode 100644 index 0000000000..8269874390 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Header.cs @@ -0,0 +1,213 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Header.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_Header +{ + private static readonly string s_headerHex = + "0000000000000000000000000000000000000000000000000000000000000000000000007227ba7b747f1a9" + + "8f68679d4a98b68927646ab195a6f56b542ca5a0e6a412662493ed0e58f0100000000000000000000000000" + + "0000000000000000000000000000000000000000000001000111"; + + [TestMethod] + public void Size_Get() + { + var val256 = UInt256.Zero; + var uut = TestUtils.MakeHeader(null, val256); + // blockbase 4 + 64 + 1 + 32 + 4 + 4 + 20 + 4 + // header 1 + Assert.AreEqual(113, uut.Size); // 105 + nonce + } + + [TestMethod] + public void GetHashCodeTest() + { + var val256 = UInt256.Zero; + var uut = TestUtils.MakeHeader(null, val256); + Assert.AreEqual(uut.Hash.GetHashCode(), uut.GetHashCode()); + } + + [TestMethod] + public void TrimTest() + { + var val256 = UInt256.Zero; + var snapshotCache = TestBlockchain.GetTestSnapshotCache().CloneCache(); + var uut = TestUtils.MakeHeader(null, val256); + uut.Witness = Witness.Empty; + + TestUtils.BlocksAdd(snapshotCache, uut.Hash, new TrimmedBlock() + { + Header = new Header + { + Timestamp = uut.Timestamp, + PrevHash = uut.PrevHash, + MerkleRoot = uut.MerkleRoot, + NextConsensus = uut.NextConsensus, + Witness = uut.Witness + }, + Hashes = [] + }); + + var trim = NativeContract.Ledger.GetTrimmedBlock(snapshotCache, uut.Hash)!; + var header = trim.Header; + + Assert.AreEqual(uut.Version, header.Version); + Assert.AreEqual(uut.PrevHash, header.PrevHash); + Assert.AreEqual(uut.MerkleRoot, header.MerkleRoot); + Assert.AreEqual(uut.Timestamp, header.Timestamp); + Assert.AreEqual(uut.Index, header.Index); + Assert.AreEqual(uut.NextConsensus, header.NextConsensus); + CollectionAssert.AreEqual(uut.Witness.InvocationScript.ToArray(), header.Witness.InvocationScript.ToArray()); + CollectionAssert.AreEqual(uut.Witness.VerificationScript.ToArray(), header.Witness.VerificationScript.ToArray()); + Assert.IsEmpty(trim.Hashes); + } + + [TestMethod] + public void Deserialize() + { + var uut = TestUtils.MakeHeader(null, UInt256.Zero); + MemoryReader reader = new(s_headerHex.HexToBytes()); + uut.Deserialize(ref reader); + } + + [TestMethod] + public void CloneTest() + { + var uut = TestUtils.MakeHeader(null, UInt256.Zero); + var clone = uut.Clone(); + CollectionAssert.AreEqual(uut.ToArray(), clone.ToArray()); + // Check not referenced + uut.Witness.InvocationScript = new byte[123]; + CollectionAssert.AreNotEqual(clone.Witness.InvocationScript.ToArray(), uut.Witness.InvocationScript.ToArray()); + } + + [TestMethod] + public void Equals_SameHeader() + { + var uut = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)); + Assert.IsTrue(uut.Equals(uut)); + } + + [TestMethod] + public void Equals_SameHash() + { + var prevHash = new UInt256(TestUtils.GetByteArray(32, 0x42)); + var uut = TestUtils.MakeHeader(null, prevHash); + var header = TestUtils.MakeHeader(null, prevHash); + + Assert.IsTrue(uut.Equals(header)); + } + + [TestMethod] + public void Equals_SameObject() + { + var uut = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)); + Assert.IsTrue(uut.Equals((object)uut)); + } + + [TestMethod] + public void Serialize() + { + var uut = TestUtils.MakeHeader(null, UInt256.Zero); + Assert.AreEqual(s_headerHex, uut.ToArray().ToHexString()); + } + + [TestMethod] + public void TestWitness() + { + IVerifiable item = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)); + item.Witnesses = [new()]; + Assert.HasCount(1, item.Witnesses); + } + + [TestMethod] + public void TestGetScriptHashesForVerifying_NullSnapshot() + { + var account1 = UInt160.Parse("0x0100000000000000000000000000000000000000"); + var account2 = UInt160.Parse("0x0200000000000000000000000000000000000000"); + + var tx = new Transaction + { + Attributes = [], + Witnesses = [Witness.Empty], + Signers = new[] + { + new Signer { Account = account1 }, + new Signer { Account = account2 } + } + }; + + var hashes = tx.GetScriptHashesForVerifying(null); + CollectionAssert.AreEqual(new[] { account1, account2 }, hashes); + } + + [TestMethod] + public void TestGetScriptHashesForVerifying_NullSnapshotGetSender() + { + var sender = UInt160.Parse("0x0100000000000000000000000000000000000000"); + var payload = new ExtensiblePayload + { + Category = "", + Witness = new() { }, + Sender = sender + }; + var hashes = ((IVerifiable)payload).GetScriptHashesForVerifying(null); + CollectionAssert.AreEqual(new[] { sender }, hashes); + } + + [TestMethod] + public void TestGetScriptHashesForVerifying_NullSnapshotGetWitness() + { + var header = new Header + { + PrevHash = UInt256.Zero, + Witness = new Witness + { + InvocationScript = Array.Empty(), + VerificationScript = new byte[] { 0x01, 0x02, 0x03 } + }, + MerkleRoot = UInt256.Zero, + NextConsensus = null! + }; + + var hashes = ((IVerifiable)header).GetScriptHashesForVerifying(null); + + CollectionAssert.AreEqual(new[] { header.Witness.ScriptHash }, hashes); + } + + [TestMethod] + public void TestGetScriptHashesForVerifying_NullSnapshotThrows() + { + var header = new Header + { + PrevHash = "0x0100000000000000000000000000000000000000000000000000000000000000", + Witness = new Witness + { + InvocationScript = Array.Empty(), + VerificationScript = new byte[] { 0x01 } + }, + MerkleRoot = UInt256.Zero, + NextConsensus = null! + }; + + Assert.ThrowsExactly(() => ((IVerifiable)header).GetScriptHashesForVerifying(null)); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_HeadersPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_HeadersPayload.cs new file mode 100644 index 0000000000..144567db0e --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_HeadersPayload.cs @@ -0,0 +1,41 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_HeadersPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_HeadersPayload +{ + [TestMethod] + public void Size_Get() + { + var header = TestUtils.MakeHeader(null, UInt256.Zero); + var test = HeadersPayload.Create(); + Assert.AreEqual(1, test.Size); + test = HeadersPayload.Create(header); + Assert.AreEqual(1 + header.Size, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var header = TestUtils.MakeHeader(null, UInt256.Zero); + var test = HeadersPayload.Create(header); + var clone = test.ToArray().AsSerializable(); + + Assert.HasCount(test.Headers.Length, clone.Headers); + Assert.AreEqual(test.Headers[0], clone.Headers[0]); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_HighPriorityAttribute.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_HighPriorityAttribute.cs new file mode 100644 index 0000000000..8fcdb85fc6 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_HighPriorityAttribute.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_HighPriorityAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract.Native; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_HighPriorityAttribute +{ + [TestMethod] + public void Size_Get() + { + var test = new HighPriorityAttribute(); + Assert.AreEqual(1, test.Size); + } + + [TestMethod] + public void ToJson() + { + var test = new HighPriorityAttribute(); + var json = test.ToJson().ToString(); + Assert.AreEqual(@"{""type"":""HighPriority""}", json); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = new HighPriorityAttribute(); + + var clone = test.ToArray().AsSerializable(); + Assert.AreEqual(clone.Type, test.Type); + + // As transactionAttribute + + byte[] buffer = test.ToArray(); + var reader = new MemoryReader(buffer); + clone = (HighPriorityAttribute)TransactionAttribute.DeserializeFrom(ref reader); + Assert.AreEqual(clone.Type, test.Type); + + // Wrong type + + buffer[0] = 0xff; + reader = new MemoryReader(buffer); + try + { + TransactionAttribute.DeserializeFrom(ref reader); + Assert.Fail(); + } + catch (FormatException) { } + reader = new MemoryReader(buffer); + try + { + new HighPriorityAttribute().Deserialize(ref reader); + Assert.Fail(); + } + catch (FormatException) { } + } + + [TestMethod] + public void Verify() + { + var test = new HighPriorityAttribute(); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + Assert.IsFalse(test.Verify(snapshotCache, new Transaction() { Signers = Array.Empty(), Attributes = [test], Witnesses = null! })); + Assert.IsFalse(test.Verify(snapshotCache, new Transaction() { Signers = new Signer[] { new() { Account = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01") } }, Attributes = [test], Witnesses = null! })); + Assert.IsTrue(test.Verify(snapshotCache, new Transaction() { Signers = new Signer[] { new() { Account = NativeContract.NEO.GetCommitteeAddress(snapshotCache) } }, Attributes = [test], Witnesses = null! })); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_InvPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_InvPayload.cs new file mode 100644 index 0000000000..b35afbefa3 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_InvPayload.cs @@ -0,0 +1,63 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_InvPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_InvPayload +{ + [TestMethod] + public void Size_Get() + { + var test = InvPayload.Create(InventoryType.TX, UInt256.Zero); + Assert.AreEqual(34, test.Size); + + test = InvPayload.Create(InventoryType.TX, UInt256.Zero, UInt256.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00a4")); + Assert.AreEqual(66, test.Size); + } + + [TestMethod] + public void CreateGroup() + { + var hashes = new UInt256[InvPayload.MaxHashesCount + 1]; + + for (int x = 0; x < hashes.Length; x++) + { + byte[] data = new byte[32]; + Array.Copy(BitConverter.GetBytes(x), data, 4); + hashes[x] = new UInt256(data); + } + + var array = InvPayload.CreateGroup(InventoryType.TX, hashes).ToArray(); + + Assert.HasCount(2, array); + Assert.AreEqual(InventoryType.TX, array[0].Type); + Assert.AreEqual(InventoryType.TX, array[1].Type); + CollectionAssert.AreEqual(hashes.Take(InvPayload.MaxHashesCount).ToArray(), array[0].Hashes); + CollectionAssert.AreEqual(hashes.Skip(InvPayload.MaxHashesCount).ToArray(), array[1].Hashes); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = InvPayload.Create(InventoryType.TX, UInt256.Zero, UInt256.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00a4")); + var clone = test.ToArray().AsSerializable(); + + Assert.AreEqual(test.Type, clone.Type); + CollectionAssert.AreEqual(test.Hashes, clone.Hashes); + + Assert.ThrowsExactly(() => _ = InvPayload.Create((InventoryType)0xff, UInt256.Zero).ToArray().AsSerializable()); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_MerkleBlockPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_MerkleBlockPayload.cs new file mode 100644 index 0000000000..814a16314e --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_MerkleBlockPayload.cs @@ -0,0 +1,52 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MerkleBlockPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using System.Collections; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_MerkleBlockPayload +{ + private NeoSystem _system = null!; + + [TestInitialize] + public void TestSetup() + { + _system = TestBlockchain.GetSystem(); + } + + [TestMethod] + public void Size_Get() + { + var test = MerkleBlockPayload.Create(_system.GenesisBlock, new BitArray(1024, false)); + Assert.AreEqual(247, test.Size); // 239 + nonce + + test = MerkleBlockPayload.Create(_system.GenesisBlock, new BitArray(0, false)); + Assert.AreEqual(119, test.Size); // 111 + nonce + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = MerkleBlockPayload.Create(_system.GenesisBlock, new BitArray(2, false)); + var clone = test.ToArray().AsSerializable(); + + Assert.AreEqual(test.TxCount, clone.TxCount); + Assert.HasCount(test.Hashes.Length, clone.Hashes); + Assert.AreEqual(test.Flags.Length, clone.Flags.Length); + CollectionAssert.AreEqual(test.Hashes, clone.Hashes); + Assert.IsTrue(test.Flags.Span.SequenceEqual(clone.Flags.Span)); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NetworkAddressWithTime.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NetworkAddressWithTime.cs new file mode 100644 index 0000000000..0d9fe2cf4a --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NetworkAddressWithTime.cs @@ -0,0 +1,59 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NetworkAddressWithTime.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.Network.P2P.Capabilities; +using Neo.Network.P2P.Payloads; +using System.Net; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_NetworkAddressWithTime +{ + [TestMethod] + public void SizeAndEndPoint_Get() + { + var test = new NetworkAddressWithTime() { Capabilities = [], Address = IPAddress.Any, Timestamp = 1 }; + Assert.AreEqual(21, test.Size); + Assert.AreEqual(0, test.EndPoint.Port); + + test = NetworkAddressWithTime.Create(IPAddress.Any, 1, [new ServerCapability(NodeCapabilityType.TcpServer, 22)]); + Assert.AreEqual(24, test.Size); + Assert.AreEqual(22, test.EndPoint.Port); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = NetworkAddressWithTime.Create(IPAddress.Any, 1, + [ + new ServerCapability(NodeCapabilityType.TcpServer, 22), + new UnknownCapability(NodeCapabilityType.Extension0), + new UnknownCapability(NodeCapabilityType.Extension0) + ]); + var clone = test.ToArray().AsSerializable(); + + Assert.AreEqual(test.Address, clone.Address); + Assert.AreEqual(test.EndPoint.ToString(), clone.EndPoint.ToString()); + Assert.AreEqual(test.Timestamp, clone.Timestamp); + CollectionAssert.AreEqual(test.Capabilities.ToByteArray(), clone.Capabilities.ToByteArray()); + + test = NetworkAddressWithTime.Create(IPAddress.Any, 1, + [ + new ServerCapability(NodeCapabilityType.TcpServer, 22), + new ServerCapability(NodeCapabilityType.TcpServer, 22) + ]); + Assert.ThrowsExactly(() => _ = test.ToArray().AsSerializable()); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotValidBefore.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotValidBefore.cs new file mode 100644 index 0000000000..86002f0f58 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotValidBefore.cs @@ -0,0 +1,86 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NotValidBefore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract.Native; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_NotValidBefore +{ + [TestMethod] + public void Size_Get() + { + var test = new NotValidBefore(); + Assert.AreEqual(5, test.Size); + } + + [TestMethod] + public void ToJson() + { + var test = new NotValidBefore + { + Height = 42 + }; + var json = test.ToJson().ToString(); + Assert.AreEqual(@"{""type"":""NotValidBefore"",""height"":42}", json); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = new NotValidBefore(); + + var clone = test.ToArray().AsSerializable(); + Assert.AreEqual(clone.Type, test.Type); + + // As transactionAttribute + + byte[] buffer = test.ToArray(); + var reader = new MemoryReader(buffer); + clone = (NotValidBefore)TransactionAttribute.DeserializeFrom(ref reader); + Assert.AreEqual(clone.Type, test.Type); + + // Wrong type + + buffer[0] = 0xff; + reader = new MemoryReader(buffer); + try + { + TransactionAttribute.DeserializeFrom(ref reader); + Assert.Fail(); + } + catch (FormatException) { } + reader = new MemoryReader(buffer); + try + { + new NotValidBefore().Deserialize(ref reader); + Assert.Fail(); + } + catch (FormatException) { } + } + + [TestMethod] + public void Verify() + { + var test = new NotValidBefore(); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + test.Height = NativeContract.Ledger.CurrentIndex(snapshotCache) + 1; + + Assert.IsFalse(test.Verify(snapshotCache, new Transaction { Signers = null!, Attributes = [test], Witnesses = null! })); + test.Height--; + Assert.IsTrue(test.Verify(snapshotCache, new Transaction { Signers = null!, Attributes = [test], Witnesses = null! })); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs new file mode 100644 index 0000000000..0d0f380b03 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs @@ -0,0 +1,89 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NotaryAssisted.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_NotaryAssisted +{ + // Use the hard-coded Notary hash value from NeoGo to ensure hashes are compatible. + private static readonly UInt160 s_notaryHash = UInt160.Parse("0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b"); + + [TestMethod] + public void Size_Get() + { + var attr = new NotaryAssisted() { NKeys = 4 }; + Assert.AreEqual(1 + 1, attr.Size); + } + + [TestMethod] + public void ToJson() + { + var attr = new NotaryAssisted() { NKeys = 4 }; + var json = attr.ToJson().ToString(); + Assert.AreEqual(@"{""type"":""NotaryAssisted"",""nkeys"":4}", json); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var attr = new NotaryAssisted() { NKeys = 4 }; + var clone = attr.ToArray().AsSerializable(); + Assert.AreEqual(clone.Type, attr.Type); + + // As transactionAttribute + var buffer = attr.ToArray(); + var reader = new MemoryReader(buffer); + clone = (NotaryAssisted)TransactionAttribute.DeserializeFrom(ref reader); + Assert.AreEqual(clone.Type, attr.Type); + + // Wrong type + buffer[0] = 0xff; + Assert.ThrowsExactly(() => MemoryReaderDeserializeFrom(buffer)); + + static void MemoryReaderDeserializeFrom(byte[] buffer) + { + var reader = new MemoryReader(buffer); + TransactionAttribute.DeserializeFrom(ref reader); + } + } + + [TestMethod] + public void Verify() + { + var attr = new NotaryAssisted() { NKeys = 4 }; + + // Temporary use Notary contract hash stub for valid transaction. + var txGood = new Transaction { Signers = [new() { Account = s_notaryHash }, new() { Account = UInt160.Zero }], Attributes = [attr], Witnesses = null! }; + var txBad1 = new Transaction { Signers = [new() { Account = s_notaryHash }], Attributes = [attr], Witnesses = null! }; + var txBad2 = new Transaction { Signers = [new() { Account = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01") }], Attributes = [attr], Witnesses = null! }; + var snapshot = TestBlockchain.GetTestSnapshotCache(); + + Assert.IsTrue(attr.Verify(snapshot, txGood)); + Assert.IsFalse(attr.Verify(snapshot, txBad1)); + Assert.IsFalse(attr.Verify(snapshot, txBad2)); + } + + [TestMethod] + public void CalculateNetworkFee() + { + var snapshot = TestBlockchain.GetTestSnapshotCache(); + var attr = new NotaryAssisted() { NKeys = 4 }; + var tx = new Transaction { Signers = [new() { Account = s_notaryHash }], Attributes = [attr], Witnesses = null! }; + + Assert.AreEqual((4 + 1) * 1000_0000, attr.CalculateNetworkFee(snapshot, tx)); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Signers.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Signers.cs new file mode 100644 index 0000000000..21747d498c --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Signers.cs @@ -0,0 +1,326 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Signers.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.Network.P2P.Payloads.Conditions; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_Signers +{ + [TestMethod] + public void Test_IEquatable() + { + var ecPoint = ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1); + var expected = new Signer() + { + Account = UInt160.Zero, + Scopes = WitnessScope.Global, + AllowedContracts = [UInt160.Zero], + AllowedGroups = [ecPoint], + Rules = [ + new WitnessRule + { + Condition = new BooleanCondition + { + Expression = true, + }, + Action = WitnessRuleAction.Allow, + }, + ] + }; + + var actual = new Signer() + { + Account = UInt160.Zero, + Scopes = WitnessScope.Global, + AllowedContracts = [UInt160.Zero], + AllowedGroups = [ecPoint], + Rules = [ + new WitnessRule + { + Condition = new BooleanCondition + { + Expression = true, + }, + Action = WitnessRuleAction.Allow, + }, + ] + }; + + var notEqual = new Signer() + { + Account = UInt160.Zero, + Scopes = WitnessScope.WitnessRules, + AllowedContracts = [], + AllowedGroups = [], + Rules = [] + }; + + var cnull = new Signer + { + Account = null!, + Scopes = WitnessScope.Global, + AllowedContracts = null, + AllowedGroups = null, + Rules = null, + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + + //Check null + Assert.AreNotEqual(cnull, notEqual); + Assert.IsFalse(cnull.Equals(notEqual)); + } + + + [TestMethod] + public void Serialize_Deserialize_Global() + { + var attr = new Signer() + { + Scopes = WitnessScope.Global, + Account = UInt160.Zero + }; + + var hex = "000000000000000000000000000000000000000080"; + CollectionAssert.AreEqual(attr.ToArray(), hex.HexToBytes()); + + var copy = hex.HexToBytes().AsSerializable(); + + Assert.AreEqual(attr.Scopes, copy.Scopes); + Assert.AreEqual(attr.Account, copy.Account); + } + + [TestMethod] + public void Serialize_Deserialize_CalledByEntry() + { + var attr = new Signer() + { + Scopes = WitnessScope.CalledByEntry, + Account = UInt160.Zero + }; + + var hex = "000000000000000000000000000000000000000001"; + CollectionAssert.AreEqual(attr.ToArray(), hex.HexToBytes()); + + var copy = hex.HexToBytes().AsSerializable(); + + Assert.AreEqual(attr.Scopes, copy.Scopes); + Assert.AreEqual(attr.Account, copy.Account); + } + + [TestMethod] + public void Serialize_Deserialize_MaxNested_And() + { + var attr = new Signer() + { + Scopes = WitnessScope.WitnessRules, + Account = UInt160.Zero, + Rules = new[]{ new WitnessRule() + { + Action = WitnessRuleAction.Allow, + Condition = new AndCondition() + { + Expressions = new WitnessCondition[] + { + new AndCondition() + { + Expressions = new WitnessCondition[] + { + new AndCondition() + { + Expressions = new WitnessCondition[] + { + new BooleanCondition() { Expression=true } + } + } + } + } + } + } + }} + }; + + var hex = "00000000000000000000000000000000000000004001010201020102010001"; + CollectionAssert.AreEqual(attr.ToArray(), hex.HexToBytes()); + + Assert.ThrowsExactly(() => _ = hex.HexToBytes().AsSerializable()); + } + + [TestMethod] + public void Serialize_Deserialize_MaxNested_Or() + { + var attr = new Signer() + { + Scopes = WitnessScope.WitnessRules, + Account = UInt160.Zero, + Rules = new[]{ new WitnessRule() + { + Action = WitnessRuleAction.Allow, + Condition = new OrCondition() + { + Expressions = new WitnessCondition[] + { + new OrCondition() + { + Expressions = new WitnessCondition[] + { + new OrCondition() + { + Expressions = new WitnessCondition[] + { + new BooleanCondition() { Expression=true } + } + } + } + } + } + } + }} + }; + + var hex = "00000000000000000000000000000000000000004001010301030103010001"; + CollectionAssert.AreEqual(attr.ToArray(), hex.HexToBytes()); + + Assert.ThrowsExactly(() => _ = hex.HexToBytes().AsSerializable()); + } + + [TestMethod] + public void Serialize_Deserialize_CustomContracts() + { + var attr = new Signer() + { + Scopes = WitnessScope.CustomContracts, + AllowedContracts = new[] { UInt160.Zero }, + Account = UInt160.Zero + }; + + var hex = "000000000000000000000000000000000000000010010000000000000000000000000000000000000000"; + CollectionAssert.AreEqual(attr.ToArray(), hex.HexToBytes()); + + var copy = hex.HexToBytes().AsSerializable(); + + Assert.AreEqual(attr.Scopes, copy.Scopes); + CollectionAssert.AreEqual(attr.AllowedContracts, copy.AllowedContracts); + Assert.AreEqual(attr.Account, copy.Account); + } + + [TestMethod] + public void Serialize_Deserialize_CustomGroups() + { + var attr = new Signer() + { + Scopes = WitnessScope.CustomGroups, + AllowedGroups = new[] { ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1) }, + Account = UInt160.Zero + }; + + var hex = "0000000000000000000000000000000000000000200103b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c"; + CollectionAssert.AreEqual(attr.ToArray(), hex.HexToBytes()); + + var copy = hex.HexToBytes().AsSerializable(); + + Assert.AreEqual(attr.Scopes, copy.Scopes); + CollectionAssert.AreEqual(attr.AllowedGroups, copy.AllowedGroups); + Assert.AreEqual(attr.Account, copy.Account); + } + + [TestMethod] + public void Json_Global() + { + var attr = new Signer() + { + Scopes = WitnessScope.Global, + Account = UInt160.Zero + }; + + var json = "{\"account\":\"0x0000000000000000000000000000000000000000\",\"scopes\":\"Global\"}"; + Assert.AreEqual(json, attr.ToJson().ToString()); + } + + [TestMethod] + public void Json_CalledByEntry() + { + var attr = new Signer() + { + Scopes = WitnessScope.CalledByEntry, + Account = UInt160.Zero + }; + + var json = "{\"account\":\"0x0000000000000000000000000000000000000000\",\"scopes\":\"CalledByEntry\"}"; + Assert.AreEqual(json, attr.ToJson().ToString()); + } + + [TestMethod] + public void Json_CustomContracts() + { + var attr = new Signer() + { + Scopes = WitnessScope.CustomContracts, + AllowedContracts = new[] { UInt160.Zero }, + Account = UInt160.Zero + }; + + var json = "{\"account\":\"0x0000000000000000000000000000000000000000\",\"scopes\":\"CustomContracts\",\"allowedcontracts\":[\"0x0000000000000000000000000000000000000000\"]}"; + Assert.AreEqual(json, attr.ToJson().ToString()); + } + + [TestMethod] + public void Json_CustomGroups() + { + var attr = new Signer() + { + Scopes = WitnessScope.CustomGroups, + AllowedGroups = new[] { ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1) }, + Account = UInt160.Zero + }; + + var json = "{\"account\":\"0x0000000000000000000000000000000000000000\",\"scopes\":\"CustomGroups\",\"allowedgroups\":[\"03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c\"]}"; + Assert.AreEqual(json, attr.ToJson().ToString()); + } + + [TestMethod] + public void Json_From() + { + Signer signer = new() + { + Account = UInt160.Zero, + Scopes = WitnessScope.CustomContracts | WitnessScope.CustomGroups | WitnessScope.WitnessRules, + AllowedContracts = [UInt160.Zero], + AllowedGroups = [ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1)], + Rules = [new() { Action = WitnessRuleAction.Allow, Condition = new BooleanCondition() { Expression = true } }] + }; + var json = signer.ToJson(); + var newSigner = Signer.FromJson(json); + Assert.IsTrue(newSigner.Account.Equals(signer.Account)); + Assert.AreEqual(signer.Scopes, newSigner.Scopes); + Assert.HasCount(1, newSigner.AllowedContracts!); + Assert.IsTrue(newSigner.AllowedContracts![0].Equals(signer.AllowedContracts[0])); + Assert.HasCount(1, newSigner.AllowedGroups!); + Assert.IsTrue(newSigner.AllowedGroups![0].Equals(signer.AllowedGroups[0])); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs new file mode 100644 index 0000000000..2ac974338b --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs @@ -0,0 +1,1300 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Transaction.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.Extensions.VM; +using Neo.IO; +using Neo.Json; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using Neo.Wallets; +using System.Numerics; +using System.Runtime.CompilerServices; +using Array = System.Array; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_Transaction +{ + Transaction _uut = null!; + + [TestInitialize] + public void TestSetup() + { + _uut = (Transaction)RuntimeHelpers.GetUninitializedObject(typeof(Transaction)); + } + + [TestMethod] + public void Script_Get() + { + Assert.IsTrue(_uut.Script.IsEmpty); + } + + [TestMethod] + public void FromStackItem() + { + Assert.ThrowsExactly(() => ((IInteroperable)_uut).FromStackItem(StackItem.Null)); + } + + [TestMethod] + public void TestEquals() + { + Assert.IsTrue(_uut.Equals(_uut)); + Assert.IsFalse(_uut.Equals(null)); + } + + [TestMethod] + public void InventoryType_Get() + { + Assert.AreEqual(InventoryType.TX, ((IInventory)_uut).InventoryType); + } + + [TestMethod] + public void Script_Set() + { + byte[] val = TestUtils.GetByteArray(32, 0x42); + _uut.Script = val; + var span = _uut.Script.Span; + Assert.AreEqual(32, span.Length); + for (int i = 0; i < val.Length; i++) + { + Assert.AreEqual(val[i], span[i]); + } + } + + [TestMethod] + public void Gas_Get() + { + Assert.AreEqual(0, _uut.SystemFee); + } + + [TestMethod] + public void Gas_Set() + { + long val = 4200000000; + _uut.SystemFee = val; + Assert.AreEqual(val, _uut.SystemFee); + } + + [TestMethod] + public void Size_Get() + { + _uut.Script = TestUtils.GetByteArray(32, 0x42); + _uut.Signers = []; + _uut.Attributes = []; + _uut.Witnesses = [Witness.Empty]; + + Assert.AreEqual(0, _uut.Version); + Assert.AreEqual(32, _uut.Script.Length); + Assert.AreEqual(33, _uut.Script.GetVarSize()); + Assert.AreEqual(63, _uut.Size); + } + + [TestMethod] + public void CheckNoItems() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + byte[] script = [(byte)OpCode.PUSH0, (byte)OpCode.DROP]; + var tx = new Transaction + { + NetworkFee = 1000000, + SystemFee = 1000000, + Script = ReadOnlyMemory.Empty, + Signers = [new() { Account = script.ToScriptHash() }], + Attributes = [], + Witnesses = + [ + new() + { + InvocationScript = ReadOnlyMemory.Empty, + VerificationScript = script + } + ] + }; + Assert.IsFalse(tx.VerifyWitnesses(TestProtocolSettings.Default, snapshotCache, tx.NetworkFee)); + } + + [TestMethod] + public void FeeIsMultiSigContract() + { + var walletA = TestUtils.GenerateTestWallet("123"); + var walletB = TestUtils.GenerateTestWallet("123"); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + var a = walletA.CreateAccount(); + var b = walletB.CreateAccount(); + + var multiSignContract = Contract.CreateMultiSigContract(2, + [ + a.GetKey()!.PublicKey, + b.GetKey()!.PublicKey + ]); + + walletA.CreateAccount(multiSignContract, a.GetKey()); + var acc = walletB.CreateAccount(multiSignContract, b.GetKey()); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshotCache.Commit(); + + // Make transaction + + var tx = walletA.MakeTransaction(snapshotCache, [ + new TransferOutput + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = acc.ScriptHash, + Value = new BigDecimal(BigInteger.One, 8) + } + ], acc.ScriptHash); + + Assert.IsNotNull(tx); + + // Sign + + var wrongData = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network + 1); + Assert.IsFalse(walletA.Sign(wrongData)); + + var data = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsTrue(walletA.Sign(data)); + Assert.IsTrue(walletB.Sign(data)); + Assert.IsTrue(data.Completed); + + tx.Witnesses = data.GetWitnesses(); + + // Fast check + + Assert.IsTrue(tx.VerifyWitnesses(TestProtocolSettings.Default, snapshotCache, tx.NetworkFee)); + + // Check + + long verificationGas = 0; + foreach (var witness in tx.Witnesses) + { + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshotCache, + settings: TestProtocolSettings.Default, gas: tx.NetworkFee); + engine.LoadScript(witness.VerificationScript); + engine.LoadScript(witness.InvocationScript); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsTrue(engine.ResultStack.Pop().GetBoolean()); + verificationGas += engine.FeeConsumed; + } + + var sizeGas = tx.Size * NativeContract.Policy.GetFeePerByte(snapshotCache); + Assert.AreEqual(1967100, verificationGas); + Assert.AreEqual(348000, sizeGas); + Assert.AreEqual(2315100, tx.NetworkFee); + } + + [TestMethod] + public void FeeIsSignatureContractDetailed() + { + var wallet = TestUtils.GenerateTestWallet("123"); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var acc = wallet.CreateAccount(); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshotCache.Commit(); + + // Make transaction + + // self-transfer of 1e-8 GAS + var tx = wallet.MakeTransaction(snapshotCache, [ + new TransferOutput + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = acc.ScriptHash, + Value = new BigDecimal(BigInteger.One, 8) + } + ], acc.ScriptHash); + + Assert.IsNotNull(tx); + + // check pre-computed network fee (already guessing signature sizes) + Assert.AreEqual(1228520L, tx.NetworkFee); + + // ---- + // Sign + // ---- + + var data = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + // 'from' is always required as witness + // if not included on cosigner with a scope, its scope should be considered 'CalledByEntry' + Assert.HasCount(1, data.ScriptHashes); + Assert.AreEqual(acc.ScriptHash, data.ScriptHashes[0]); + // will sign tx + bool signed = wallet.Sign(data); + Assert.IsTrue(signed); + // get witnesses from signed 'data' + tx.Witnesses = data.GetWitnesses(); + Assert.HasCount(1, tx.Witnesses); + + // Fast check + + Assert.IsTrue(tx.VerifyWitnesses(TestProtocolSettings.Default, snapshotCache, tx.NetworkFee)); + + // Check + + long verificationGas = 0; + foreach (var witness in tx.Witnesses) + { + using var engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshotCache, + settings: TestProtocolSettings.Default, gas: tx.NetworkFee); + engine.LoadScript(witness.VerificationScript); + engine.LoadScript(witness.InvocationScript); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsTrue(engine.ResultStack.Pop().GetBoolean()); + verificationGas += engine.FeeConsumed; + } + + // ------------------ + // check tx_size cost + // ------------------ + Assert.AreEqual(245, tx.Size); + + // will verify tx size, step by step + + // Part I +#pragma warning disable MSTEST0032 + Assert.AreEqual(25, Transaction.HeaderSize); +#pragma warning restore MSTEST0032 + // Part II + Assert.AreEqual(1, tx.Attributes.GetVarSize()); + Assert.IsEmpty(tx.Attributes); + Assert.HasCount(1, tx.Signers); + // Note that Data size and Usage size are different (because of first byte on GetVarSize()) + Assert.AreEqual(22, tx.Signers.GetVarSize()); + // Part III + Assert.AreEqual(88, tx.Script.GetVarSize()); + // Part IV + Assert.AreEqual(109, tx.Witnesses.GetVarSize()); + // I + II + III + IV + Assert.AreEqual(25 + 22 + 1 + 88 + 109, tx.Size); + + Assert.AreEqual(1000, NativeContract.Policy.GetFeePerByte(snapshotCache)); + var sizeGas = tx.Size * NativeContract.Policy.GetFeePerByte(snapshotCache); + + // final check: verification_cost and tx_size + Assert.AreEqual(245000, sizeGas); + Assert.AreEqual(983520, verificationGas); + + // final assert + Assert.AreEqual(tx.NetworkFee, verificationGas + sizeGas); + } + + [TestMethod] + public void FeeIsSignatureContract_TestScope_Global() + { + var wallet = TestUtils.GenerateTestWallet(""); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var acc = wallet.CreateAccount(); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshotCache.Commit(); + + // Make transaction + // Manually creating script + + byte[] script; + using (ScriptBuilder sb = new()) + { + // self-transfer of 1e-8 GAS + var value = new BigDecimal(BigInteger.One, 8).Value; + sb.EmitDynamicCall(NativeContract.GAS.Hash, "transfer", acc.ScriptHash, acc.ScriptHash, value, null); + sb.Emit(OpCode.ASSERT); + script = sb.ToArray(); + } + + // trying global scope + var signers = new[]{ new Signer + { + Account = acc.ScriptHash, + Scopes = WitnessScope.Global + } }; + + // using this... + + var tx = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers); + + Assert.IsNotNull(tx); + + // ---- + // Sign + // ---- + + var data = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + bool signed = wallet.Sign(data); + Assert.IsTrue(signed); + + // get witnesses from signed 'data' + tx.Witnesses = data.GetWitnesses(); + Assert.HasCount(1, tx.Witnesses); + + // Fast check + Assert.IsTrue(tx.VerifyWitnesses(TestProtocolSettings.Default, snapshotCache, tx.NetworkFee)); + + // Check + long verificationGas = 0; + foreach (var witness in tx.Witnesses) + { + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshotCache, + settings: TestProtocolSettings.Default, gas: tx.NetworkFee); + engine.LoadScript(witness.VerificationScript); + engine.LoadScript(witness.InvocationScript); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsTrue(engine.ResultStack.Pop().GetBoolean()); + verificationGas += engine.FeeConsumed; + } + // get sizeGas + var sizeGas = tx.Size * NativeContract.Policy.GetFeePerByte(snapshotCache); + // final check on sum: verification_cost + tx_size + Assert.AreEqual(1228520, verificationGas + sizeGas); + // final assert + Assert.AreEqual(tx.NetworkFee, verificationGas + sizeGas); + } + + [TestMethod] + public void FeeIsSignatureContract_TestScope_CurrentHash_GAS() + { + var wallet = TestUtils.GenerateTestWallet(""); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var acc = wallet.CreateAccount(); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshotCache.Commit(); + + // Make transaction + // Manually creating script + + byte[] script; + using (ScriptBuilder sb = new()) + { + // self-transfer of 1e-8 GAS + BigInteger value = new BigDecimal(BigInteger.One, 8).Value; + sb.EmitDynamicCall(NativeContract.GAS.Hash, "transfer", acc.ScriptHash, acc.ScriptHash, value, null); + sb.Emit(OpCode.ASSERT); + script = sb.ToArray(); + } + + // trying global scope + var signers = new[]{ new Signer + { + Account = acc.ScriptHash, + Scopes = WitnessScope.CustomContracts, + AllowedContracts = [NativeContract.GAS.Hash] + } }; + + // using this... + + var tx = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers); + + Assert.IsNotNull(tx); + + // ---- + // Sign + // ---- + + var data = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + bool signed = wallet.Sign(data); + Assert.IsTrue(signed); + + // get witnesses from signed 'data' + tx.Witnesses = data.GetWitnesses(); + Assert.HasCount(1, tx.Witnesses); + + // Fast check + Assert.IsTrue(tx.VerifyWitnesses(TestProtocolSettings.Default, snapshotCache, tx.NetworkFee)); + + // Check + long verificationGas = 0; + foreach (var witness in tx.Witnesses) + { + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshotCache, + settings: TestProtocolSettings.Default, gas: tx.NetworkFee); + engine.LoadScript(witness.VerificationScript); + engine.LoadScript(witness.InvocationScript); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsTrue(engine.ResultStack.Pop().GetBoolean()); + verificationGas += engine.FeeConsumed; + } + // get sizeGas + var sizeGas = tx.Size * NativeContract.Policy.GetFeePerByte(snapshotCache); + // final check on sum: verification_cost + tx_size + Assert.AreEqual(1249520, verificationGas + sizeGas); + // final assert + Assert.AreEqual(tx.NetworkFee, verificationGas + sizeGas); + } + + [TestMethod] + public void FeeIsSignatureContract_TestScope_CalledByEntry_Plus_GAS() + { + var wallet = TestUtils.GenerateTestWallet(""); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var acc = wallet.CreateAccount(); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshotCache.Commit(); + + // Make transaction + // Manually creating script + + byte[] script; + using (ScriptBuilder sb = new()) + { + // self-transfer of 1e-8 GAS + var value = new BigDecimal(BigInteger.One, 8).Value; + sb.EmitDynamicCall(NativeContract.GAS.Hash, "transfer", acc.ScriptHash, acc.ScriptHash, value, null); + sb.Emit(OpCode.ASSERT); + script = sb.ToArray(); + } + + // trying CalledByEntry together with GAS + var signers = new[]{ new Signer + { + Account = acc.ScriptHash, + // This combination is supposed to actually be an OR, + // where it's valid in both Entry and also for Custom hash provided (in any execution level) + // it would be better to test this in the future including situations + // where a deeper call level uses this custom witness successfully + Scopes = WitnessScope.CustomContracts | WitnessScope.CalledByEntry, + AllowedContracts = [NativeContract.GAS.Hash] + } }; + + // using this... + + var tx = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers); + + Assert.IsNotNull(tx); + + // ---- + // Sign + // ---- + + var data = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + bool signed = wallet.Sign(data); + Assert.IsTrue(signed); + + // get witnesses from signed 'data' + tx.Witnesses = data.GetWitnesses(); + Assert.HasCount(1, tx.Witnesses); + + // Fast check + Assert.IsTrue(tx.VerifyWitnesses(TestProtocolSettings.Default, snapshotCache, tx.NetworkFee)); + + // Check + long verificationGas = 0; + foreach (var witness in tx.Witnesses) + { + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshotCache, + settings: TestProtocolSettings.Default, gas: tx.NetworkFee); + engine.LoadScript(witness.VerificationScript); + engine.LoadScript(witness.InvocationScript); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsTrue(engine.ResultStack.Pop().GetBoolean()); + verificationGas += engine.FeeConsumed; + } + // get sizeGas + var sizeGas = tx.Size * NativeContract.Policy.GetFeePerByte(snapshotCache); + // final check on sum: verification_cost + tx_size + Assert.AreEqual(1249520, verificationGas + sizeGas); + // final assert + Assert.AreEqual(tx.NetworkFee, verificationGas + sizeGas); + } + + [TestMethod] + public void FeeIsSignatureContract_TestScope_CurrentHash_NEO_FAULT() + { + var wallet = TestUtils.GenerateTestWallet(""); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var acc = wallet.CreateAccount(); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + // Make transaction + // Manually creating script + + byte[] script; + using (ScriptBuilder sb = new()) + { + // self-transfer of 1e-8 GAS + BigInteger value = new BigDecimal(BigInteger.One, 8).Value; + sb.EmitDynamicCall(NativeContract.GAS.Hash, "transfer", acc.ScriptHash, acc.ScriptHash, value); + sb.Emit(OpCode.ASSERT); + script = sb.ToArray(); + } + + // trying global scope + var signers = new[]{ new Signer + { + Account = acc.ScriptHash, + Scopes = WitnessScope.CustomContracts, + AllowedContracts = [NativeContract.NEO.Hash] + } }; + + // using this... + + // expects FAULT on execution of 'transfer' Application script + // due to lack of a valid witness validation + Assert.ThrowsExactly( + () => wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers)); + } + + [TestMethod] + public void FeeIsSignatureContract_TestScope_CurrentHash_NEO_GAS() + { + var wallet = TestUtils.GenerateTestWallet(""); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var acc = wallet.CreateAccount(); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshotCache.Commit(); + + // Make transaction + // Manually creating script + + byte[] script; + using (ScriptBuilder sb = new()) + { + // self-transfer of 1e-8 GAS + BigInteger value = new BigDecimal(BigInteger.One, 8).Value; + sb.EmitDynamicCall(NativeContract.GAS.Hash, "transfer", acc.ScriptHash, acc.ScriptHash, value, null); + sb.Emit(OpCode.ASSERT); + script = sb.ToArray(); + } + + // trying two custom hashes, for same target account + var signers = new[]{ new Signer + { + Account = acc.ScriptHash, + Scopes = WitnessScope.CustomContracts, + AllowedContracts = [NativeContract.NEO.Hash, NativeContract.GAS.Hash] + } }; + + // using this... + + var tx = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers); + + Assert.IsNotNull(tx); + + // ---- + // Sign + // ---- + + var data = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + bool signed = wallet.Sign(data); + Assert.IsTrue(signed); + + // get witnesses from signed 'data' + tx.Witnesses = data.GetWitnesses(); + // only a single witness should exist + Assert.HasCount(1, tx.Witnesses); + // no attributes must exist + Assert.IsEmpty(tx.Attributes); + // one cosigner must exist + Assert.HasCount(1, tx.Signers); + + // Fast check + Assert.IsTrue(tx.VerifyWitnesses(TestProtocolSettings.Default, snapshotCache, tx.NetworkFee)); + + // Check + long verificationGas = 0; + foreach (var witness in tx.Witnesses) + { + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshotCache, + settings: TestProtocolSettings.Default, gas: tx.NetworkFee); + engine.LoadScript(witness.VerificationScript); + engine.LoadScript(witness.InvocationScript); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsTrue(engine.ResultStack.Pop().GetBoolean()); + verificationGas += engine.FeeConsumed; + } + // get sizeGas + var sizeGas = tx.Size * NativeContract.Policy.GetFeePerByte(snapshotCache); + // final check on sum: verification_cost + tx_size + Assert.AreEqual(1269520, verificationGas + sizeGas); + // final assert + Assert.AreEqual(tx.NetworkFee, verificationGas + sizeGas); + } + + [TestMethod] + public void FeeIsSignatureContract_TestScope_NoScopeFAULT() + { + var wallet = TestUtils.GenerateTestWallet(""); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var acc = wallet.CreateAccount(); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + // Make transaction + // Manually creating script + + byte[] script; + using (ScriptBuilder sb = new()) + { + // self-transfer of 1e-8 GAS + BigInteger value = new BigDecimal(BigInteger.One, 8).Value; + sb.EmitDynamicCall(NativeContract.GAS.Hash, "transfer", acc.ScriptHash, acc.ScriptHash, value); + sb.Emit(OpCode.ASSERT); + script = sb.ToArray(); + } + + // trying with no scope + var attributes = Array.Empty(); + var signers = new[]{ new Signer + { + Account = acc.ScriptHash, + Scopes = WitnessScope.CustomContracts, + AllowedContracts = [NativeContract.NEO.Hash, NativeContract.GAS.Hash] + } }; + + // using this... + + // expects FAULT on execution of 'transfer' Application script + // due to lack of a valid witness validation + Assert.ThrowsExactly( + () => wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers, attributes)); + } + + [TestMethod] + public void FeeIsSignatureContract_UnexistingVerificationContractFAULT() + { + var wallet = TestUtils.GenerateTestWallet(""); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var acc = wallet.CreateAccount(); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshotCache.Commit(); + + // Make transaction + // Manually creating script + + byte[] script; + using (ScriptBuilder sb = new()) + { + // self-transfer of 1e-8 GAS + BigInteger value = new BigDecimal(BigInteger.One, 8).Value; + sb.EmitDynamicCall(NativeContract.GAS.Hash, "transfer", acc.ScriptHash, acc.ScriptHash, value, null); + sb.Emit(OpCode.ASSERT); + script = sb.ToArray(); + } + + // trying global scope + var signers = new[]{ new Signer + { + Account = acc.ScriptHash, + Scopes = WitnessScope.Global + } }; + + // creating new wallet with missing account for test + var walletWithoutAcc = TestUtils.GenerateTestWallet(""); + + // using this... + + // expects ArgumentException on execution of 'CalculateNetworkFee' due to + // null witness_script (no account in the wallet, no corresponding witness + // and no verification contract for the signer) + Assert.ThrowsExactly( + () => walletWithoutAcc.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers)); + } + + [TestMethod] + public void Transaction_Reverify_Hashes_Length_Unequal_To_Witnesses_Length() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + Transaction txSimple = new() + { + Version = 0x00, + Nonce = 0x01020304, + SystemFee = (long)BigInteger.Pow(10, 8), // 1 GAS + NetworkFee = 0x0000000000000001, + ValidUntilBlock = 0x01020304, + Attributes = [], + Signers = [ + new() + { + Account = UInt160.Parse("0x0001020304050607080900010203040506070809"), + Scopes = WitnessScope.Global + } + ], + Script = new byte[] { (byte)OpCode.PUSH1 }, + Witnesses = [], + }; + UInt160[] hashes = txSimple.GetScriptHashesForVerifying(snapshotCache); + Assert.HasCount(1, hashes); + Assert.AreNotEqual(VerifyResult.Succeed, + txSimple.VerifyStateDependent(TestProtocolSettings.Default, snapshotCache, new(), [])); + } + + [TestMethod] + public void Transaction_Serialize_Deserialize_Simple() + { + // good and simple transaction + Transaction txSimple = new() + { + Version = 0x00, + Nonce = 0x01020304, + SystemFee = (long)BigInteger.Pow(10, 8), // 1 GAS + NetworkFee = 0x0000000000000001, + ValidUntilBlock = 0x01020304, + Signers = [new() { Account = UInt160.Zero }], + Attributes = [], + Script = new[] { (byte)OpCode.PUSH1 }, + Witnesses = [Witness.Empty] + }; + + byte[] sTx = txSimple.ToArray(); + + // detailed hexstring info (basic checking) + Assert.AreEqual("00" + // version + "04030201" + // nonce + "00e1f50500000000" + // system fee (1 GAS) + "0100000000000000" + // network fee (1 datoshi) + "04030201" + // timelimit + "01000000000000000000000000000000000000000000" + // empty signer + "00" + // no attributes + "0111" + // push1 script + "010000", sTx.ToHexString()); // empty witnesses + + // try to deserialize + Transaction tx2 = sTx.AsSerializable(); + + Assert.AreEqual(0x00, tx2.Version); + Assert.AreEqual(0x01020304u, tx2.Nonce); + Assert.AreEqual(UInt160.Zero, tx2.Sender); + Assert.AreEqual(0x0000000005f5e100, tx2.SystemFee); // 1 GAS (long)BigInteger.Pow(10, 8) + Assert.AreEqual(0x0000000000000001, tx2.NetworkFee); + Assert.AreEqual(0x01020304u, tx2.ValidUntilBlock); + CollectionAssert.AreEqual(Array.Empty(), tx2.Attributes); + CollectionAssert.AreEqual(new Signer[] + { + new() + { + Account = UInt160.Zero, + AllowedContracts = [], + AllowedGroups = [], + Rules = [], + } + }, tx2.Signers); + Assert.IsTrue(tx2.Script.Span.SequenceEqual([(byte)OpCode.PUSH1])); + Assert.IsTrue(tx2.Witnesses[0].InvocationScript.Span.IsEmpty); + Assert.IsTrue(tx2.Witnesses[0].VerificationScript.Span.IsEmpty); + } + + [TestMethod] + public void Transaction_Serialize_Deserialize_DistinctSigners() + { + // the `Signers` must be distinct (regarding account) + var txDoubleSigners = new Transaction + { + Version = 0x00, + Nonce = 0x01020304, + SystemFee = (long)BigInteger.Pow(10, 8), // 1 GAS + NetworkFee = 0x0000000000000001, + ValidUntilBlock = 0x01020304, + Attributes = [], + Signers = + [ + new() + { + Account = UInt160.Parse("0x0001020304050607080900010203040506070809"), + Scopes = WitnessScope.Global + }, + new() + { + Account = UInt160.Parse("0x0001020304050607080900010203040506070809"), // same account as above + Scopes = WitnessScope.CalledByEntry // different scope, but still, same account (cannot do that) + } + ], + Script = new[] { (byte)OpCode.PUSH1 }, + Witnesses = [Witness.Empty] + }; + + var sTx = txDoubleSigners.ToArray(); + + // no need for detailed hexstring here (see basic tests for it) + var expected = "000403020100e1f50500000000010000000000000004030201020908070605040302010009080706050403020" + + "10080090807060504030201000908070605040302010001000111010000"; + Assert.AreEqual(expected, sTx.ToHexString()); + + // back to transaction (should fail, due to non-distinct signers) + Assert.ThrowsExactly(() => sTx.AsSerializable()); + } + + + [TestMethod] + public void Transaction_Serialize_Deserialize_MaxSizeSigners() + { + // the `Signers` must respect count + int maxSigners = 16; + + // -------------------------------------- + // this should pass (respecting max size) + var signers1 = new Signer[maxSigners]; + for (int i = 0; i < signers1.Length; i++) + { + string hex = i.ToString("X4"); + while (hex.Length < 40) + hex = hex.Insert(0, "0"); + signers1[i] = new Signer + { + Account = UInt160.Parse(hex), + Scopes = WitnessScope.CalledByEntry + }; + } + + var txSigners1 = new Transaction + { + Version = 0x00, + Nonce = 0x01020304, + SystemFee = (long)BigInteger.Pow(10, 8), // 1 GAS + NetworkFee = 0x0000000000000001, + ValidUntilBlock = 0x01020304, + Attributes = [], + Signers = signers1, // max + 1 (should fail) + Script = new[] { (byte)OpCode.PUSH1 }, + Witnesses = [Witness.Empty] + }; + + var sTx1 = txSigners1.ToArray(); + + // back to transaction (should fail, due to non-distinct signers) + Assert.ThrowsExactly(() => _ = sTx1.AsSerializable()); + + // ---------------------------- + // this should fail (max + 1) + + var signers = new Signer[maxSigners + 1]; + for (var i = 0; i < maxSigners + 1; i++) + { + var hex = i.ToString("X4"); + while (hex.Length < 40) + hex = hex.Insert(0, "0"); + signers[i] = new Signer + { + Account = UInt160.Parse(hex) + }; + } + + var txSigners = new Transaction + { + Version = 0x00, + Nonce = 0x01020304, + SystemFee = (long)BigInteger.Pow(10, 8), // 1 GAS + NetworkFee = 0x0000000000000001, + ValidUntilBlock = 0x01020304, + Attributes = [], + Signers = signers, // max + 1 (should fail) + Script = new[] { (byte)OpCode.PUSH1 }, + Witnesses = [Witness.Empty] + }; + + var sTx2 = txSigners.ToArray(); + + // back to transaction (should fail, due to non-distinct signers) + Assert.ThrowsExactly(() => sTx2.AsSerializable()); + } + + [TestMethod] + public void FeeIsSignatureContract_TestScope_FeeOnly_Default() + { + // None is supposed to be default + var signer = (Signer)RuntimeHelpers.GetUninitializedObject(typeof(Signer)); + Assert.AreEqual(WitnessScope.None, signer.Scopes); + + var wallet = TestUtils.GenerateTestWallet(""); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var acc = wallet.CreateAccount(); + + // Fake balance + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshotCache.Commit(); + + // Make transaction + // Manually creating script + + byte[] script; + using (var sb = new ScriptBuilder()) + { + // self-transfer of 1e-8 GAS + BigInteger value = new BigDecimal(BigInteger.One, 8).Value; + sb.EmitDynamicCall(NativeContract.GAS.Hash, "transfer", acc.ScriptHash, acc.ScriptHash, value, null); + sb.Emit(OpCode.ASSERT); + script = sb.ToArray(); + } + + // try to use fee only inside the smart contract + var signers = new[]{ + new Signer() + { + Account = acc.ScriptHash, + Scopes = WitnessScope.None + } + }; + + Assert.ThrowsExactly( + () => _ = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers)); + + // change to global scope + signers[0].Scopes = WitnessScope.Global; + + var tx = wallet.MakeTransaction(snapshotCache, script, acc.ScriptHash, signers); + + Assert.IsNotNull(tx); + + // ---- + // Sign + // ---- + + var data = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + bool signed = wallet.Sign(data); + Assert.IsTrue(signed); + + // get witnesses from signed 'data' + tx.Witnesses = data.GetWitnesses(); + Assert.HasCount(1, tx.Witnesses); + + // Fast check + Assert.IsTrue(tx.VerifyWitnesses(TestProtocolSettings.Default, snapshotCache, tx.NetworkFee)); + + // Check + long verificationGas = 0; + foreach (var witness in tx.Witnesses) + { + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshotCache, + settings: TestProtocolSettings.Default, gas: tx.NetworkFee); + engine.LoadScript(witness.VerificationScript); + engine.LoadScript(witness.InvocationScript); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsTrue(engine.ResultStack.Pop().GetBoolean()); + verificationGas += engine.FeeConsumed; + } + // get sizeGas + var sizeGas = tx.Size * NativeContract.Policy.GetFeePerByte(snapshotCache); + // final check on sum: verification_cost + tx_size + Assert.AreEqual(1228520, verificationGas + sizeGas); + // final assert + Assert.AreEqual(tx.NetworkFee, verificationGas + sizeGas); + } + + [TestMethod] + public void ToJson() + { + _uut.Script = TestUtils.GetByteArray(32, 0x42); + _uut.SystemFee = 4200000000; + _uut.Signers = [new() { Account = UInt160.Zero }]; + _uut.Attributes = []; + _uut.Witnesses = [Witness.Empty]; + + JObject jObj = _uut.ToJson(ProtocolSettings.Default); + Assert.IsNotNull(jObj); + Assert.AreEqual("0x0ab073429086d9e48fc87386122917989705d1c81fe4a60bf90e2fc228de3146", jObj["hash"]!.AsString()); + Assert.AreEqual(84, jObj["size"]!.AsNumber()); + Assert.AreEqual(0, jObj["version"]!.AsNumber()); + Assert.IsEmpty((JArray)jObj["attributes"]!); + Assert.AreEqual("0", jObj["netfee"]!.AsString()); + Assert.AreEqual("QiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA=", jObj["script"]!.AsString()); + Assert.AreEqual("4200000000", jObj["sysfee"]!.AsString()); + } + + [TestMethod] + public void Test_GetAttribute() + { + var tx = new Transaction + { + Attributes = [], + NetworkFee = 0, + Nonce = (uint)Environment.TickCount, + Script = new byte[Transaction.MaxTransactionSize], + Signers = [new Signer { Account = UInt160.Zero }], + SystemFee = 0, + ValidUntilBlock = 0, + Version = 0, + Witnesses = [], + }; + + Assert.IsNull(tx.GetAttribute()); + Assert.IsNull(tx.GetAttribute()); + + tx.Attributes = [new HighPriorityAttribute()]; + + Assert.IsNull(tx.GetAttribute()); + Assert.IsNotNull(tx.GetAttribute()); + } + + [TestMethod] + public void Test_VerifyStateIndependent() + { + var tx = new Transaction + { + Attributes = [], + NetworkFee = 0, + Nonce = (uint)Environment.TickCount, + Script = new byte[Transaction.MaxTransactionSize], + Signers = [new() { Account = UInt160.Zero }], + SystemFee = 0, + ValidUntilBlock = 0, + Version = 0, + Witnesses = [Witness.Empty], + }; + Assert.AreEqual(VerifyResult.OverSize, tx.VerifyStateIndependent(TestProtocolSettings.Default)); + tx.Script = Array.Empty(); + Assert.AreEqual(VerifyResult.Succeed, tx.VerifyStateIndependent(TestProtocolSettings.Default)); + + var walletA = TestUtils.GenerateTestWallet("123"); + var walletB = TestUtils.GenerateTestWallet("123"); + var walletC = TestUtils.GenerateTestWallet("123"); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + var a = walletA.CreateAccount(); + var b = walletB.CreateAccount(); + var c = walletC.CreateAccount(); + + var multiSignContract = Contract.CreateMultiSigContract(2, + [ + a.GetKey()!.PublicKey, + b.GetKey()!.PublicKey + ]); + var wrongMultisigContract = Contract.CreateMultiSigContract(2, + [ + a.GetKey()!.PublicKey, + c.GetKey()!.PublicKey + ]); + + walletA.CreateAccount(multiSignContract, a.GetKey()); + var acc = walletB.CreateAccount(multiSignContract, b.GetKey()); + + walletA.CreateAccount(wrongMultisigContract, a.GetKey()); + var wrongAcc = walletC.CreateAccount(wrongMultisigContract, c.GetKey()); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshotCache.Commit(); + + // Make transaction + + tx = walletA.MakeTransaction(snapshotCache, [ + new TransferOutput + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = acc.ScriptHash, + Value = new BigDecimal(BigInteger.One, 8) + } + ], acc.ScriptHash); + + // Sign + + var data = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsTrue(walletA.Sign(data)); + Assert.IsTrue(walletB.Sign(data)); + Assert.IsTrue(data.Completed); + + tx.Witnesses = data.GetWitnesses(); + Assert.AreEqual(VerifyResult.Succeed, tx.VerifyStateIndependent(TestProtocolSettings.Default)); + + // Different invocation script (contains signatures of A&C whereas originally signatures + // from A&B are required). + tx.Signers[0].Account = wrongAcc.ScriptHash; // temporary replace Sender's scripthash to be able to construct A&C signature. + var wrongData = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsTrue(walletA.Sign(wrongData)); + Assert.IsTrue(walletC.Sign(wrongData)); + Assert.IsTrue(wrongData.Completed); + + tx.Signers[0].Account = acc.ScriptHash; // get back the original value of Sender's scripthash. + tx.Witnesses[0].InvocationScript = wrongData.GetWitnesses()[0].InvocationScript.ToArray(); + Assert.AreEqual(VerifyResult.InvalidSignature, tx.VerifyStateIndependent(TestProtocolSettings.Default)); + } + + [TestMethod] + public void Test_VerifyStateDependent() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var height = NativeContract.Ledger.CurrentIndex(snapshotCache); + var tx = new Transaction() + { + Attributes = [], + NetworkFee = 55000, + Nonce = (uint)Environment.TickCount, + Script = Array.Empty(), + Signers = [new Signer() { Account = UInt160.Zero }], + SystemFee = 0, + ValidUntilBlock = height + 1, + Version = 0, + Witnesses = + [ + Witness.Empty, + new() { InvocationScript = ReadOnlyMemory.Empty, VerificationScript = new byte[1] } + ] + }; + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, tx.Sender); + var balance = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + balance.GetInteroperable().Balance = tx.NetworkFee; + var conflicts = new List(); + + Assert.AreEqual(VerifyResult.Invalid, tx.VerifyStateDependent(TestProtocolSettings.Default, snapshotCache, new(), conflicts)); + balance.GetInteroperable().Balance = 0; + tx.SystemFee = 10; + Assert.AreEqual(VerifyResult.InsufficientFunds, tx.VerifyStateDependent(TestProtocolSettings.Default, snapshotCache, new(), conflicts)); + + var walletA = TestUtils.GenerateTestWallet("123"); + var walletB = TestUtils.GenerateTestWallet("123"); + + var a = walletA.CreateAccount(); + var b = walletB.CreateAccount(); + + var multiSignContract = Contract.CreateMultiSigContract(2, + [ + a.GetKey()!.PublicKey, + b.GetKey()!.PublicKey + ]); + + walletA.CreateAccount(multiSignContract, a.GetKey()); + var acc = walletB.CreateAccount(multiSignContract, b.GetKey()); + + // Fake balance + + snapshotCache = TestBlockchain.GetTestSnapshotCache(); + key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + balance = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + balance.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + // Make transaction + + snapshotCache.Commit(); + tx = walletA.MakeTransaction(snapshotCache, new[] + { + new TransferOutput() + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = acc.ScriptHash, + Value = new BigDecimal(BigInteger.One,8) + } + }, acc.ScriptHash); + + // Sign + + var data = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsTrue(walletA.Sign(data)); + Assert.IsTrue(walletB.Sign(data)); + Assert.IsTrue(data.Completed); + + tx.Witnesses = data.GetWitnesses(); + Assert.AreEqual(VerifyResult.Succeed, tx.VerifyStateDependent(TestProtocolSettings.Default, snapshotCache, new(), [])); + } + + [TestMethod] + public void Test_VerifyStateInDependent_Multi() + { + var txData = Convert.FromBase64String("AHXd31W0NlsAAAAAAJRGawAAAAAA3g8CAAGSs5x3qmDym1fBc87ZF/F/0yGm6wEAX" + + "wsDAOQLVAIAAAAMFLqZBJj+L0XZPXNHHM9MBfCza5HnDBSSs5x3qmDym1fBc87ZF/F/0yGm6xTAHwwIdHJhbnNmZXIMFM924ovQ" + + "BixKR47jVWEBExnzz6TSQWJ9W1I5Af1KAQxAnZvOQOCdkM+j22dS5SdEncZVYVVi1F26MhheNzNImTD4Ekw5kFR6Fojs7gD57Bd" + + "euo8tLS1UXpzflmKcQ3pniAxAYvGgxtokrk6PVdduxCBwVbdfie+ZxiaDsjK0FYregl24cDr2v5cTLHrURVfJJ1is+4G6Jaer7n" + + "B1JrDrw+Qt6QxATA5GdR4rKFPPPQQ24+42OP2tz0HylG1LlANiOtIdag3ZPkUfZiBfEGoOteRD1O0UnMdJP4Su7PFhDuCdHu4Ml" + + "wxAuGFEk2m/rdruleBGYz8DIzExJtwb/TsFxZdHxo4VV8ktv2Nh71Fwhg2bhW2tq8hV6RK2GFXNAU72KAgf/Qv6BQxA0j3srkwY" + + "333KvGNtw7ZvSG8X36Tqu000CEtDx4SMOt8qhVYGMr9PClsUVcYFHdrJaodilx8ewXDHNIq+OnS7SfwVDCEDAJt1QOEPJWLl/Y+" + + "snq7CUWaliybkEjSP9ahpJ7+sIqIMIQMCBenO+upaHfxYCvIMjVqiRouwFI8aXkYF/GIsgOYEugwhAhS68M7qOmbxfn4eg56iX9" + + "i+1s2C5rtuaCUBiQZfRP8BDCECPpsy6om5TQZuZJsST9UOOW7pE2no4qauGxHBcNAiJW0MIQNAjc1BY5b2R4OsWH6h4Vk8V9n+q" + + "IDIpqGSDpKiWUd4BgwhAqeDS+mzLimB0VfLW706y0LP0R6lw7ECJNekTpjFkQ8bDCECuixw9ZlvNXpDGYcFhZ+uLP6hPhFyligA" + + "dys9WIqdSr0XQZ7Q3Do="); + + var tx = (Transaction)RuntimeHelpers.GetUninitializedObject(typeof(Transaction)); + MemoryReader reader = new(txData); + ((ISerializable)tx).Deserialize(ref reader); + + var settings = ProtocolSettings.Default with { Network = 844378958 }; + var result = tx.VerifyStateIndependent(settings); + Assert.AreEqual(VerifyResult.Succeed, result); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_VersionPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_VersionPayload.cs new file mode 100644 index 0000000000..4956eb84db --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_VersionPayload.cs @@ -0,0 +1,61 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_VersionPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.Collections; +using Neo.Extensions.IO; +using Neo.Network.P2P.Capabilities; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_VersionPayload +{ + [TestMethod] + public void SizeAndEndPoint_Get() + { + var test = new VersionPayload() { Capabilities = Array.Empty(), UserAgent = "neo3" }; + Assert.AreEqual(22, test.Size); + + test = VersionPayload.Create(123, 456, "neo3", new NodeCapability[] { new ServerCapability(NodeCapabilityType.TcpServer, 22) }); + Assert.AreEqual(25, test.Size); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var test = VersionPayload.Create(123, 456, "neo3", new NodeCapability[] { new ServerCapability(NodeCapabilityType.TcpServer, 22) }); + var clone = test.ToArray().AsSerializable(); + + CollectionAssert.AreEqual(test.Capabilities.ToByteArray(), clone.Capabilities.ToByteArray()); + Assert.AreEqual(test.UserAgent, clone.UserAgent); + Assert.AreEqual(test.Nonce, clone.Nonce); + Assert.AreEqual(test.Timestamp, clone.Timestamp); + CollectionAssert.AreEqual(test.Capabilities.ToByteArray(), clone.Capabilities.ToByteArray()); + + Assert.ThrowsExactly(() => _ = VersionPayload.Create(123, 456, "neo3", + new NodeCapability[] { + new ServerCapability(NodeCapabilityType.TcpServer, 22) , + new ServerCapability(NodeCapabilityType.TcpServer, 22) + }).ToArray().AsSerializable()); + + var buf = test.ToArray(); + buf[buf.Length - 2 - 1 - 1] += 3; // We've got 1 capability with 2 bytes, this adds three more to the array size. + buf = buf.Concat(new byte[] { 0xfe, 0x00 }).ToArray(); // Type = 0xfe, zero bytes of data. + buf = buf.Concat(new byte[] { 0xfd, 0x02, 0x00, 0x00 }).ToArray(); // Type = 0xfd, two bytes of data. + buf = buf.Concat(new byte[] { 0x10, 0x01, 0x00, 0x00, 0x00 }).ToArray(); // FullNode capability, 0x01 index. + + clone = buf.AsSerializable(); + Assert.HasCount(4, clone.Capabilities); + Assert.AreEqual(2, clone.Capabilities.OfType().Count()); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Witness.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Witness.cs new file mode 100644 index 0000000000..dad488fc34 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_Witness.cs @@ -0,0 +1,171 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Witness.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.Wallets; +using Neo.Wallets.NEP6; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_Witness +{ + + [TestMethod] + public void InvocationScript_Get() + { + var uut = Witness.Empty; + Assert.IsTrue(uut.InvocationScript.IsEmpty); + } + + private static Witness PrepareDummyWitness(int pubKeys, int m) + { + var address = new WalletAccount[pubKeys]; + var wallets = new NEP6Wallet[pubKeys]; + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + for (int x = 0; x < pubKeys; x++) + { + wallets[x] = TestUtils.GenerateTestWallet("123"); + address[x] = wallets[x].CreateAccount(); + } + + // Generate multisignature + + var multiSignContract = Contract.CreateMultiSigContract(m, address.Select(a => a.GetKey()!.PublicKey).ToArray()); + + for (int x = 0; x < pubKeys; x++) + { + wallets[x].CreateAccount(multiSignContract, address[x].GetKey()); + } + + // Sign + + var data = new ContractParametersContext(snapshotCache, new Transaction() + { + Attributes = [], + Signers = [new() { Account = multiSignContract.ScriptHash, Scopes = WitnessScope.CalledByEntry }], + NetworkFee = 0, + Nonce = 0, + Script = ReadOnlyMemory.Empty, + SystemFee = 0, + ValidUntilBlock = 0, + Version = 0, + Witnesses = [] + }, TestProtocolSettings.Default.Network); + + for (int x = 0; x < m; x++) + { + Assert.IsTrue(wallets[x].Sign(data)); + } + + Assert.IsTrue(data.Completed); + return data.GetWitnesses()[0]; + } + + [TestMethod] + public void MaxSize_OK() + { + var witness = PrepareDummyWitness(10, 10); + + // Check max size + + Assert.AreEqual(1023, witness.Size); + Assert.AreEqual(663, witness.InvocationScript.GetVarSize()); + Assert.AreEqual(360, witness.VerificationScript.GetVarSize()); + + var copy = witness.ToArray().AsSerializable(); + + Assert.IsTrue(witness.InvocationScript.Span.SequenceEqual(copy.InvocationScript.Span)); + Assert.IsTrue(witness.VerificationScript.Span.SequenceEqual(copy.VerificationScript.Span)); + } + + [TestMethod] + public void MaxSize_Error() + { + var witness = new Witness + { + InvocationScript = new byte[1025], + VerificationScript = new byte[10] + }; + + // Check max size + + Assert.ThrowsExactly(() => _ = witness.ToArray().AsSerializable()); + + // Check max size + + witness.InvocationScript = new byte[10]; + witness.VerificationScript = new byte[1025]; + Assert.ThrowsExactly(() => _ = witness.ToArray().AsSerializable()); + } + + [TestMethod] + public void InvocationScript_Set() + { + var uut = new Witness() { InvocationScript = new byte[] { 0, 32, 32, 20, 32, 32 } }; + Assert.AreEqual(6, uut.InvocationScript.Length); + Assert.AreEqual("002020142020", uut.InvocationScript.Span.ToHexString()); + } + + private static Witness MakeWitnessWithValues(int lenghtInvocation, int lengthVerification) + { + return new() + { + InvocationScript = TestUtils.GetByteArray(lenghtInvocation, 0x20), + VerificationScript = TestUtils.GetByteArray(lengthVerification, 0x20) + }; + } + + [TestMethod] + public void SizeWitness_Small_Arrary() + { + var uut = MakeWitnessWithValues(252, 253); + Assert.AreEqual(509, uut.Size); // (1 + 252*1) + (1 + 2 + 253*1) + } + + [TestMethod] + public void SizeWitness_Large_Arrary() + { + var uut = MakeWitnessWithValues(65535, 65536); + Assert.AreEqual(131079, uut.Size); // (1 + 2 + 65535*1) + (1 + 4 + 65536*1) + } + + [TestMethod] + public void ToJson() + { + var uut = MakeWitnessWithValues(2, 3); + JObject json = uut.ToJson(); + Assert.IsTrue(json.ContainsProperty("invocation")); + Assert.IsTrue(json.ContainsProperty("verification")); + Assert.AreEqual("ICA=", json["invocation"]!.AsString()); + Assert.AreEqual("ICAg", json["verification"]!.AsString()); + } + + [TestMethod] + public void TestEmpty() + { + var w1 = Witness.Empty; + var w2 = Witness.Empty; + Assert.AreEqual(2, w1.Size); + Assert.AreEqual(0, w1.InvocationScript.Length); + Assert.AreEqual(0, w1.VerificationScript.Length); + Assert.AreEqual(2, w2.Size); + Assert.AreEqual(0, w2.InvocationScript.Length); + Assert.AreEqual(0, w2.VerificationScript.Length); + Assert.IsFalse(ReferenceEquals(w1, w2)); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessCondition.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessCondition.cs new file mode 100644 index 0000000000..2808c7266c --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessCondition.cs @@ -0,0 +1,474 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_WitnessCondition.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Network.P2P.Payloads.Conditions; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_WitnessCondition +{ + [TestMethod] + public void Test_IEquatable_ScriptHashCondition() + { + var expected = new ScriptHashCondition + { + Hash = UInt160.Zero, + }; + + var actual = new ScriptHashCondition + { + Hash = UInt160.Zero, + }; + + var notEqual = new ScriptHashCondition + { + Hash = UInt160.Parse("0xfff4f52ca43d6bf4fec8647a60415b183303d961"), + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + } + + [TestMethod] + public void Test_IEquatable_GroupCondition() + { + var point = ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1); + var expected = new GroupCondition + { + Group = point, + }; + + var actual = new GroupCondition + { + Group = point, + }; + + var notEqual = new GroupCondition + { + Group = ECPoint.Parse("03b209fd4f53a7170ea4444e0ca0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1), + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + } + + [TestMethod] + public void Test_IEquatable_CalledByGroupCondition() + { + var point = ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1); + var expected = new CalledByGroupCondition + { + Group = point, + }; + + var actual = new CalledByGroupCondition + { + Group = point, + }; + + var notEqual = new CalledByGroupCondition + { + Group = ECPoint.Parse("03b209fd4f53a7170ea4444e0ca0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1), + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + } + + [TestMethod] + public void Test_IEquatable_CalledByEntryCondition() + { + var expected = new CalledByEntryCondition(); + + var actual = new CalledByEntryCondition(); + + var notEqual = new CalledByContractCondition + { + Hash = UInt160.Parse("0xfff4f52ca43d6bf4fec8647a60415b183303d961"), + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + } + + [TestMethod] + public void Test_IEquatable_CalledByContractCondition() + { + var expected = new CalledByContractCondition + { + Hash = UInt160.Zero, + }; + + var actual = new CalledByContractCondition + { + Hash = UInt160.Zero, + }; + + var notEqual = new CalledByContractCondition + { + Hash = UInt160.Parse("0xfff4f52ca43d6bf4fec8647a60415b183303d961"), + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + } + + [TestMethod] + public void Test_IEquatable_BooleanCondition() + { + var expected = new BooleanCondition + { + Expression = true, + }; + + var actual = new BooleanCondition + { + Expression = true, + }; + + var notEqual = new BooleanCondition + { + Expression = false, + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + } + + [TestMethod] + public void Test_IEquatable_AndCondition() + { + var point = ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1); + var hash = UInt160.Zero; + var expected = new AndCondition + { + Expressions = new WitnessCondition[] + { + new CalledByContractCondition { Hash = hash }, + new CalledByGroupCondition { Group = point } + } + }; + + var actual = new AndCondition + { + Expressions = new WitnessCondition[] + { + new CalledByContractCondition { Hash = hash }, + new CalledByGroupCondition { Group = point } + } + }; + + var notEqual = new AndCondition + { + Expressions = new WitnessCondition[] + { + new CalledByContractCondition { Hash = hash }, + } + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + } + + [TestMethod] + public void Test_IEquatable_OrCondition() + { + var point = ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1); + var hash = UInt160.Zero; + var expected = new OrCondition + { + Expressions = new WitnessCondition[] + { + new CalledByContractCondition { Hash = hash }, + new CalledByGroupCondition { Group = point } + } + }; + + var actual = new OrCondition + { + Expressions = new WitnessCondition[] + { + new CalledByContractCondition { Hash = hash }, + new CalledByGroupCondition { Group = point } + } + }; + + var notEqual = new OrCondition + { + Expressions = new WitnessCondition[] + { + new CalledByContractCondition { Hash = hash }, + } + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + } + + [TestMethod] + public void TestFromJson1() + { + var point = ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1); + var hash = UInt160.Zero; + var condition = new OrCondition + { + Expressions = new WitnessCondition[] + { + new CalledByContractCondition { Hash = hash }, + new CalledByGroupCondition { Group = point } + } + }; + var json = condition.ToJson(); + var new_condi = WitnessCondition.FromJson(json, 2); + Assert.IsTrue(new_condi is OrCondition); + var or_condi = (OrCondition)new_condi; + Assert.HasCount(2, or_condi.Expressions); + Assert.IsTrue(or_condi.Expressions[0] is CalledByContractCondition); + var cbcc = (CalledByContractCondition)(or_condi.Expressions[0]); + Assert.IsTrue(or_condi.Expressions[1] is CalledByGroupCondition); + var cbgc = (CalledByGroupCondition)(or_condi.Expressions[1]); + Assert.IsTrue(cbcc.Hash.Equals(hash)); + Assert.IsTrue(cbgc.Group.Equals(point)); + } + + [TestMethod] + public void TestFromJson2() + { + var point = ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1); + var hash1 = UInt160.Zero; + var hash2 = UInt160.Parse("0xd2a4cff31913016155e38e474a2c06d08be276cf"); + var jstr = "{\"type\":\"Or\",\"expressions\":[{\"type\":\"And\",\"expressions\":[{\"type\":\"CalledByContract\",\"hash\":\"0x0000000000000000000000000000000000000000\"},{\"type\":\"ScriptHash\",\"hash\":\"0xd2a4cff31913016155e38e474a2c06d08be276cf\"}]},{\"type\":\"Or\",\"expressions\":[{\"type\":\"CalledByGroup\",\"group\":\"03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c\"},{\"type\":\"Boolean\",\"expression\":true}]}]}"; + var json = (JObject)JToken.Parse(jstr)!; + var condi = WitnessCondition.FromJson(json, WitnessCondition.MaxNestingDepth); + var or_condi = (OrCondition)condi; + Assert.HasCount(2, or_condi.Expressions); + var and_condi = (AndCondition)or_condi.Expressions[0]; + var or_condi1 = (OrCondition)or_condi.Expressions[1]; + Assert.HasCount(2, and_condi.Expressions); + Assert.HasCount(2, or_condi1.Expressions); + var cbcc = (CalledByContractCondition)and_condi.Expressions[0]; + var cbsc = (ScriptHashCondition)and_condi.Expressions[1]; + Assert.IsTrue(cbcc.Hash.Equals(hash1)); + Assert.IsTrue(cbsc.Hash.Equals(hash2)); + var cbgc = (CalledByGroupCondition)or_condi1.Expressions[0]; + var bc = (BooleanCondition)or_condi1.Expressions[1]; + Assert.IsTrue(cbgc.Group.Equals(point)); + Assert.IsTrue(bc.Expression); + } + + [TestMethod] + public void Test_WitnessCondition_Nesting() + { + WitnessCondition nested = new OrCondition + { + Expressions = [ + new OrCondition { Expressions = [new BooleanCondition { Expression = true }] } + ] + }; + + var buf = nested.ToArray(); + var reader = new MemoryReader(buf); + + var deser = WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + Assert.AreEqual(nested, deser); + + nested = new AndCondition + { + Expressions = [ + new AndCondition { Expressions = [new BooleanCondition { Expression = true }] } + ] + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + deser = WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + Assert.AreEqual(nested, deser); + + nested = new NotCondition + { + Expression = new NotCondition + { + Expression = new BooleanCondition { Expression = true } + } + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + deser = WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + Assert.AreEqual(nested, deser); + + // Overflow maxNestingDepth + nested = new OrCondition + { + Expressions = [ + new OrCondition { + Expressions = [ + new OrCondition { Expressions = [new BooleanCondition { Expression = true }] } + ] + } + ] + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + var exceptionHappened = false; + // CS8175 prevents from using Assert.ThrowsException here + try + { + WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + } + catch (FormatException) + { + exceptionHappened = true; + } + Assert.IsTrue(exceptionHappened); + + nested = new AndCondition + { + Expressions = [ + new AndCondition { + Expressions = [ + new AndCondition { Expressions = [new BooleanCondition { Expression = true }] } + ] + } + ] + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + exceptionHappened = false; + // CS8175 prevents from using Assert.ThrowsException here + try + { + WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + } + catch (FormatException) + { + exceptionHappened = true; + } + Assert.IsTrue(exceptionHappened); + + nested = new NotCondition + { + Expression = new NotCondition + { + Expression = new NotCondition + { + Expression = new BooleanCondition { Expression = true } + } + } + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + exceptionHappened = false; + // CS8175 prevents from using Assert.ThrowsException here + try + { + WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + } + catch (FormatException) + { + exceptionHappened = true; + } + Assert.IsTrue(exceptionHappened); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessRule.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessRule.cs new file mode 100644 index 0000000000..191a64e75b --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessRule.cs @@ -0,0 +1,63 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_WitnessRule.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.P2P.Payloads.Conditions; + +namespace Neo.UnitTests.Network.P2P.Payloads; + +[TestClass] +public class UT_WitnessRule +{ + [TestMethod] + public void Test_IEquatable() + { + var expected = new WitnessRule + { + Action = WitnessRuleAction.Allow, + Condition = new BooleanCondition + { + Expression = true, + } + }; + + var actual = new WitnessRule + { + Action = WitnessRuleAction.Allow, + Condition = new BooleanCondition + { + Expression = true, + } + }; + + var notEqual = new WitnessRule + { + Action = WitnessRuleAction.Deny, + Condition = new BooleanCondition + { + Expression = false, + } + }; + + Assert.IsTrue(expected.Equals(expected)); + + Assert.AreEqual(expected, actual); + Assert.IsTrue(expected == actual); + Assert.IsTrue(expected.Equals(actual)); + + Assert.AreNotEqual(expected, notEqual); + Assert.IsTrue(expected != notEqual); + Assert.IsFalse(expected.Equals(notEqual)); + + Assert.IsNotNull(expected); + Assert.IsFalse(expected.Equals(null)); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/UT_ChannelsConfig.cs b/tests/Neo.UnitTests/Network/P2P/UT_ChannelsConfig.cs new file mode 100644 index 0000000000..73b510b771 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/UT_ChannelsConfig.cs @@ -0,0 +1,42 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ChannelsConfig.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P; +using System.Net; + +namespace Neo.UnitTests.Network.P2P; + +[TestClass] +public class UT_ChannelsConfig +{ + [TestMethod] + public void CreateTest() + { + var config = new ChannelsConfig(); + + Assert.IsNull(config.Tcp); + Assert.AreEqual(10, config.MinDesiredConnections); + Assert.AreEqual(40, config.MaxConnections); + Assert.AreEqual(3, config.MaxConnectionsPerAddress); + + config.Tcp = new IPEndPoint(IPAddress.Any, 21); + config.MaxConnectionsPerAddress++; + config.MaxConnections++; + config.MinDesiredConnections++; + + Assert.AreSame(config.Tcp, config.Tcp); + CollectionAssert.AreEqual(IPAddress.Any.GetAddressBytes(), config.Tcp.Address.GetAddressBytes()); + Assert.AreEqual(21, config.Tcp.Port); + Assert.AreEqual(11, config.MinDesiredConnections); + Assert.AreEqual(41, config.MaxConnections); + Assert.AreEqual(4, config.MaxConnectionsPerAddress); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/UT_LocalNode.cs b/tests/Neo.UnitTests/Network/P2P/UT_LocalNode.cs new file mode 100644 index 0000000000..2cf42731f9 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/UT_LocalNode.cs @@ -0,0 +1,60 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_LocalNode.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO; +using Akka.TestKit.MsTest; +using Neo.Network.P2P; +using System.Net; + +namespace Neo.UnitTests.Network.P2P; + +[TestClass] +public class UT_LocalNode : TestKit +{ + private static NeoSystem _system = null!; + + [TestInitialize] + public void Init() + { + _system = TestBlockchain.GetSystem(); + } + + [TestMethod] + public void TestDefaults() + { + var senderProbe = CreateTestProbe(); + senderProbe.Send(_system.LocalNode, new ChannelsConfig()); // No Tcp + senderProbe.Send(_system.LocalNode, new LocalNode.GetInstance()); + var localnode = senderProbe.ExpectMsg(cancellationToken: CancellationToken.None); + + Assert.AreEqual(0, localnode.ListenerTcpPort); + Assert.AreEqual(3, localnode.Config.MaxConnectionsPerAddress); + Assert.AreEqual(10, localnode.Config.MinDesiredConnections); + Assert.AreEqual(40, localnode.Config.MaxConnections); + Assert.AreEqual(0, localnode.UnconnectedCount); + } + + [TestMethod] + public void ProcessesTcpConnectedAfterConfigArrives() + { + var connectionProbe = CreateTestProbe(); + var remote = new IPEndPoint(IPAddress.Parse("192.0.2.1"), 20333); + var local = new IPEndPoint(IPAddress.Loopback, 20334); + + connectionProbe.Send(_system.LocalNode, new Tcp.Connected(remote, local)); + connectionProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(200), cancellationToken: CancellationToken.None); + + var configProbe = CreateTestProbe(); + configProbe.Send(_system.LocalNode, new ChannelsConfig()); + + connectionProbe.ExpectMsg(TimeSpan.FromSeconds(1), cancellationToken: CancellationToken.None); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/UT_Message.cs b/tests/Neo.UnitTests/Network/P2P/UT_Message.cs new file mode 100644 index 0000000000..713f2a92f7 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/UT_Message.cs @@ -0,0 +1,184 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Message.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO; +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.VM; + +namespace Neo.UnitTests.Network.P2P; + +[TestClass] +public class UT_Message +{ + [TestMethod] + public void Serialize_Deserialize() + { + var payload = PingPayload.Create(uint.MaxValue); + var msg = Message.Create(MessageCommand.Ping, payload); + var buffer = msg.ToArray(); + var copy = buffer.AsSerializable(); + var payloadCopy = (PingPayload)copy.Payload!; + + Assert.AreEqual(msg.Command, copy.Command); + Assert.AreEqual(msg.Flags, copy.Flags); + Assert.AreEqual(payload.Size + 3, msg.Size); + + Assert.AreEqual(payload.LastBlockIndex, payloadCopy.LastBlockIndex); + Assert.AreEqual(payload.Nonce, payloadCopy.Nonce); + Assert.AreEqual(payload.Timestamp, payloadCopy.Timestamp); + } + + [TestMethod] + public void Serialize_Deserialize_WithoutPayload() + { + var msg = Message.Create(MessageCommand.GetAddr); + var buffer = msg.ToArray(); + var copy = buffer.AsSerializable(); + + Assert.AreEqual(msg.Command, copy.Command); + Assert.AreEqual(msg.Flags, copy.Flags); + Assert.IsNull(copy.Payload); + } + + [TestMethod] + public void ToArray() + { + var payload = PingPayload.Create(uint.MaxValue); + var msg = Message.Create(MessageCommand.Ping, payload); + _ = msg.ToArray(); + + Assert.AreEqual(payload.Size + 3, msg.Size); + } + + [TestMethod] + public void Serialize_Deserialize_ByteString() + { + var payload = PingPayload.Create(uint.MaxValue); + var msg = Message.Create(MessageCommand.Ping, payload); + var buffer = ByteString.CopyFrom(msg.ToArray()); + var length = Message.TryDeserialize(buffer, out var copy); + + var payloadCopy = (PingPayload)copy!.Payload!; + + Assert.AreEqual(msg.Command, copy.Command); + Assert.AreEqual(msg.Flags, copy.Flags); + + Assert.AreEqual(payload.LastBlockIndex, payloadCopy.LastBlockIndex); + Assert.AreEqual(payload.Nonce, payloadCopy.Nonce); + Assert.AreEqual(payload.Timestamp, payloadCopy.Timestamp); + + Assert.AreEqual(length, buffer.Count); + } + + [TestMethod] + public void ToArray_WithoutPayload() + { + var msg = Message.Create(MessageCommand.GetAddr); + _ = msg.ToArray(); + } + + [TestMethod] + public void Serialize_Deserialize_WithoutPayload_ByteString() + { + var msg = Message.Create(MessageCommand.GetAddr); + var buffer = ByteString.CopyFrom(msg.ToArray()); + var length = Message.TryDeserialize(buffer, out var copy); + + Assert.AreEqual(msg.Command, copy!.Command); + Assert.AreEqual(msg.Flags, copy.Flags); + Assert.IsNull(copy.Payload); + + Assert.AreEqual(length, buffer.Count); + } + + [TestMethod] + public void MultipleSizes() + { + var msg = Message.Create(MessageCommand.GetAddr); + var buffer = msg.ToArray(); + + var length = Message.TryDeserialize(ByteString.Empty, out var copy); + Assert.AreEqual(0, length); + Assert.IsNull(copy); + + length = Message.TryDeserialize(ByteString.CopyFrom(buffer), out copy); + Assert.AreEqual(buffer.Length, length); + Assert.IsNotNull(copy); + + length = Message.TryDeserialize(ByteString.CopyFrom(buffer.Take(2).Concat(new byte[] { 0xFD }).ToArray()), out copy); + Assert.AreEqual(0, length); + Assert.IsNull(copy); + + length = Message.TryDeserialize(ByteString.CopyFrom(buffer.Take(2).Concat(new byte[] { 0xFD, buffer[2], 0x00 }).Concat(buffer.Skip(3)).ToArray()), out copy); + Assert.AreEqual(buffer.Length + 2, length); + Assert.IsNotNull(copy); + + length = Message.TryDeserialize(ByteString.CopyFrom(buffer.Take(2).Concat(new byte[] { 0xFD, 0x01, 0x00 }).Concat(buffer.Skip(3)).ToArray()), out copy); + Assert.AreEqual(0, length); + Assert.IsNull(copy); + + length = Message.TryDeserialize(ByteString.CopyFrom(buffer.Take(2).Concat(new byte[] { 0xFE }).Concat(buffer.Skip(3)).ToArray()), out copy); + Assert.AreEqual(0, length); + Assert.IsNull(copy); + + length = Message.TryDeserialize(ByteString.CopyFrom(buffer.Take(2).Concat(new byte[] { 0xFE, buffer[2], 0x00, 0x00, 0x00 }).Concat(buffer.Skip(3)).ToArray()), out copy); + Assert.AreEqual(buffer.Length + 4, length); + Assert.IsNotNull(copy); + + length = Message.TryDeserialize(ByteString.CopyFrom(buffer.Take(2).Concat(new byte[] { 0xFF }).Concat(buffer.Skip(3)).ToArray()), out copy); + Assert.AreEqual(0, length); + Assert.IsNull(copy); + + length = Message.TryDeserialize(ByteString.CopyFrom(buffer.Take(2).Concat(new byte[] { 0xFF, buffer[2], 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }).Concat(buffer.Skip(3)).ToArray()), out copy); + Assert.AreEqual(buffer.Length + 8, length); + Assert.IsNotNull(copy); + + // Big message + + Assert.ThrowsExactly(() => _ = Message.TryDeserialize(ByteString.CopyFrom(buffer.Take(2).Concat(new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }).Concat(buffer.Skip(3)).ToArray()), out copy)); + } + + [TestMethod] + public void Compression() + { + var payload = new Transaction() + { + Nonce = 1, + Version = 0, + Attributes = [], + Script = new byte[] { (byte)OpCode.PUSH1 }, + Signers = [new() { Account = UInt160.Zero }], + Witnesses = [Witness.Empty], + }; + + var msg = Message.Create(MessageCommand.Transaction, payload); + var buffer = msg.ToArray(); + + Assert.HasCount(56, buffer); + + byte[] script = new byte[100]; + Array.Fill(script, (byte)OpCode.PUSH2); + payload.Script = script; + msg = Message.Create(MessageCommand.Transaction, payload); + buffer = msg.ToArray(); + + Assert.HasCount(30, buffer); + Assert.IsTrue(msg.Flags.HasFlag(MessageFlags.Compressed)); + + _ = Message.TryDeserialize(ByteString.CopyFrom(msg.ToArray()), out var copy); + Assert.IsNotNull(copy); + + Assert.IsTrue(copy.Flags.HasFlag(MessageFlags.Compressed)); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/UT_RemoteNode.cs b/tests/Neo.UnitTests/Network/P2P/UT_RemoteNode.cs new file mode 100644 index 0000000000..280713fa36 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/UT_RemoteNode.cs @@ -0,0 +1,94 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_RemoteNode.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO; +using Akka.TestKit.MsTest; +using Neo.Extensions.IO; +using Neo.Network.P2P; +using Neo.Network.P2P.Capabilities; +using Neo.Network.P2P.Payloads; +using System.Net; + +namespace Neo.UnitTests.Network.P2P; + +[TestClass] +public class UT_RemoteNode : TestKit +{ + private static NeoSystem _system = null!; + + public UT_RemoteNode() + : base($"remote-node-mailbox {{ mailbox-type: \"{typeof(RemoteNodeMailbox).AssemblyQualifiedName}\" }}") + { + } + + [ClassInitialize] + public static void TestSetup(TestContext ctx) + { + _system = TestBlockchain.GetSystem(); + } + + [TestMethod] + public void RemoteNode_Test_Abort_DifferentNetwork() + { + var connectionTestProbe = CreateTestProbe(); + var remoteNodeActor = ActorOfAsTestActorRef(() => new RemoteNode(_system, new LocalNode(_system), connectionTestProbe, null!, null!, new ChannelsConfig())); + + var msg = Message.Create(MessageCommand.Version, new VersionPayload + { + UserAgent = "".PadLeft(1024, '0'), + Nonce = 1, + Network = 2, + Timestamp = 5, + Version = 6, + Capabilities = + [ + new ServerCapability(NodeCapabilityType.TcpServer, 25) + ] + }); + + var testProbe = CreateTestProbe(); + testProbe.Send(remoteNodeActor, new Tcp.Received((ByteString)msg.ToArray())); + + connectionTestProbe.ExpectMsg(cancellationToken: CancellationToken.None); + } + + [TestMethod] + public void RemoteNode_Test_Accept_IfSameNetwork() + { + var connectionTestProbe = CreateTestProbe(); + var remoteNodeActor = ActorOfAsTestActorRef(() => + new RemoteNode(_system, + new LocalNode(_system), + connectionTestProbe, + new IPEndPoint(IPAddress.Parse("192.168.1.2"), 8080), new IPEndPoint(IPAddress.Parse("192.168.1.1"), 8080), new ChannelsConfig())); + + var msg = Message.Create(MessageCommand.Version, new VersionPayload() + { + UserAgent = "Unit Test".PadLeft(1024, '0'), + Nonce = 1, + Network = TestProtocolSettings.Default.Network, + Timestamp = 5, + Version = 6, + Capabilities = + [ + new ServerCapability(NodeCapabilityType.TcpServer, 25) + ] + }); + + var testProbe = CreateTestProbe(); + testProbe.Send(remoteNodeActor, new Tcp.Received((ByteString)msg.ToArray())); + + var verackMessage = connectionTestProbe.ExpectMsg(cancellationToken: CancellationToken.None); + + //Verack + Assert.HasCount(3, verackMessage.Data); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/UT_RemoteNodeMailbox.cs b/tests/Neo.UnitTests/Network/P2P/UT_RemoteNodeMailbox.cs new file mode 100644 index 0000000000..8e573d5113 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/UT_RemoteNodeMailbox.cs @@ -0,0 +1,193 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_RemoteNodeMailbox.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.IO; +using Akka.TestKit.MsTest; +using Neo.IO; +using Neo.Network.P2P; + +namespace Neo.UnitTests.Network.P2P; + +[TestClass] +public class UT_RemoteNodeMailbox : TestKit +{ + RemoteNodeMailbox uut = null!; + + [TestCleanup] + public void Cleanup() + { + Shutdown(); + } + + [TestInitialize] + public void TestSetup() + { + ActorSystem system = Sys; + var config = DefaultConfig; + var akkaSettings = new Settings(system, config); + uut = new RemoteNodeMailbox(akkaSettings, config); + } + + [TestMethod] + public void RemoteNode_Test_IsHighPriority() + { + ISerializable? s = null; + + //handshaking + Assert.IsTrue(uut.IsHighPriority(Message.Create(MessageCommand.Version, s))); + Assert.IsTrue(uut.IsHighPriority(Message.Create(MessageCommand.Verack, s))); + + //connectivity + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.GetAddr, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.Addr, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.Ping, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.Pong, s))); + + //synchronization + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.GetHeaders, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.Headers, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.GetBlocks, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.Mempool, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.Inv, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.GetData, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.NotFound, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.Transaction, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.Block, s))); + Assert.IsTrue(uut.IsHighPriority(Message.Create(MessageCommand.Extensible, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.Reject, s))); + + //SPV protocol + Assert.IsTrue(uut.IsHighPriority(Message.Create(MessageCommand.FilterLoad, s))); + Assert.IsTrue(uut.IsHighPriority(Message.Create(MessageCommand.FilterAdd, s))); + Assert.IsTrue(uut.IsHighPriority(Message.Create(MessageCommand.FilterClear, s))); + Assert.IsFalse(uut.IsHighPriority(Message.Create(MessageCommand.MerkleBlock, s))); + + //others + Assert.IsTrue(uut.IsHighPriority(Message.Create(MessageCommand.Alert, s))); + + // high priority commands + Assert.IsTrue(uut.IsHighPriority(new Tcp.ConnectionClosed())); + Assert.IsTrue(uut.IsHighPriority(new Connection.Close())); + Assert.IsTrue(uut.IsHighPriority(new Connection.Ack())); + + // any random object should not have priority + object obj = null!; + Assert.IsFalse(uut.IsHighPriority(obj)); + } + + public void ProtocolHandlerMailbox_Test_ShallDrop() + { + // using this for messages + ISerializable? s = null; + Message msg; // multiple uses + // empty queue + IEnumerable emptyQueue = Enumerable.Empty(); + + // any random object (non Message) should be dropped + object obj = null!; + Assert.IsTrue(uut.ShallDrop(obj, emptyQueue)); + + //handshaking + // Version (no drop) + msg = Message.Create(MessageCommand.Version, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // Verack (no drop) + msg = Message.Create(MessageCommand.Verack, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + + //connectivity + // GetAddr (drop) + msg = Message.Create(MessageCommand.GetAddr, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsTrue(uut.ShallDrop(msg, new object[] { msg })); + // Addr (no drop) + msg = Message.Create(MessageCommand.Addr, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // Ping (no drop) + msg = Message.Create(MessageCommand.Ping, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // Pong (no drop) + msg = Message.Create(MessageCommand.Pong, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + + //synchronization + // GetHeaders (drop) + msg = Message.Create(MessageCommand.GetHeaders, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsTrue(uut.ShallDrop(msg, new object[] { msg })); + // Headers (no drop) + msg = Message.Create(MessageCommand.Headers, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // GetBlocks (drop) + msg = Message.Create(MessageCommand.GetBlocks, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsTrue(uut.ShallDrop(msg, new object[] { msg })); + // Mempool (drop) + msg = Message.Create(MessageCommand.Mempool, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsTrue(uut.ShallDrop(msg, new object[] { msg })); + // Inv (no drop) + msg = Message.Create(MessageCommand.Inv, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // NotFound (no drop) + msg = Message.Create(MessageCommand.NotFound, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // Transaction (no drop) + msg = Message.Create(MessageCommand.Transaction, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // Block (no drop) + msg = Message.Create(MessageCommand.Block, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // Consensus (no drop) + msg = Message.Create(MessageCommand.Extensible, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // Reject (no drop) + msg = Message.Create(MessageCommand.Reject, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + + //SPV protocol + // FilterLoad (no drop) + msg = Message.Create(MessageCommand.FilterLoad, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // FilterAdd (no drop) + msg = Message.Create(MessageCommand.FilterAdd, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // FilterClear (no drop) + msg = Message.Create(MessageCommand.FilterClear, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + // MerkleBlock (no drop) + msg = Message.Create(MessageCommand.MerkleBlock, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + + //others + // Alert (no drop) + msg = Message.Create(MessageCommand.Alert, s); + Assert.IsFalse(uut.ShallDrop(msg, emptyQueue)); + Assert.IsFalse(uut.ShallDrop(msg, new object[] { msg })); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/UT_TaskManagerMailbox.cs b/tests/Neo.UnitTests/Network/P2P/UT_TaskManagerMailbox.cs new file mode 100644 index 0000000000..4273dc361a --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/UT_TaskManagerMailbox.cs @@ -0,0 +1,59 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_TaskManagerMailbox.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.TestKit.MsTest; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P; + +[TestClass] +public class UT_TaskManagerMailbox : TestKit +{ + TaskManagerMailbox uut = null!; + + [TestCleanup] + public void Cleanup() + { + Shutdown(); + } + + [TestInitialize] + public void TestSetup() + { + ActorSystem system = Sys; + var config = DefaultConfig; + var akkaSettings = new Settings(system, config); + uut = new TaskManagerMailbox(akkaSettings, config); + } + + [TestMethod] + public void TaskManager_Test_IsHighPriority() + { + // high priority + Assert.IsTrue(uut.IsHighPriority(new TaskManager.Register(null!))); + Assert.IsTrue(uut.IsHighPriority(new TaskManager.RestartTasks(null!))); + + // low priority + // -> NewTasks: generic InvPayload + Assert.IsFalse(uut.IsHighPriority(new TaskManager.NewTasks(new InvPayload { Hashes = null! }))); + + // high priority + // -> NewTasks: payload Block or Consensus + Assert.IsTrue(uut.IsHighPriority(new TaskManager.NewTasks(new InvPayload { Type = InventoryType.Block, Hashes = null! }))); + Assert.IsTrue(uut.IsHighPriority(new TaskManager.NewTasks(new InvPayload { Type = InventoryType.Extensible, Hashes = null! }))); + + // any random object should not have priority + object obj = null!; + Assert.IsFalse(uut.IsHighPriority(obj)); + } +} diff --git a/tests/Neo.UnitTests/Network/P2P/UT_TaskSession.cs b/tests/Neo.UnitTests/Network/P2P/UT_TaskSession.cs new file mode 100644 index 0000000000..79fd3a4a25 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/UT_TaskSession.cs @@ -0,0 +1,38 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_TaskSession.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Network.P2P; +using Neo.Network.P2P.Capabilities; +using Neo.Network.P2P.Payloads; + +namespace Neo.UnitTests.Network.P2P; + +[TestClass] +public class UT_TaskSession +{ + [TestMethod] + public void CreateTest() + { + var ses = new TaskSession(new VersionPayload() { Capabilities = new NodeCapability[] { new FullNodeCapability(123) }, UserAgent = "" }); + + Assert.IsFalse(ses.HasTooManyTasks); + Assert.AreEqual((uint)123, ses.LastBlockIndex); + Assert.IsEmpty(ses.IndexTasks); + Assert.IsTrue(ses.IsFullNode); + + ses = new TaskSession(new VersionPayload() { Capabilities = Array.Empty(), UserAgent = "" }); + + Assert.IsFalse(ses.HasTooManyTasks); + Assert.AreEqual((uint)0, ses.LastBlockIndex); + Assert.IsEmpty(ses.IndexTasks); + Assert.IsFalse(ses.IsFullNode); + } +} diff --git a/tests/Neo.UnitTests/Persistence/TestMemoryStoreProvider.cs b/tests/Neo.UnitTests/Persistence/TestMemoryStoreProvider.cs new file mode 100644 index 0000000000..d690bad3a6 --- /dev/null +++ b/tests/Neo.UnitTests/Persistence/TestMemoryStoreProvider.cs @@ -0,0 +1,22 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TestMemoryStoreProvider.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.Persistence.Providers; + +namespace Neo.UnitTests.Persistence; + +public class TestMemoryStoreProvider(MemoryStore memoryStore) : IStoreProvider +{ + public MemoryStore MemoryStore { get; set; } = memoryStore; + public string Name => nameof(MemoryStore); + public IStore GetStore(string? path) => MemoryStore; +} diff --git a/tests/Neo.UnitTests/Persistence/UT_CloneCache.cs b/tests/Neo.UnitTests/Persistence/UT_CloneCache.cs new file mode 100644 index 0000000000..0f90967448 --- /dev/null +++ b/tests/Neo.UnitTests/Persistence/UT_CloneCache.cs @@ -0,0 +1,189 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_CloneCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Persistence; +using Neo.Persistence.Providers; +using Neo.SmartContract; +using System.Text; + +namespace Neo.UnitTests.Persistence; + +[TestClass] +public class UT_CloneCache +{ + private readonly MemoryStore _store = new(); + + private static readonly StorageKey s_key1 = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key1") }; + private static readonly StorageKey s_key2 = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key2") }; + private static readonly StorageKey s_key3 = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key3") }; + private static readonly StorageKey s_key4 = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key4") }; + + private static readonly StorageItem s_value1 = new(Encoding.UTF8.GetBytes("value1")); + private static readonly StorageItem s_value2 = new(Encoding.UTF8.GetBytes("value2")); + private static readonly StorageItem s_value3 = new(Encoding.UTF8.GetBytes("value3")); + + [TestMethod] + public void TestCloneCache() + { + var myDataCache = new StoreCache(_store); + var clonedCache = myDataCache.CloneCache(); + Assert.IsNotNull(clonedCache); + } + + [TestMethod] + public void TestAddInternal() + { + var myDataCache = new StoreCache(_store); + var clonedCache = myDataCache.CloneCache(); + + clonedCache.Add(s_key1, s_value1); + Assert.AreEqual(s_value1, clonedCache[s_key1]); + + clonedCache.Commit(); + Assert.IsTrue(myDataCache[s_key1].Value.Span.SequenceEqual(s_value1.Value.Span)); + } + + [TestMethod] + public void TestDeleteInternal() + { + var myDataCache = new StoreCache(_store); + var clonedCache = myDataCache.CloneCache(); + + myDataCache.Add(s_key1, s_value1); + clonedCache.Delete(s_key1); // trackable.State = TrackState.Deleted + clonedCache.Commit(); + + Assert.IsNull(clonedCache.TryGet(s_key1)); + Assert.IsNull(myDataCache.TryGet(s_key1)); + } + + [TestMethod] + public void TestFindInternal() + { + var myDataCache = new StoreCache(_store); + var clonedCache = myDataCache.CloneCache(); + + clonedCache.Add(s_key1, s_value1); + myDataCache.Add(s_key2, s_value2); + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + + var items = clonedCache.Find(s_key1); + Assert.AreEqual(s_key1, items.ElementAt(0).Key); + Assert.AreEqual(s_value1, items.ElementAt(0).Value); + Assert.AreEqual(1, items.Count()); + + items = clonedCache.Find(s_key2); + Assert.AreEqual(s_key2, items.ElementAt(0).Key); + Assert.IsTrue(s_value2.EqualsTo(items.ElementAt(0).Value)); + Assert.AreEqual(1, items.Count()); + + items = clonedCache.Find(s_key3); + Assert.AreEqual(s_key3, items.ElementAt(0).Key); + Assert.IsTrue(s_value3.EqualsTo(items.ElementAt(0).Value)); + Assert.AreEqual(1, items.Count()); + + items = clonedCache.Find(s_key4); + Assert.AreEqual(0, items.Count()); + } + + [TestMethod] + public void TestGetInternal() + { + var myDataCache = new StoreCache(_store); + var clonedCache = myDataCache.CloneCache(); + + clonedCache.Add(s_key1, s_value1); + myDataCache.Add(s_key2, s_value2); + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + + Assert.IsTrue(s_value1.EqualsTo(clonedCache[s_key1])); + Assert.IsTrue(s_value2.EqualsTo(clonedCache[s_key2])); + Assert.IsTrue(s_value3.EqualsTo(clonedCache[s_key3])); + + void Action() + { + var item = clonedCache[s_key4]; + } + Assert.ThrowsExactly(Action); + } + + [TestMethod] + public void TestTryGetInternal() + { + var myDataCache = new StoreCache(_store); + var clonedCache = myDataCache.CloneCache(); + + clonedCache.Add(s_key1, s_value1); + myDataCache.Add(s_key2, s_value2); + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + + Assert.IsTrue(s_value1.EqualsTo(clonedCache.TryGet(s_key1)!)); + Assert.IsTrue(s_value2.EqualsTo(clonedCache.TryGet(s_key2)!)); + Assert.IsTrue(s_value3.EqualsTo(clonedCache.TryGet(s_key3)!)); + Assert.IsNull(clonedCache.TryGet(s_key4)); + } + + [TestMethod] + public void TestUpdateInternal() + { + var myDataCache = new StoreCache(_store); + var clonedCache = myDataCache.CloneCache(); + + clonedCache.Add(s_key1, s_value1); + myDataCache.Add(s_key2, s_value2); + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + + clonedCache.GetAndChange(s_key1)!.Value = Encoding.Default.GetBytes("value_new_1"); + clonedCache.GetAndChange(s_key2)!.Value = Encoding.Default.GetBytes("value_new_2"); + clonedCache.GetAndChange(s_key3)!.Value = Encoding.Default.GetBytes("value_new_3"); + + clonedCache.Commit(); + + StorageItem value_new_1 = new(Encoding.UTF8.GetBytes("value_new_1")); + StorageItem value_new_2 = new(Encoding.UTF8.GetBytes("value_new_2")); + StorageItem value_new_3 = new(Encoding.UTF8.GetBytes("value_new_3")); + + Assert.IsTrue(value_new_1.EqualsTo(clonedCache[s_key1])); + Assert.IsTrue(value_new_2.EqualsTo(clonedCache[s_key2])); + Assert.IsTrue(value_new_3.EqualsTo(clonedCache[s_key3])); + Assert.IsTrue(value_new_2.EqualsTo(clonedCache[s_key2])); + } + + [TestMethod] + public void TestCacheOverrideIssue2572() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var storages = snapshotCache.CloneCache(); + + storages.Add + ( + new StorageKey() { Key = new byte[] { 0x00, 0x01 }, Id = 0 }, + new StorageItem() { Value = Array.Empty() } + ); + storages.Add + ( + new StorageKey() { Key = new byte[] { 0x01, 0x01 }, Id = 0 }, + new StorageItem() { Value = new byte[] { 0x05 } } + ); + + storages.Commit(); + + var item = storages.GetAndChange(new StorageKey() { Key = new byte[] { 0x01, 0x01 }, Id = 0 })!; + item.Value = new byte[] { 0x06 }; + + var res = snapshotCache.TryGet(new StorageKey() { Key = new byte[] { 0x01, 0x01 }, Id = 0 })!; + Assert.AreEqual("05", res.Value.Span.ToHexString()); + storages.Commit(); + res = snapshotCache.TryGet(new StorageKey() { Key = new byte[] { 0x01, 0x01 }, Id = 0 })!; + Assert.AreEqual("06", res.Value.Span.ToHexString()); + } +} diff --git a/tests/Neo.UnitTests/Persistence/UT_DataCache.cs b/tests/Neo.UnitTests/Persistence/UT_DataCache.cs new file mode 100644 index 0000000000..2a05d2fdf8 --- /dev/null +++ b/tests/Neo.UnitTests/Persistence/UT_DataCache.cs @@ -0,0 +1,425 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_DataCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Persistence; +using Neo.Persistence.Providers; +using Neo.SmartContract; +using System.Text; + +namespace Neo.UnitTests.Persistence; + +[TestClass] +public class UT_DataCache +{ + private readonly MemoryStore _store = new(); + private StoreCache _myDataCache = null!; + + private static readonly StorageKey s_key1 = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key1") }; + private static readonly StorageKey s_key2 = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key2") }; + private static readonly StorageKey s_key3 = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key3") }; + private static readonly StorageKey s_key4 = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key4") }; + private static readonly StorageKey s_key5 = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key5") }; + + private static readonly StorageItem s_value1 = new(Encoding.UTF8.GetBytes("value1")); + private static readonly StorageItem s_value2 = new(Encoding.UTF8.GetBytes("value2")); + private static readonly StorageItem s_value3 = new(Encoding.UTF8.GetBytes("value3")); + private static readonly StorageItem s_value4 = new(Encoding.UTF8.GetBytes("value4")); + private static readonly StorageItem s_value5 = new(Encoding.UTF8.GetBytes("value5")); + + [TestInitialize] + public void Initialize() + { + _myDataCache = new(_store, false); + } + + [TestMethod] + public void TestAccessByKey() + { + _myDataCache.Add(s_key1, s_value1); + _myDataCache.Add(s_key2, s_value2); + + Assert.IsTrue(_myDataCache[s_key1].EqualsTo(s_value1)); + + // case 2 read from inner + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + Assert.IsTrue(_myDataCache[s_key3].EqualsTo(s_value3)); + } + + [TestMethod] + public void TestAccessByNotFoundKey() + { + Assert.ThrowsExactly(() => + { + _ = _myDataCache[s_key1]; + }); + } + + [TestMethod] + public void TestAccessByDeletedKey() + { + _store.Put(s_key1.ToArray(), s_value1.ToArray()); + _myDataCache.Delete(s_key1); + + Assert.ThrowsExactly(() => + { + _ = _myDataCache[s_key1]; + }); + } + + [TestMethod] + public void TestAdd() + { + var read = 0; + var updated = 0; + _myDataCache.OnRead += (sender, key, value) => { read++; }; + _myDataCache.OnUpdate += (sender, key, value) => { updated++; }; + _myDataCache.Add(s_key1, s_value1); + + Assert.AreEqual(s_value1, _myDataCache[s_key1]); + Assert.AreEqual(0, read); + Assert.AreEqual(0, updated); + + Action action = () => _myDataCache.Add(s_key1, s_value1); + Assert.ThrowsExactly(action); + + _store.Put(s_key2.ToArray(), s_value2.ToArray()); + _myDataCache.Delete(s_key2); + Assert.AreEqual(TrackState.Deleted, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key2)).Select(u => u.Value.State).FirstOrDefault()); + _myDataCache.Add(s_key2, s_value2); + Assert.AreEqual(TrackState.Changed, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key2)).Select(u => u.Value.State).FirstOrDefault()); + + action = () => _myDataCache.Add(s_key2, s_value2); + Assert.ThrowsExactly(action); + } + + [TestMethod] + public void TestCommit() + { + using var store = new MemoryStore(); + store.Put(s_key2.ToArray(), s_value2.ToArray()); + store.Put(s_key3.ToArray(), s_value3.ToArray()); + + using var snapshot = store.GetSnapshot(); + using var myDataCache = new StoreCache(snapshot); + + var read = 0; + var updated = 0; + myDataCache.OnRead += (sender, key, value) => { read++; }; + myDataCache.OnUpdate += (sender, key, value) => { updated++; }; + + myDataCache.Add(s_key1, s_value1); + Assert.AreEqual(TrackState.Added, myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key1)).Select(u => u.Value.State).FirstOrDefault()); + Assert.AreEqual(0, read); + Assert.AreEqual(0, updated); + + myDataCache.Delete(s_key2); + + Assert.AreEqual(TrackState.Deleted, myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key2)).Select(u => u.Value.State).FirstOrDefault()); + Assert.AreEqual(1, read); + Assert.AreEqual(0, updated); + Assert.AreEqual(TrackState.None, myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key3)).Select(u => u.Value.State).FirstOrDefault()); + + myDataCache.Delete(s_key3); + + Assert.AreEqual(2, read); + Assert.AreEqual(0, updated); + Assert.AreEqual(TrackState.Deleted, myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key3)).Select(u => u.Value.State).FirstOrDefault()); + + myDataCache.Add(s_key3, s_value4); + Assert.AreEqual(TrackState.Changed, myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key3)).Select(u => u.Value.State).FirstOrDefault()); + Assert.AreEqual(2, read); + Assert.AreEqual(0, updated); + + // If we use myDataCache after it is committed, it will return wrong result. + myDataCache.Commit(); + + Assert.AreEqual(0, myDataCache.GetChangeSet().Count()); + Assert.AreEqual(2, read); + Assert.AreEqual(1, updated); + Assert.IsTrue(store.TryGet(s_key1.ToArray()).SequenceEqual(s_value1.ToArray())); + Assert.IsNull(store.TryGet(s_key2.ToArray())); + Assert.IsTrue(store.TryGet(s_key3.ToArray()).SequenceEqual(s_value4.ToArray())); + + Assert.IsTrue(myDataCache.TryGet(s_key1)!.Value.ToArray().SequenceEqual(s_value1.ToArray())); + // Though value is deleted from the store, the value can still be gotten from the snapshot cache. + Assert.IsTrue(myDataCache.TryGet(s_key2)!.Value.ToArray().SequenceEqual(s_value2.ToArray())); + Assert.IsTrue(myDataCache.TryGet(s_key3)!.Value.ToArray().SequenceEqual(s_value4.ToArray())); + } + + [TestMethod] + public void TestCreateSnapshot() + { + Assert.IsNotNull(_myDataCache.CloneCache()); + } + + [TestMethod] + public void TestDelete() + { + using var store = new MemoryStore(); + store.Put(s_key2.ToArray(), s_value2.ToArray()); + + using var snapshot = store.GetSnapshot(); + using var myDataCache = new StoreCache(snapshot); + + myDataCache.Add(s_key1, s_value1); + myDataCache.Delete(s_key1); + Assert.IsNull(store.TryGet(s_key1.ToArray())); + + myDataCache.Delete(s_key2); + myDataCache.Commit(); + Assert.IsNull(store.TryGet(s_key2.ToArray())); + } + + [TestMethod] + public void TestFind() + { + _myDataCache.Add(s_key1, s_value1); + _myDataCache.Add(s_key2, s_value2); + + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + _store.Put(s_key4.ToArray(), s_value4.ToArray()); + + var k1 = s_key1.ToArray(); + var items = _myDataCache.Find(k1); + Assert.AreEqual(s_key1, items.ElementAt(0).Key); + Assert.AreEqual(s_value1, items.ElementAt(0).Value); + Assert.AreEqual(1, items.Count()); + + // null and empty with the forward direction -> finds everything. + items = _myDataCache.Find(null); + Assert.AreEqual(4, items.Count()); + items = _myDataCache.Find([]); + Assert.AreEqual(4, items.Count()); + + // null and empty with the backwards direction -> miserably fails. + Action action = () => _myDataCache.Find(null, SeekDirection.Backward); + Assert.ThrowsExactly(action); + action = () => _myDataCache.Find([], SeekDirection.Backward); + Assert.ThrowsExactly(action); + + items = _myDataCache.Find(k1, SeekDirection.Backward); + Assert.AreEqual(s_key1, items.ElementAt(0).Key); + Assert.AreEqual(s_value1, items.ElementAt(0).Value); + Assert.AreEqual(1, items.Count()); + + var prefix = k1.Take(k1.Length - 1).ToArray(); // Just the "key" part to match everything. + items = _myDataCache.Find(prefix); + Assert.AreEqual(4, items.Count()); + Assert.AreEqual(s_key1, items.ElementAt(0).Key); + Assert.AreEqual(s_value1, items.ElementAt(0).Value); + Assert.AreEqual(s_key2, items.ElementAt(1).Key); + Assert.AreEqual(s_value2, items.ElementAt(1).Value); + Assert.AreEqual(s_key3, items.ElementAt(2).Key); + Assert.IsTrue(items.ElementAt(2).Value.EqualsTo(s_value3)); + Assert.AreEqual(s_key4, items.ElementAt(3).Key); + Assert.IsTrue(items.ElementAt(3).Value.EqualsTo(s_value4)); + + items = _myDataCache.Find(prefix, SeekDirection.Backward); + Assert.AreEqual(4, items.Count()); + Assert.AreEqual(s_key4, items.ElementAt(0).Key); + Assert.IsTrue(items.ElementAt(0).Value.EqualsTo(s_value4)); + Assert.AreEqual(s_key3, items.ElementAt(1).Key); + Assert.IsTrue(items.ElementAt(1).Value.EqualsTo(s_value3)); + Assert.AreEqual(s_key2, items.ElementAt(2).Key); + Assert.AreEqual(s_value2, items.ElementAt(2).Value); + Assert.AreEqual(s_key1, items.ElementAt(3).Key); + Assert.AreEqual(s_value1, items.ElementAt(3).Value); + + items = _myDataCache.Find(s_key5); + Assert.AreEqual(0, items.Count()); + } + + [TestMethod] + public void TestSeek() + { + _myDataCache.Add(s_key1, s_value1); + _myDataCache.Add(s_key2, s_value2); + + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + _store.Put(s_key4.ToArray(), s_value4.ToArray()); + + var items = _myDataCache.Seek(s_key3.ToArray(), SeekDirection.Backward).ToArray(); + Assert.AreEqual(s_key3, items[0].Key); + Assert.IsTrue(items[0].Value.EqualsTo(s_value3)); + Assert.AreEqual(s_key2, items[1].Key); + Assert.IsTrue(items[1].Value.EqualsTo(s_value2)); + Assert.HasCount(3, items); + + items = [.. _myDataCache.Seek(s_key5.ToArray(), SeekDirection.Forward)]; + Assert.IsEmpty(items); + } + + [TestMethod] + public void TestFindRange() + { + var store = new MemoryStore(); + store.Put(s_key3.ToArray(), s_value3.ToArray()); + store.Put(s_key4.ToArray(), s_value4.ToArray()); + + var myDataCache = new StoreCache(store); + myDataCache.Add(s_key1, s_value1); + myDataCache.Add(s_key2, s_value2); + + var items = myDataCache.FindRange(s_key3.ToArray(), s_key5.ToArray()).ToArray(); + Assert.AreEqual(s_key3, items[0].Key); + Assert.IsTrue(items[0].Value.EqualsTo(s_value3)); + Assert.AreEqual(s_key4, items[1].Key); + Assert.IsTrue(items[1].Value.EqualsTo(s_value4)); + Assert.HasCount(2, items); + + // case 2 Need to sort the cache of myDataCache + + store = new(); + store.Put(s_key4.ToArray(), s_value4.ToArray()); + store.Put(s_key3.ToArray(), s_value3.ToArray()); + + myDataCache = new(store); + myDataCache.Add(s_key1, s_value1); + myDataCache.Add(s_key2, s_value2); + + items = [.. myDataCache.FindRange(s_key3.ToArray(), s_key5.ToArray())]; + Assert.AreEqual(s_key3, items[0].Key); + Assert.IsTrue(items[0].Value.EqualsTo(s_value3)); + Assert.AreEqual(s_key4, items[1].Key); + Assert.IsTrue(items[1].Value.EqualsTo(s_value4)); + Assert.HasCount(2, items); + + // case 3 FindRange by Backward + + store = new(); + store.Put(s_key4.ToArray(), s_value4.ToArray()); + store.Put(s_key3.ToArray(), s_value3.ToArray()); + store.Put(s_key5.ToArray(), s_value5.ToArray()); + + myDataCache = new(store); + myDataCache.Add(s_key1, s_value1); + myDataCache.Add(s_key2, s_value2); + + items = [.. myDataCache.FindRange(s_key5.ToArray(), s_key3.ToArray(), SeekDirection.Backward)]; + Assert.AreEqual(s_key5, items[0].Key); + Assert.IsTrue(items[0].Value.EqualsTo(s_value5)); + Assert.AreEqual(s_key4, items[1].Key); + Assert.IsTrue(items[1].Value.EqualsTo(s_value4)); + Assert.HasCount(2, items); + } + + [TestMethod] + public void TestGetChangeSet() + { + _myDataCache.Add(s_key1, s_value1); + Assert.AreEqual(TrackState.Added, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key1)).Select(u => u.Value.State).FirstOrDefault()); + _myDataCache.Add(s_key2, s_value2); + Assert.AreEqual(TrackState.Added, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key2)).Select(u => u.Value.State).FirstOrDefault()); + + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + _store.Put(s_key4.ToArray(), s_value4.ToArray()); + _myDataCache.Delete(s_key3); + Assert.AreEqual(TrackState.Deleted, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key3)).Select(u => u.Value.State).FirstOrDefault()); + _myDataCache.Delete(s_key4); + Assert.AreEqual(TrackState.Deleted, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key4)).Select(u => u.Value.State).FirstOrDefault()); + + var items = _myDataCache.GetChangeSet(); + var i = 0; + foreach (var item in items) + { + i++; + StorageKey key = new() { Id = 0, Key = Encoding.UTF8.GetBytes("key" + i) }; + StorageItem value = new(Encoding.UTF8.GetBytes("value" + i)); + Assert.AreEqual(key, item.Key); + Assert.IsTrue(value.EqualsTo(item.Value.Item)); + } + Assert.AreEqual(4, i); + } + + [TestMethod] + public void TestGetAndChange() + { + _myDataCache.Add(s_key1, s_value1); + Assert.AreEqual(TrackState.Added, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key1)).Select(u => u.Value.State).FirstOrDefault()); + _store.Put(s_key2.ToArray(), s_value2.ToArray()); + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + _myDataCache.Delete(s_key3); + Assert.AreEqual(TrackState.Deleted, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key3)).Select(u => u.Value.State).FirstOrDefault()); + + StorageItem value_bk_1 = new(Encoding.UTF8.GetBytes("value_bk_1")); + StorageItem value_bk_2 = new(Encoding.UTF8.GetBytes("value_bk_2")); + StorageItem value_bk_3 = new(Encoding.UTF8.GetBytes("value_bk_3")); + StorageItem value_bk_4 = new(Encoding.UTF8.GetBytes("value_bk_4")); + + Assert.IsTrue(_myDataCache.GetAndChange(s_key1, () => value_bk_1).EqualsTo(s_value1)); + Assert.IsTrue(_myDataCache.GetAndChange(s_key2, () => value_bk_2).EqualsTo(s_value2)); + Assert.IsTrue(_myDataCache.GetAndChange(s_key3, () => value_bk_3).EqualsTo(value_bk_3)); + Assert.IsTrue(_myDataCache.GetAndChange(s_key4, () => value_bk_4).EqualsTo(value_bk_4)); + } + + [TestMethod] + public void TestGetOrAdd() + { + _myDataCache.Add(s_key1, s_value1); + Assert.AreEqual(TrackState.Added, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key1)).Select(u => u.Value.State).FirstOrDefault()); + _store.Put(s_key2.ToArray(), s_value2.ToArray()); + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + _myDataCache.Delete(s_key3); + Assert.AreEqual(TrackState.Deleted, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key3)).Select(u => u.Value.State).FirstOrDefault()); + + StorageItem value_bk_1 = new(Encoding.UTF8.GetBytes("value_bk_1")); + StorageItem value_bk_2 = new(Encoding.UTF8.GetBytes("value_bk_2")); + StorageItem value_bk_3 = new(Encoding.UTF8.GetBytes("value_bk_3")); + StorageItem value_bk_4 = new(Encoding.UTF8.GetBytes("value_bk_4")); + + Assert.IsTrue(_myDataCache.GetOrAdd(s_key1, () => value_bk_1).EqualsTo(s_value1)); + Assert.IsTrue(_myDataCache.GetOrAdd(s_key2, () => value_bk_2).EqualsTo(s_value2)); + Assert.IsTrue(_myDataCache.GetOrAdd(s_key3, () => value_bk_3).EqualsTo(value_bk_3)); + Assert.IsTrue(_myDataCache.GetOrAdd(s_key4, () => value_bk_4).EqualsTo(value_bk_4)); + } + + [TestMethod] + public void TestTryGet() + { + _myDataCache.Add(s_key1, s_value1); + Assert.AreEqual(TrackState.Added, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key1)).Select(u => u.Value.State).FirstOrDefault()); + _store.Put(s_key2.ToArray(), s_value2.ToArray()); + _store.Put(s_key3.ToArray(), s_value3.ToArray()); + _myDataCache.Delete(s_key3); + Assert.AreEqual(TrackState.Deleted, _myDataCache.GetChangeSet().Where(u => u.Key.Equals(s_key3)).Select(u => u.Value.State).FirstOrDefault()); + + Assert.IsTrue(_myDataCache.TryGet(s_key1)!.EqualsTo(s_value1)); + Assert.IsTrue(_myDataCache.TryGet(s_key2)!.EqualsTo(s_value2)); + Assert.IsNull(_myDataCache.TryGet(s_key3)); + } + + [TestMethod] + public void TestFindInvalid() + { + using var store = new MemoryStore(); + using var myDataCache = new StoreCache(store); + myDataCache.Add(s_key1, s_value1); + + store.Put(s_key2.ToArray(), s_value2.ToArray()); + store.Put(s_key3.ToArray(), s_value3.ToArray()); + store.Put(s_key4.ToArray(), s_value3.ToArray()); + + var items = myDataCache.Find(SeekDirection.Forward).GetEnumerator(); + items.MoveNext(); + Assert.AreEqual(s_key1, items.Current.Key); + + myDataCache.TryGet(s_key3); // GETLINE + + items.MoveNext(); + Assert.AreEqual(s_key2, items.Current.Key); + items.MoveNext(); + Assert.AreEqual(s_key3, items.Current.Key); + items.MoveNext(); + Assert.AreEqual(s_key4, items.Current.Key); + Assert.IsFalse(items.MoveNext()); + } +} diff --git a/tests/Neo.UnitTests/Persistence/UT_MemoryClonedCache.cs b/tests/Neo.UnitTests/Persistence/UT_MemoryClonedCache.cs new file mode 100644 index 0000000000..babf17fa42 --- /dev/null +++ b/tests/Neo.UnitTests/Persistence/UT_MemoryClonedCache.cs @@ -0,0 +1,127 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MemoryClonedCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.Persistence.Providers; +using Neo.SmartContract; + +namespace Neo.UnitTests.Persistence; + +/// +/// When adding data to `datacache` , +/// it gets passed to `snapshotcache` during commit. +/// If `snapshotcache` commits, the data is then passed +/// to the underlying store . +/// However, because snapshots are immutable, the new data +/// cannot be retrieved from the snapshot . +/// +/// When deleting data from `datacache` , +/// it won't exist in `datacache` upon commit, and therefore will be removed from `snapshotcache` . +/// Upon `snapshotcache` commit, the data is deleted from the store . +/// However, since the snapshot remains unchanged, the data still exists in the snapshot. +/// If you attempt to read this data from `datacache` or `snapshotcache` , +/// which do not have the data, they will retrieve it from the snapshot instead of the store. +/// Thus, they can still access data that has been deleted. +/// +[TestClass] +public class UT_MemoryClonedCache +{ + private MemoryStore _memoryStore = null!; + private MemorySnapshot _snapshot = null!; + private StoreCache _snapshotCache = null!; + private DataCache _dataCache = null!; + + [TestInitialize] + public void Setup() + { + _memoryStore = new MemoryStore(); + _snapshot = (MemorySnapshot)_memoryStore.GetSnapshot(); + _snapshotCache = new StoreCache(_snapshot); + _dataCache = _snapshotCache.CloneCache(); + } + + [TestCleanup] + public void CleanUp() + { + _dataCache.Commit(); + _snapshotCache.Commit(); + _memoryStore.Reset(); + } + + [TestMethod] + public void SingleSnapshotCacheTest() + { + var key1 = new KeyBuilder(0, 1); + var value1 = new StorageItem([0x03, 0x04]); + + Assert.IsFalse(_dataCache.Contains(key1)); + _dataCache.Add(key1, value1); + + Assert.IsTrue(_dataCache.Contains(key1)); + Assert.IsFalse(_snapshotCache.Contains(key1)); + Assert.IsFalse(_snapshot.Contains(key1.ToArray())); + Assert.IsFalse(_memoryStore.Contains(key1.ToArray())); + + // After the data cache is committed, it should be dropped + // so its value after the commit is meaningless and should not be used. + _dataCache.Commit(); + + Assert.IsTrue(_dataCache.Contains(key1)); + Assert.IsTrue(_snapshotCache.Contains(key1)); + Assert.IsFalse(_snapshot.Contains(key1.ToArray())); + Assert.IsFalse(_memoryStore.Contains(key1.ToArray())); + + // After the snapshot is committed, it should be dropped + // so its value after the commit is meaningless and should not be used. + _snapshotCache.Commit(); + + Assert.IsTrue(_dataCache.Contains(key1)); + Assert.IsTrue(_snapshotCache.Contains(key1)); + Assert.IsFalse(_snapshot.Contains(key1.ToArray())); + Assert.IsTrue(_memoryStore.Contains(key1.ToArray())); + + // Test delete + + // Reset the snapshot to make it accessible to the new value. + _snapshot = (MemorySnapshot)_memoryStore.GetSnapshot(); + _snapshotCache = new StoreCache(_snapshot); + _dataCache = _snapshotCache.CloneCache(); + + Assert.IsTrue(_dataCache.Contains(key1)); + _dataCache.Delete(key1); + + Assert.IsFalse(_dataCache.Contains(key1)); + Assert.IsTrue(_snapshotCache.Contains(key1)); + Assert.IsTrue(_snapshot.Contains(key1.ToArray())); + Assert.IsTrue(_memoryStore.Contains(key1.ToArray())); + + // After the data cache is committed, it should be dropped + // so its value after the commit is meaningless and should not be used. + _dataCache.Commit(); + + Assert.IsFalse(_dataCache.Contains(key1)); + Assert.IsFalse(_snapshotCache.Contains(key1)); + Assert.IsTrue(_snapshot.Contains(key1.ToArray())); + Assert.IsTrue(_memoryStore.Contains(key1.ToArray())); + + + // After the snapshot cache is committed, it should be dropped + // so its value after the commit is meaningless and should not be used. + _snapshotCache.Commit(); + + // The reason that datacache, snapshotcache still contains key1 is because + // they can not find the value from its cache, so they fetch it from the snapshot of the store. + Assert.IsTrue(_dataCache.Contains(key1)); + Assert.IsTrue(_snapshotCache.Contains(key1)); + Assert.IsTrue(_snapshot.Contains(key1.ToArray())); + Assert.IsFalse(_memoryStore.Contains(key1.ToArray())); + } +} diff --git a/tests/Neo.UnitTests/Persistence/UT_MemorySnapshot.cs b/tests/Neo.UnitTests/Persistence/UT_MemorySnapshot.cs new file mode 100644 index 0000000000..9e706ebff0 --- /dev/null +++ b/tests/Neo.UnitTests/Persistence/UT_MemorySnapshot.cs @@ -0,0 +1,174 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MemorySnapshot.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence.Providers; + +namespace Neo.UnitTests.Persistence; + +[TestClass] +public class UT_MemorySnapshot +{ + private MemoryStore _memoryStore = null!; + private MemorySnapshot _snapshot = null!; + + [TestInitialize] + public void Setup() + { + _memoryStore = new MemoryStore(); + _snapshot = (MemorySnapshot)_memoryStore.GetSnapshot(); + } + + [TestCleanup] + public void CleanUp() + { + _memoryStore.Reset(); + } + + [TestMethod] + public void TestDobleCommit() + { + var key1 = new byte[] { 0x05, 0x02 }; + var value1 = new byte[] { 0x06, 0x04 }; + + var snapshot = (MemorySnapshot)_memoryStore.GetSnapshot(); + Assert.AreEqual(0, snapshot.WriteBatchLength); + + snapshot.Put(key1, value1); + Assert.AreEqual(1, snapshot.WriteBatchLength); + + snapshot.Delete(key1); + Assert.AreEqual(1, snapshot.WriteBatchLength); + snapshot.Commit(); + + Assert.AreEqual(0, snapshot.WriteBatchLength); + } + + [TestMethod] + public void TestDobleCommitTwo() + { + var key1 = new byte[] { 0x51, 0x02 }; + var value1 = new byte[] { 0x06, 0x04 }; + + var snapshotA = (MemorySnapshot)_memoryStore.GetSnapshot(); + var snapshotB = (MemorySnapshot)_memoryStore.GetSnapshot(); + + Assert.IsFalse(_memoryStore.Contains(key1)); + snapshotA.Put(key1, value1); + snapshotA.Commit(); + Assert.IsTrue(_memoryStore.Contains(key1)); + + snapshotB.Delete(key1); + snapshotB.Commit(); + Assert.IsFalse(_memoryStore.Contains(key1)); + + snapshotA.Put(key1, value1); + snapshotA.Commit(); + Assert.IsTrue(_memoryStore.Contains(key1)); + + snapshotB.Commit(); // Already committed + Assert.IsTrue(_memoryStore.Contains(key1)); // It fails before #3953 + } + + [TestMethod] + public void SingleSnapshotTest() + { + var key1 = new byte[] { 0x01, 0x02 }; + var value1 = new byte[] { 0x03, 0x04 }; + + _snapshot.Delete(key1); + Assert.IsNull(_snapshot.TryGet(key1)); + + // Both Store and Snapshot can not get the value that are cached in the snapshot + _snapshot.Put(key1, value1); + Assert.IsNull(_snapshot.TryGet(key1)); + Assert.IsNull(_memoryStore.TryGet(key1)); + + _snapshot.Commit(); + + // After commit the snapshot, the value can be get from the store but still can not get from the snapshot + CollectionAssert.AreEqual(value1, _memoryStore.TryGet(key1)); + Assert.IsNull(_snapshot.TryGet(key1)); + + _snapshot.Delete(key1); + + // Deleted value can not be found from the snapshot but can still get from the store + // This is because snapshot has no key1 at all. + Assert.IsFalse(_snapshot.Contains(key1)); + Assert.IsTrue(_memoryStore.Contains(key1)); + + _snapshot.Commit(); + + // After commit the snapshot, the value can not be found from the store + Assert.IsFalse(_memoryStore.Contains(key1)); + + // Test seek in order + _snapshot.Put([0x00, 0x00, 0x04], [0x04]); + _snapshot.Put([0x00, 0x00, 0x00], [0x00]); + _snapshot.Put([0x00, 0x00, 0x01], [0x01]); + _snapshot.Put([0x00, 0x00, 0x02], [0x02]); + _snapshot.Put([0x00, 0x00, 0x03], [0x03]); + + // Can not get anything from the snapshot + var entries = _snapshot.Find([0x00, 0x00, 0x02]).ToArray(); + Assert.IsEmpty(entries); + } + + [TestMethod] + public void MultiSnapshotTest() + { + var key1 = new byte[] { 0x01, 0x02 }; + var value1 = new byte[] { 0x03, 0x04 }; + + _snapshot.Delete(key1); + Assert.IsNull(_snapshot.TryGet(key1)); + + // Both Store and Snapshot can not get the value that are cached in the snapshot + _snapshot.Put(key1, value1); + // After commit the snapshot, the value can be get from the store but still can not get from the snapshot + // But can get the value from a new snapshot + _snapshot.Commit(); + var snapshot2 = _memoryStore.GetSnapshot(); + CollectionAssert.AreEqual(value1, _memoryStore.TryGet(key1)); + Assert.IsNull(_snapshot.TryGet(key1)); + Assert.IsTrue(snapshot2.TryGet(key1, out var result)); + CollectionAssert.AreEqual(value1, result); + + Assert.IsFalse(_snapshot.TryGet(key1, out _)); + + Assert.IsTrue(snapshot2.TryGet(key1, out var value2)); + CollectionAssert.AreEqual(value1, value2); + + Assert.IsTrue(_memoryStore.TryGet(key1, out value2)); + CollectionAssert.AreEqual(value1, value2); + + _snapshot.Delete(key1); + + // Deleted value can not being found from the snapshot but can still get from the store and snapshot2 + Assert.IsFalse(_snapshot.Contains(key1)); + Assert.IsTrue(_memoryStore.Contains(key1)); + Assert.IsTrue(snapshot2.Contains(key1)); + + _snapshot.Commit(); + + // After commit the snapshot, the value can not be found from the store, but can be found in snapshots + // Cause snapshot1 or store can not change the status of snapshot2. + Assert.IsFalse(_memoryStore.Contains(key1)); + Assert.IsTrue(snapshot2.Contains(key1)); + Assert.IsFalse(_snapshot.Contains(key1)); + + // Add value via snapshot2 will not affect snapshot1 at all + snapshot2.Put(key1, value1); + snapshot2.Commit(); + Assert.IsNull(_snapshot.TryGet(key1)); + Assert.IsTrue(snapshot2.TryGet(key1, out result)); + CollectionAssert.AreEqual(value1, result); + } +} diff --git a/tests/Neo.UnitTests/Persistence/UT_MemorySnapshotCache.cs b/tests/Neo.UnitTests/Persistence/UT_MemorySnapshotCache.cs new file mode 100644 index 0000000000..0f4198e3a6 --- /dev/null +++ b/tests/Neo.UnitTests/Persistence/UT_MemorySnapshotCache.cs @@ -0,0 +1,133 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MemorySnapshotCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.Persistence.Providers; +using Neo.SmartContract; + +namespace Neo.UnitTests.Persistence; + +[TestClass] +public class UT_MemorySnapshotCache +{ + private MemoryStore _memoryStore = null!; + private MemorySnapshot _snapshot = null!; + private StoreCache _snapshotCache = null!; + + [TestInitialize] + public void Setup() + { + _memoryStore = new MemoryStore(); + _snapshot = (MemorySnapshot)_memoryStore.GetSnapshot(); + _snapshotCache = new StoreCache(_snapshot); + } + + [TestCleanup] + public void CleanUp() + { + _snapshotCache.Commit(); + _memoryStore.Reset(); + } + + [TestMethod] + public void SingleSnapshotCacheTest() + { + var key1 = new KeyBuilder(0, 1); + var value1 = new StorageItem([0x03, 0x04]); + + _snapshotCache.Delete(key1); + Assert.IsNull(_snapshotCache.TryGet(key1)); + + // Adding value to the snapshot cache will not affect the snapshot or the store + // But the snapshot cache itself can see the added item right after it is added. + _snapshotCache.Add(key1, value1); + + Assert.AreEqual(value1.Value, _snapshotCache.TryGet(key1)!.Value); + Assert.IsNull(_snapshot.TryGet(key1.ToArray())); + Assert.IsNull(_memoryStore.TryGet(key1.ToArray())); + + // After commit the snapshot cache, it works the same as commit the snapshot. + // the value can be get from the snapshot cache and store but still can not get from the snapshot + _snapshotCache.Commit(); + + Assert.AreEqual(value1.Value, _snapshotCache.TryGet(key1)!.Value); + Assert.IsFalse(_snapshot.Contains(key1.ToArray())); + Assert.IsTrue(_memoryStore.Contains(key1.ToArray())); + + // Test delete + + // Reset the snapshot to make it accessible to the new value. + _snapshot = (MemorySnapshot)_memoryStore.GetSnapshot(); + _snapshotCache = new StoreCache(_snapshot); + + // Delete value to the snapshot cache will not affect the snapshot or the store + // But the snapshot cache itself can not see the added item. + _snapshotCache.Delete(key1); + + // Value is removed from the snapshot cache immediately + Assert.IsNull(_snapshotCache.TryGet(key1)); + // But the underline snapshot will not be changed. + Assert.IsTrue(_snapshot.Contains(key1.ToArray())); + // And the store is also not affected. + Assert.IsNotNull(_memoryStore.TryGet(key1.ToArray())); + + // commit the snapshot cache + _snapshotCache.Commit(); + + // Value is removed from both the store, but the snapshot and snapshot cache remains the same. + Assert.IsTrue(_snapshotCache.Contains(key1)); + Assert.IsTrue(_snapshot.Contains(key1.ToArray())); + Assert.IsFalse(_memoryStore.Contains(key1.ToArray())); + } + + [TestMethod] + public void MultiSnapshotCacheTest() + { + var key1 = new KeyBuilder(0, 1); + var value1 = new StorageItem([0x03, 0x04]); + + _snapshotCache.Delete(key1); + Assert.IsNull(_snapshotCache.TryGet(key1)); + + // Adding value to the snapshot cache will not affect the snapshot or the store + // But the snapshot cache itself can see the added item. + _snapshotCache.Add(key1, value1); + + // After commit the snapshot cache, it works the same as commit the snapshot. + // the value can be get from the snapshot cache but still can not get from the snapshot + _snapshotCache.Commit(); + + // Get a new snapshot cache to test if the value can be seen from the new snapshot cache + var snapshotCache2 = new StoreCache(_snapshot); + Assert.IsNull(snapshotCache2.TryGet(key1)); + Assert.IsFalse(_snapshot.Contains(key1.ToArray())); + + // Test delete + + // Reset the snapshot to make it accessible to the new value. + _snapshot = (MemorySnapshot)_memoryStore.GetSnapshot(); + _snapshotCache = new StoreCache(_snapshot); + + // Delete value to the snapshot cache will affect the snapshot + // But the snapshot and store itself can still see the item. + _snapshotCache.Delete(key1); + + // Commiting the snapshot cache will change the store, but the existing snapshot remains same. + _snapshotCache.Commit(); + + // reset the snapshotcache2 to snapshot + snapshotCache2 = new StoreCache(_snapshot); + // Value is removed from the store, but the snapshot remains the same. + // thus the snapshot cache from the snapshot will remain the same. + Assert.IsNotNull(snapshotCache2.TryGet(key1)); + Assert.IsNull(_memoryStore.TryGet(key1.ToArray())); + } +} diff --git a/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs b/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs new file mode 100644 index 0000000000..a8cd32123b --- /dev/null +++ b/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs @@ -0,0 +1,137 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MemoryStore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Persistence; +using Neo.Persistence.Providers; +using Neo.SmartContract; +using System.Text; + +namespace Neo.UnitTests.Persistence; + +[TestClass] +public class UT_MemoryStore +{ + private NeoSystem _system = null!; + private MemoryStore _memoryStore = null!; + + [TestInitialize] + public void Setup() + { + _memoryStore = new MemoryStore(); + _system = new NeoSystem(TestProtocolSettings.Default, new TestMemoryStoreProvider(_memoryStore)); + } + + [TestCleanup] + public void CleanUp() + { + _memoryStore.Reset(); + } + + [TestMethod] + public void LoadStoreTest() + { + Assert.IsInstanceOfType(_system.LoadStore("abc")); + } + + [TestMethod] + public void StoreTest() + { + using var store = new MemoryStore(); + + store.Delete([1]); + Assert.IsNull(store.TryGet([1])); + Assert.IsFalse(store.TryGet([1], out var got)); + Assert.IsNull(got); + + store.Put([1], [1, 2, 3]); + CollectionAssert.AreEqual(new byte[] { 1, 2, 3 }, store.TryGet([1])); + + store.Put([2], [4, 5, 6]); + CollectionAssert.AreEqual(new byte[] { 1 }, store.Find([]).Select(u => u.Key).First()); + CollectionAssert.AreEqual(new byte[] { 2 }, store.Find([2], SeekDirection.Backward).Select(u => u.Key).First()); + CollectionAssert.AreEqual(new byte[] { 1 }, store.Find([1], SeekDirection.Backward).Select(u => u.Key).First()); + + store.Delete([1]); + store.Delete([2]); + + store.Put([0x00, 0x00, 0x00], [0x00]); + store.Put([0x00, 0x00, 0x01], [0x01]); + store.Put([0x00, 0x00, 0x02], [0x02]); + store.Put([0x00, 0x00, 0x03], [0x03]); + store.Put([0x00, 0x00, 0x04], [0x04]); + + var entries = store.Find([], SeekDirection.Backward).ToArray(); + Assert.IsEmpty(entries); + } + + [TestMethod] + public void NeoSystemStoreViewTest() + { + Assert.IsNotNull(_system.StoreView); + var store = _system.StoreView; + var key = new StorageKey(Encoding.UTF8.GetBytes("testKey")); + var value = new StorageItem(Encoding.UTF8.GetBytes("testValue")); + + store.Add(key, value); + _ = Assert.ThrowsExactly(store.Commit); + + var result = store.TryGet(key)!; + // The StoreView is a readonly view of the store, here it will have value in the cache + Assert.AreEqual("testValue", Encoding.UTF8.GetString(result.Value.ToArray())); + + // But the value will not be written to the underlying store even its committed. + Assert.IsNull(_memoryStore.TryGet(key.ToArray())); + Assert.IsFalse(_memoryStore.TryGet(key.ToArray(), out var got)); + Assert.IsNull(got); + } + + [TestMethod] + public void NeoSystemStoreAddTest() + { + var storeCache = _system.GetSnapshotCache(); + var key = new KeyBuilder(0, 0); + storeCache.Add(key, new StorageItem(UInt256.Zero.ToArray())); + storeCache.Commit(); + + CollectionAssert.AreEqual(UInt256.Zero.ToArray(), storeCache.TryGet(key)!.ToArray()); + } + + [TestMethod] + public void NeoSystemStoreGetAndChange() + { + var storeView = _system.GetSnapshotCache(); + var key = new KeyBuilder(1, 1); + var item = new StorageItem([1, 2, 3]); + storeView.Delete(key); + Assert.IsNull(storeView.TryGet(key)); + storeView.Add(key, item); + CollectionAssert.AreEqual(new byte[] { 1, 2, 3 }, storeView.TryGet(key)!.ToArray()); + + var key2 = new KeyBuilder(1, 2); + var item2 = new StorageItem([4, 5, 6]); + storeView.Add(key2, item2); + CollectionAssert.AreEqual(key2.ToArray(), storeView.Seek(key2.ToArray(), SeekDirection.Backward).Select(u => u.Key).First().ToArray()); + CollectionAssert.AreEqual(key.ToArray(), storeView.Seek(key.ToArray(), SeekDirection.Backward).Select(u => u.Key).First().ToArray()); + + storeView.Delete(key); + storeView.Delete(key2); + + storeView.Add(new KeyBuilder(1, 0x000000), new StorageItem([0x00])); + storeView.Add(new KeyBuilder(1, 0x000001), new StorageItem([0x01])); + storeView.Add(new KeyBuilder(1, 0x000002), new StorageItem([0x02])); + storeView.Add(new KeyBuilder(1, 0x000003), new StorageItem([0x03])); + storeView.Add(new KeyBuilder(1, 0x000004), new StorageItem([0x04])); + + var entries = storeView.Seek([], SeekDirection.Backward).ToArray(); + Assert.IsEmpty(entries); + } +} diff --git a/tests/Neo.UnitTests/Plugins/TestPlugin.cs b/tests/Neo.UnitTests/Plugins/TestPlugin.cs new file mode 100644 index 0000000000..efa43b38fe --- /dev/null +++ b/tests/Neo.UnitTests/Plugins/TestPlugin.cs @@ -0,0 +1,97 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TestPlugin.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.Plugins; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.UnitTests.Plugins; + + +internal class TestPluginSettings : IPluginSettings +{ + public static TestPluginSettings? Default { get; private set; } + + public UnhandledExceptionPolicy ExceptionPolicy => UnhandledExceptionPolicy.Ignore; + + [MemberNotNull(nameof(Default))] + public static void Load(IConfigurationSection section) + { + Default = new TestPluginSettings(); + } +} +internal class TestNonPlugin +{ + public TestNonPlugin() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + } + + private static void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + throw new NotImplementedException("Test exception from OnCommitting"); + } + + private static void OnCommitted(NeoSystem system, Block block) + { + throw new NotImplementedException("Test exception from OnCommitted"); + } +} + + +internal class TestPlugin : Plugin +{ + private readonly UnhandledExceptionPolicy _exceptionPolicy; + protected internal override UnhandledExceptionPolicy ExceptionPolicy => _exceptionPolicy; + + public TestPlugin(UnhandledExceptionPolicy exceptionPolicy = UnhandledExceptionPolicy.StopPlugin) + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + _exceptionPolicy = exceptionPolicy; + } + + protected override void Configure() + { + TestPluginSettings.Load(GetConfiguration()); + } + + public void LogMessage(string message) + { + Log(message); + } + + public bool TestOnMessage(object message) + { + return OnMessage(message); + } + + public IConfigurationSection TestGetConfiguration() + { + return GetConfiguration(); + } + + protected override bool OnMessage(object message) => true; + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + throw new NotImplementedException(); + } + + private void OnCommitted(NeoSystem system, Block block) + { + throw new NotImplementedException(); + } +} diff --git a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs new file mode 100644 index 0000000000..6e46e933b3 --- /dev/null +++ b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs @@ -0,0 +1,219 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Plugin.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Plugins; +using System.Reflection; + +namespace Neo.UnitTests.Plugins; + +[TestClass] +public class UT_Plugin +{ + private static readonly Lock s_locker = new(); + + [TestInitialize] + public void TestInitialize() + { + ClearEventHandlers(); + } + + [TestCleanup] + public void TestCleanup() + { + ClearEventHandlers(); + } + + private static void ClearEventHandlers() + { + ClearEventHandler("Committing"); + ClearEventHandler("Committed"); + } + + private static void ClearEventHandler(string eventName) + { + var eventInfo = typeof(Blockchain).GetEvent(eventName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (eventInfo == null) + { + return; + } + + var fields = typeof(Blockchain).GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); + foreach (var field in fields) + { + if (field.FieldType == typeof(MulticastDelegate) || field.FieldType.BaseType == typeof(MulticastDelegate)) + { + var eventDelegate = (MulticastDelegate)field.GetValue(null)!; + if (eventDelegate != null && field.Name.Contains(eventName)) + { + foreach (var handler in eventDelegate.GetInvocationList()) + { + eventInfo.RemoveEventHandler(null, handler); + } + break; + } + } + } + } + + [TestMethod] + public void TestGetConfigFile() + { + var pp = new TestPlugin(); + var file = pp.ConfigFile; + Assert.EndsWith("config.json", file); + } + + [TestMethod] + public void TestGetName() + { + var pp = new TestPlugin(); + Assert.AreEqual("TestPlugin", pp.Name); + } + + [TestMethod] + public void TestGetVersion() + { + var pp = new TestPlugin(); + try + { + _ = pp.Version.ToString(); + } + catch (Exception ex) + { + Assert.Fail($"Should not throw but threw {ex}"); + } + } + + [TestMethod] + public void TestSendMessage() + { + lock (s_locker) + { + Plugin.Plugins.Clear(); + Assert.IsFalse(Plugin.SendMessage("hey1")); + + _ = new TestPlugin(); + Assert.IsTrue(Plugin.SendMessage("hey2")); + } + } + + [TestMethod] + public void TestGetConfiguration() + { + var pp = new TestPlugin(); + Assert.AreEqual("PluginConfiguration", pp.TestGetConfiguration().Key); + } + + [TestMethod] + public void TestOnException() + { + _ = new TestPlugin(); + // Ensure no exception is thrown + try + { + Blockchain.InvokeCommitting(null!, null!, null!, null!); + Blockchain.InvokeCommitted(null!, null!); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + // Register TestNonPlugin that throws exceptions + _ = new TestNonPlugin(); + + // Ensure exception is thrown + Assert.ThrowsExactly(() => + { + Blockchain.InvokeCommitting(null!, null!, null!, null!); + }); + + Assert.ThrowsExactly(() => + { + Blockchain.InvokeCommitted(null!, null!); + }); + } + + [TestMethod] + public void TestOnPluginStopped() + { + var pp = new TestPlugin(); + Assert.IsFalse(pp.IsStopped); + // Ensure no exception is thrown + try + { + Blockchain.InvokeCommitting(null!, null!, null!, null!); + Blockchain.InvokeCommitted(null!, null!); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.IsTrue(pp.IsStopped); + } + + [TestMethod] + public void TestOnPluginStopOnException() + { + // pp will stop on exception. + var pp = new TestPlugin(); + Assert.IsFalse(pp.IsStopped); + // Ensure no exception is thrown + try + { + Blockchain.InvokeCommitting(null!, null!, null!, null!); + Blockchain.InvokeCommitted(null!, null!); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.IsTrue(pp.IsStopped); + + // pp2 will not stop on exception. + var pp2 = new TestPlugin(UnhandledExceptionPolicy.Ignore); + Assert.IsFalse(pp2.IsStopped); + // Ensure no exception is thrown + try + { + Blockchain.InvokeCommitting(null!, null!, null!, null!); + Blockchain.InvokeCommitted(null!, null!); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.IsFalse(pp2.IsStopped); + } + + [TestMethod] + public void TestOnNodeStopOnPluginException() + { + // node will stop on pp exception. + var pp = new TestPlugin(UnhandledExceptionPolicy.StopNode); + Assert.IsFalse(pp.IsStopped); + Assert.ThrowsExactly(() => + { + Blockchain.InvokeCommitting(null!, null!, null!, null!); + }); + + Assert.ThrowsExactly(() => + { + Blockchain.InvokeCommitted(null!, null!); + }); + + Assert.IsFalse(pp.IsStopped); + } +} diff --git a/neo.UnitTests/README.md b/tests/Neo.UnitTests/README.md similarity index 66% rename from neo.UnitTests/README.md rename to tests/Neo.UnitTests/README.md index 1b8a238e9c..310a17ab1e 100644 --- a/neo.UnitTests/README.md +++ b/tests/Neo.UnitTests/README.md @@ -17,26 +17,10 @@ With .NET Core SDK installed, use the CLI to navigate to the neo.UnitTest folder Coverage ==================== -* Base - * Fixed8.cs -* Core - * AccountState.cs - * AssetState.cs - * Block.cs - Some code coverage missing on the Verify() method. - * ClaimTransaction.cs - * CoinReference.cs - * Header.cs - * Helper.cs - * InvocationTransaction.cs - * IssueTransaction.cs - * MinerTransaction.cs - * SpentCoin.cs - * SpentCoinState.cs - * StorageItem.cs - * StorageKey.cs - * TransactionAttribute.cs - * TransactionOuput.cs - * TransactionResult.cs - * UnspentCoinState.cs - * ValidatorState.cs - * Witness.cs \ No newline at end of file +* Block.cs - Some code coverage missing on the Verify() method. +* Header.cs +* Helper.cs +* StorageItem.cs +* StorageKey.cs +* Transaction.cs +* Witness.cs \ No newline at end of file diff --git a/tests/Neo.UnitTests/SmartContract/Iterators/UT_StorageIterator.cs b/tests/Neo.UnitTests/SmartContract/Iterators/UT_StorageIterator.cs new file mode 100644 index 0000000000..a58ad1c85f --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Iterators/UT_StorageIterator.cs @@ -0,0 +1,54 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_StorageIterator.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract.Iterators; +using Neo.VM.Types; + +namespace Neo.UnitTests.SmartContract.Iterators; + +[TestClass] +public class UT_StorageIterator +{ + [TestMethod] + public void TestGeneratorAndDispose() + { + StorageIterator storageIterator = new(new List<(StorageKey, StorageItem)>().GetEnumerator(), 0, FindOptions.None); + Assert.IsNotNull(storageIterator); + try + { + storageIterator.Dispose(); + } + catch + { + Assert.Fail(); + } + + } + + [TestMethod] + public void TestKeyAndValueAndNext() + { + List<(StorageKey, StorageItem)> list = new(); + StorageKey storageKey = new() + { + Key = new byte[1] + }; + StorageItem storageItem = new() + { + Value = new byte[1] + }; + list.Add((storageKey, storageItem)); + StorageIterator storageIterator = new(list.GetEnumerator(), 0, FindOptions.ValuesOnly); + storageIterator.Next(); + Assert.AreEqual(new ByteString(new byte[1]), storageIterator.Value(null)); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleContract.manifest.json b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleContract.manifest.json new file mode 100644 index 0000000000..185d683e1a --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleContract.manifest.json @@ -0,0 +1,722 @@ +{ + "name": "Test", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "a", + "parameters": [{"name": "amountIn", "type": "Integer"}], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { "name": "a0", "parameters": [], "returntype": "Integer", "offset": 77, "safe": false }, + { "name": "a10", "parameters": [], "returntype": "Void", "offset": 378, "safe": false }, + { "name": "a11", "parameters": [], "returntype": "Void", "offset": 398, "safe": false }, + { "name": "a12", "parameters": [], "returntype": "Void", "offset": 418, "safe": false }, + { "name": "a13", "parameters": [], "returntype": "Void", "offset": 438, "safe": false }, + { "name": "a14", "parameters": [], "returntype": "Void", "offset": 458, "safe": false }, + { "name": "a15", "parameters": [], "returntype": "Void", "offset": 478, "safe": false }, + { "name": "a16", "parameters": [], "returntype": "Void", "offset": 498, "safe": false }, + { "name": "a17", "parameters": [], "returntype": "Void", "offset": 518, "safe": false }, + { "name": "a18", "parameters": [], "returntype": "Void", "offset": 539, "safe": false }, + { "name": "a19", "parameters": [], "returntype": "Void", "offset": 560, "safe": false }, + { "name": "a20", "parameters": [], "returntype": "Void", "offset": 581, "safe": false }, + { "name": "a21", "parameters": [], "returntype": "Void", "offset": 602, "safe": false }, + { "name": "a22", "parameters": [], "returntype": "Void", "offset": 623, "safe": false }, + { "name": "a23", "parameters": [], "returntype": "Void", "offset": 644, "safe": false }, + { "name": "a24", "parameters": [], "returntype": "Void", "offset": 665, "safe": false }, + { "name": "a25", "parameters": [], "returntype": "Void", "offset": 686, "safe": false }, + { "name": "a26", "parameters": [], "returntype": "Void", "offset": 707, "safe": false }, + { "name": "a27", "parameters": [], "returntype": "Void", "offset": 728, "safe": false }, + { "name": "a28", "parameters": [], "returntype": "Void", "offset": 749, "safe": false }, + { "name": "a29", "parameters": [], "returntype": "Void", "offset": 770, "safe": false }, + { "name": "a30", "parameters": [], "returntype": "Void", "offset": 791, "safe": false }, + { "name": "a31", "parameters": [], "returntype": "Void", "offset": 812, "safe": false }, + { "name": "a32", "parameters": [], "returntype": "Void", "offset": 833, "safe": false }, + { "name": "a33", "parameters": [], "returntype": "Void", "offset": 854, "safe": false }, + { "name": "a34", "parameters": [], "returntype": "Void", "offset": 875, "safe": false }, + { "name": "a35", "parameters": [], "returntype": "Void", "offset": 896, "safe": false }, + { "name": "a36", "parameters": [], "returntype": "Void", "offset": 917, "safe": false }, + { "name": "a37", "parameters": [], "returntype": "Void", "offset": 938, "safe": false }, + { "name": "a38", "parameters": [], "returntype": "Void", "offset": 959, "safe": false }, + { "name": "a39", "parameters": [], "returntype": "Void", "offset": 980, "safe": false }, + { "name": "a40", "parameters": [], "returntype": "Void", "offset": 1001, "safe": false }, + { "name": "a41", "parameters": [], "returntype": "Void", "offset": 1022, "safe": false }, + { "name": "a42", "parameters": [], "returntype": "Void", "offset": 1043, "safe": false }, + { "name": "a43", "parameters": [], "returntype": "Void", "offset": 1064, "safe": false }, + { "name": "a44", "parameters": [], "returntype": "Void", "offset": 1085, "safe": false }, + { "name": "a45", "parameters": [], "returntype": "Void", "offset": 1106, "safe": false }, + { "name": "a46", "parameters": [], "returntype": "Void", "offset": 1127, "safe": false }, + { "name": "a47", "parameters": [], "returntype": "Void", "offset": 1148, "safe": false }, + { "name": "a48", "parameters": [], "returntype": "Void", "offset": 1169, "safe": false }, + { "name": "a49", "parameters": [], "returntype": "Void", "offset": 1190, "safe": false }, + { "name": "a50", "parameters": [], "returntype": "Void", "offset": 1211, "safe": false }, + { + "name": "b", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 1232, + "safe": false + }, + { "name": "b0", "parameters": [], "returntype": "Integer", "offset": 1251, "safe": false }, + { "name": "b10", "parameters": [], "returntype": "Void", "offset": 1275, "safe": false }, + { "name": "b11", "parameters": [], "returntype": "Void", "offset": 1295, "safe": false }, + { "name": "b12", "parameters": [], "returntype": "Void", "offset": 1315, "safe": false }, + { "name": "b13", "parameters": [], "returntype": "Void", "offset": 1335, "safe": false }, + { "name": "b14", "parameters": [], "returntype": "Void", "offset": 1355, "safe": false }, + { "name": "b15", "parameters": [], "returntype": "Void", "offset": 1375, "safe": false }, + { "name": "b16", "parameters": [], "returntype": "Void", "offset": 1395, "safe": false }, + { "name": "b17", "parameters": [], "returntype": "Void", "offset": 1415, "safe": false }, + { "name": "b18", "parameters": [], "returntype": "Void", "offset": 1436, "safe": false }, + { "name": "b19", "parameters": [], "returntype": "Void", "offset": 1457, "safe": false }, + { "name": "b20", "parameters": [], "returntype": "Void", "offset": 1478, "safe": false }, + { "name": "b21", "parameters": [], "returntype": "Void", "offset": 1499, "safe": false }, + { "name": "b22", "parameters": [], "returntype": "Void", "offset": 1520, "safe": false }, + { "name": "b23", "parameters": [], "returntype": "Void", "offset": 1541, "safe": false }, + { "name": "b24", "parameters": [], "returntype": "Void", "offset": 1562, "safe": false }, + { "name": "b25", "parameters": [], "returntype": "Void", "offset": 1583, "safe": false }, + { "name": "b26", "parameters": [], "returntype": "Void", "offset": 1604, "safe": false }, + { "name": "b27", "parameters": [], "returntype": "Void", "offset": 1625, "safe": false }, + { "name": "b28", "parameters": [], "returntype": "Void", "offset": 1646, "safe": false }, + { "name": "b29", "parameters": [], "returntype": "Void", "offset": 1667, "safe": false }, + { "name": "b30", "parameters": [], "returntype": "Void", "offset": 1688, "safe": false }, + { "name": "b31", "parameters": [], "returntype": "Void", "offset": 1709, "safe": false }, + { "name": "b32", "parameters": [], "returntype": "Void", "offset": 1730, "safe": false }, + { "name": "b33", "parameters": [], "returntype": "Void", "offset": 1751, "safe": false }, + { "name": "b34", "parameters": [], "returntype": "Void", "offset": 1772, "safe": false }, + { "name": "b35", "parameters": [], "returntype": "Void", "offset": 1793, "safe": false }, + { "name": "b36", "parameters": [], "returntype": "Void", "offset": 1814, "safe": false }, + { "name": "b37", "parameters": [], "returntype": "Void", "offset": 1835, "safe": false }, + { "name": "b38", "parameters": [], "returntype": "Void", "offset": 1856, "safe": false }, + { "name": "b39", "parameters": [], "returntype": "Void", "offset": 1877, "safe": false }, + { "name": "b40", "parameters": [], "returntype": "Void", "offset": 1898, "safe": false }, + { "name": "b41", "parameters": [], "returntype": "Void", "offset": 1919, "safe": false }, + { "name": "b42", "parameters": [], "returntype": "Void", "offset": 1940, "safe": false }, + { "name": "b43", "parameters": [], "returntype": "Void", "offset": 1961, "safe": false }, + { "name": "b44", "parameters": [], "returntype": "Void", "offset": 1982, "safe": false }, + { "name": "b45", "parameters": [], "returntype": "Void", "offset": 2003, "safe": false }, + { "name": "b46", "parameters": [], "returntype": "Void", "offset": 2024, "safe": false }, + { "name": "b47", "parameters": [], "returntype": "Void", "offset": 2045, "safe": false }, + { "name": "b48", "parameters": [], "returntype": "Void", "offset": 2066, "safe": false }, + { "name": "b49", "parameters": [], "returntype": "Void", "offset": 2087, "safe": false }, + { "name": "b50", "parameters": [], "returntype": "Void", "offset": 2108, "safe": false }, + { + "name": "c", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 2129, + "safe": false + }, + { "name": "c0", "parameters": [], "returntype": "Integer", "offset": 2148, "safe": false }, + { "name": "c10", "parameters": [], "returntype": "Void", "offset": 2172, "safe": false }, + { "name": "c11", "parameters": [], "returntype": "Void", "offset": 2192, "safe": false }, + { "name": "c12", "parameters": [], "returntype": "Void", "offset": 2212, "safe": false }, + { "name": "c13", "parameters": [], "returntype": "Void", "offset": 2232, "safe": false }, + { "name": "c14", "parameters": [], "returntype": "Void", "offset": 2252, "safe": false }, + { "name": "c15", "parameters": [], "returntype": "Void", "offset": 2272, "safe": false }, + { "name": "c16", "parameters": [], "returntype": "Void", "offset": 2292, "safe": false }, + { "name": "c17", "parameters": [], "returntype": "Void", "offset": 2312, "safe": false }, + { "name": "c18", "parameters": [], "returntype": "Void", "offset": 2333, "safe": false }, + { "name": "c19", "parameters": [], "returntype": "Void", "offset": 2354, "safe": false }, + { "name": "c20", "parameters": [], "returntype": "Void", "offset": 2375, "safe": false }, + { "name": "c21", "parameters": [], "returntype": "Void", "offset": 2396, "safe": false }, + { "name": "c22", "parameters": [], "returntype": "Void", "offset": 2417, "safe": false }, + { "name": "c23", "parameters": [], "returntype": "Void", "offset": 2438, "safe": false }, + { "name": "c24", "parameters": [], "returntype": "Void", "offset": 2459, "safe": false }, + { "name": "c25", "parameters": [], "returntype": "Void", "offset": 2480, "safe": false }, + { "name": "c26", "parameters": [], "returntype": "Void", "offset": 2501, "safe": false }, + { "name": "c27", "parameters": [], "returntype": "Void", "offset": 2522, "safe": false }, + { "name": "c28", "parameters": [], "returntype": "Void", "offset": 2543, "safe": false }, + { "name": "c29", "parameters": [], "returntype": "Void", "offset": 2564, "safe": false }, + { "name": "c30", "parameters": [], "returntype": "Void", "offset": 2585, "safe": false }, + { "name": "c31", "parameters": [], "returntype": "Void", "offset": 2606, "safe": false }, + { "name": "c32", "parameters": [], "returntype": "Void", "offset": 2627, "safe": false }, + { "name": "c33", "parameters": [], "returntype": "Void", "offset": 2648, "safe": false }, + { "name": "c34", "parameters": [], "returntype": "Void", "offset": 2669, "safe": false }, + { "name": "c35", "parameters": [], "returntype": "Void", "offset": 2690, "safe": false }, + { "name": "c36", "parameters": [], "returntype": "Void", "offset": 2711, "safe": false }, + { "name": "c37", "parameters": [], "returntype": "Void", "offset": 2732, "safe": false }, + { "name": "c38", "parameters": [], "returntype": "Void", "offset": 2753, "safe": false }, + { "name": "c39", "parameters": [], "returntype": "Void", "offset": 2774, "safe": false }, + { "name": "c40", "parameters": [], "returntype": "Void", "offset": 2795, "safe": false }, + { "name": "c41", "parameters": [], "returntype": "Void", "offset": 2816, "safe": false }, + { "name": "c42", "parameters": [], "returntype": "Void", "offset": 2837, "safe": false }, + { "name": "c43", "parameters": [], "returntype": "Void", "offset": 2858, "safe": false }, + { "name": "c44", "parameters": [], "returntype": "Void", "offset": 2879, "safe": false }, + { "name": "c45", "parameters": [], "returntype": "Void", "offset": 2900, "safe": false }, + { "name": "c46", "parameters": [], "returntype": "Void", "offset": 2921, "safe": false }, + { "name": "c47", "parameters": [], "returntype": "Void", "offset": 2942, "safe": false }, + { "name": "c48", "parameters": [], "returntype": "Void", "offset": 2963, "safe": false }, + { "name": "c49", "parameters": [], "returntype": "Void", "offset": 2984, "safe": false }, + { "name": "c50", "parameters": [], "returntype": "Void", "offset": 3005, "safe": false }, + { "name": "verify", "parameters": [], "returntype": "Boolean", "offset": 3026, "safe": false }, + { + "name": "d", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 3039, + "safe": false + }, + { "name": "d0", "parameters": [], "returntype": "Integer", "offset": 3058, "safe": false }, + { "name": "d10", "parameters": [], "returntype": "Void", "offset": 3082, "safe": false }, + { "name": "d11", "parameters": [], "returntype": "Void", "offset": 3102, "safe": false }, + { "name": "d12", "parameters": [], "returntype": "Void", "offset": 3122, "safe": false }, + { "name": "d13", "parameters": [], "returntype": "Void", "offset": 3142, "safe": false }, + { "name": "d14", "parameters": [], "returntype": "Void", "offset": 3162, "safe": false }, + { "name": "d15", "parameters": [], "returntype": "Void", "offset": 3182, "safe": false }, + { "name": "d16", "parameters": [], "returntype": "Void", "offset": 3202, "safe": false }, + { "name": "d17", "parameters": [], "returntype": "Void", "offset": 3222, "safe": false }, + { "name": "d18", "parameters": [], "returntype": "Void", "offset": 3243, "safe": false }, + { "name": "d19", "parameters": [], "returntype": "Void", "offset": 3264, "safe": false }, + { "name": "d20", "parameters": [], "returntype": "Void", "offset": 3285, "safe": false }, + { "name": "d21", "parameters": [], "returntype": "Void", "offset": 3306, "safe": false }, + { "name": "d22", "parameters": [], "returntype": "Void", "offset": 3327, "safe": false }, + { "name": "d23", "parameters": [], "returntype": "Void", "offset": 3348, "safe": false }, + { "name": "d24", "parameters": [], "returntype": "Void", "offset": 3369, "safe": false }, + { "name": "d25", "parameters": [], "returntype": "Void", "offset": 3390, "safe": false }, + { "name": "d26", "parameters": [], "returntype": "Void", "offset": 3411, "safe": false }, + { "name": "d27", "parameters": [], "returntype": "Void", "offset": 3432, "safe": false }, + { "name": "d28", "parameters": [], "returntype": "Void", "offset": 3453, "safe": false }, + { "name": "d29", "parameters": [], "returntype": "Void", "offset": 3474, "safe": false }, + { "name": "d30", "parameters": [], "returntype": "Void", "offset": 3495, "safe": false }, + { "name": "d31", "parameters": [], "returntype": "Void", "offset": 3516, "safe": false }, + { "name": "d32", "parameters": [], "returntype": "Void", "offset": 3537, "safe": false }, + { "name": "d33", "parameters": [], "returntype": "Void", "offset": 3558, "safe": false }, + { "name": "d34", "parameters": [], "returntype": "Void", "offset": 3579, "safe": false }, + { "name": "d35", "parameters": [], "returntype": "Void", "offset": 3600, "safe": false }, + { "name": "d36", "parameters": [], "returntype": "Void", "offset": 3621, "safe": false }, + { "name": "d37", "parameters": [], "returntype": "Void", "offset": 3642, "safe": false }, + { "name": "d38", "parameters": [], "returntype": "Void", "offset": 3663, "safe": false }, + { "name": "d39", "parameters": [], "returntype": "Void", "offset": 3684, "safe": false }, + { "name": "d40", "parameters": [], "returntype": "Void", "offset": 3705, "safe": false }, + { "name": "d41", "parameters": [], "returntype": "Void", "offset": 3726, "safe": false }, + { "name": "d42", "parameters": [], "returntype": "Void", "offset": 3747, "safe": false }, + { "name": "d43", "parameters": [], "returntype": "Void", "offset": 3768, "safe": false }, + { "name": "d44", "parameters": [], "returntype": "Void", "offset": 3789, "safe": false }, + { "name": "d45", "parameters": [], "returntype": "Void", "offset": 3810, "safe": false }, + { "name": "d46", "parameters": [], "returntype": "Void", "offset": 3831, "safe": false }, + { "name": "d47", "parameters": [], "returntype": "Void", "offset": 3852, "safe": false }, + { "name": "d48", "parameters": [], "returntype": "Void", "offset": 3873, "safe": false }, + { "name": "d49", "parameters": [], "returntype": "Void", "offset": 3894, "safe": false }, + { "name": "d50", "parameters": [], "returntype": "Void", "offset": 3915, "safe": false }, + { + "name": "e", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 3936, + "safe": false + }, + { "name": "e0", "parameters": [], "returntype": "Integer", "offset": 3955, "safe": false }, + { "name": "e10", "parameters": [], "returntype": "Void", "offset": 3979, "safe": false }, + { "name": "e11", "parameters": [], "returntype": "Void", "offset": 3999, "safe": false }, + { "name": "e12", "parameters": [], "returntype": "Void", "offset": 4019, "safe": false }, + { "name": "e13", "parameters": [], "returntype": "Void", "offset": 4039, "safe": false }, + { "name": "e14", "parameters": [], "returntype": "Void", "offset": 4059, "safe": false }, + { "name": "e15", "parameters": [], "returntype": "Void", "offset": 4079, "safe": false }, + { "name": "e16", "parameters": [], "returntype": "Void", "offset": 4099, "safe": false }, + { "name": "e17", "parameters": [], "returntype": "Void", "offset": 4119, "safe": false }, + { "name": "e18", "parameters": [], "returntype": "Void", "offset": 4140, "safe": false }, + { "name": "e19", "parameters": [], "returntype": "Void", "offset": 4161, "safe": false }, + { "name": "e20", "parameters": [], "returntype": "Void", "offset": 4182, "safe": false }, + { "name": "e21", "parameters": [], "returntype": "Void", "offset": 4203, "safe": false }, + { "name": "e22", "parameters": [], "returntype": "Void", "offset": 4224, "safe": false }, + { "name": "e23", "parameters": [], "returntype": "Void", "offset": 4245, "safe": false }, + { "name": "e24", "parameters": [], "returntype": "Void", "offset": 4266, "safe": false }, + { "name": "e25", "parameters": [], "returntype": "Void", "offset": 4287, "safe": false }, + { "name": "e26", "parameters": [], "returntype": "Void", "offset": 4308, "safe": false }, + { "name": "e27", "parameters": [], "returntype": "Void", "offset": 4329, "safe": false }, + { "name": "e28", "parameters": [], "returntype": "Void", "offset": 4350, "safe": false }, + { "name": "e29", "parameters": [], "returntype": "Void", "offset": 4371, "safe": false }, + { "name": "e30", "parameters": [], "returntype": "Void", "offset": 4392, "safe": false }, + { "name": "e31", "parameters": [], "returntype": "Void", "offset": 4413, "safe": false }, + { "name": "e32", "parameters": [], "returntype": "Void", "offset": 4434, "safe": false }, + { "name": "e33", "parameters": [], "returntype": "Void", "offset": 4455, "safe": false }, + { "name": "e34", "parameters": [], "returntype": "Void", "offset": 4476, "safe": false }, + { "name": "e35", "parameters": [], "returntype": "Void", "offset": 4497, "safe": false }, + { "name": "e36", "parameters": [], "returntype": "Void", "offset": 4518, "safe": false }, + { "name": "e37", "parameters": [], "returntype": "Void", "offset": 4539, "safe": false }, + { "name": "e38", "parameters": [], "returntype": "Void", "offset": 4560, "safe": false }, + { "name": "e39", "parameters": [], "returntype": "Void", "offset": 4581, "safe": false }, + { "name": "e40", "parameters": [], "returntype": "Void", "offset": 4602, "safe": false }, + { "name": "e41", "parameters": [], "returntype": "Void", "offset": 4623, "safe": false }, + { "name": "e42", "parameters": [], "returntype": "Void", "offset": 4644, "safe": false }, + { "name": "e43", "parameters": [], "returntype": "Void", "offset": 4665, "safe": false }, + { "name": "e44", "parameters": [], "returntype": "Void", "offset": 4686, "safe": false }, + { "name": "e45", "parameters": [], "returntype": "Void", "offset": 4707, "safe": false }, + { "name": "e46", "parameters": [], "returntype": "Void", "offset": 4728, "safe": false }, + { "name": "e47", "parameters": [], "returntype": "Void", "offset": 4749, "safe": false }, + { "name": "e48", "parameters": [], "returntype": "Void", "offset": 4770, "safe": false }, + { "name": "e49", "parameters": [], "returntype": "Void", "offset": 4791, "safe": false }, + { "name": "e50", "parameters": [], "returntype": "Void", "offset": 4812, "safe": false }, + { + "name": "f", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 4833, + "safe": false + }, + { "name": "f0", "parameters": [], "returntype": "Integer", "offset": 4852, "safe": false }, + { "name": "f10", "parameters": [], "returntype": "Void", "offset": 4876, "safe": false }, + { "name": "f11", "parameters": [], "returntype": "Void", "offset": 4896, "safe": false }, + { "name": "f12", "parameters": [], "returntype": "Void", "offset": 4916, "safe": false }, + { "name": "f13", "parameters": [], "returntype": "Void", "offset": 4936, "safe": false }, + { "name": "f14", "parameters": [], "returntype": "Void", "offset": 4956, "safe": false }, + { "name": "f15", "parameters": [], "returntype": "Void", "offset": 4976, "safe": false }, + { "name": "f16", "parameters": [], "returntype": "Void", "offset": 4996, "safe": false }, + { "name": "f17", "parameters": [], "returntype": "Void", "offset": 5016, "safe": false }, + { "name": "f18", "parameters": [], "returntype": "Void", "offset": 5037, "safe": false }, + { "name": "f19", "parameters": [], "returntype": "Void", "offset": 5058, "safe": false }, + { "name": "f20", "parameters": [], "returntype": "Void", "offset": 5079, "safe": false }, + { "name": "f21", "parameters": [], "returntype": "Void", "offset": 5100, "safe": false }, + { "name": "f22", "parameters": [], "returntype": "Void", "offset": 5121, "safe": false }, + { "name": "f23", "parameters": [], "returntype": "Void", "offset": 5142, "safe": false }, + { "name": "f24", "parameters": [], "returntype": "Void", "offset": 5163, "safe": false }, + { "name": "f25", "parameters": [], "returntype": "Void", "offset": 5184, "safe": false }, + { "name": "f26", "parameters": [], "returntype": "Void", "offset": 5205, "safe": false }, + { "name": "f27", "parameters": [], "returntype": "Void", "offset": 5226, "safe": false }, + { "name": "f28", "parameters": [], "returntype": "Void", "offset": 5247, "safe": false }, + { "name": "f29", "parameters": [], "returntype": "Void", "offset": 5268, "safe": false }, + { "name": "f30", "parameters": [], "returntype": "Void", "offset": 5289, "safe": false }, + { "name": "f31", "parameters": [], "returntype": "Void", "offset": 5310, "safe": false }, + { "name": "f32", "parameters": [], "returntype": "Void", "offset": 5331, "safe": false }, + { "name": "f33", "parameters": [], "returntype": "Void", "offset": 5352, "safe": false }, + { "name": "f34", "parameters": [], "returntype": "Void", "offset": 5373, "safe": false }, + { "name": "f35", "parameters": [], "returntype": "Void", "offset": 5394, "safe": false }, + { "name": "f36", "parameters": [], "returntype": "Void", "offset": 5415, "safe": false }, + { "name": "f37", "parameters": [], "returntype": "Void", "offset": 5436, "safe": false }, + { "name": "f38", "parameters": [], "returntype": "Void", "offset": 5457, "safe": false }, + { "name": "f39", "parameters": [], "returntype": "Void", "offset": 5478, "safe": false }, + { "name": "f40", "parameters": [], "returntype": "Void", "offset": 5499, "safe": false }, + { "name": "f41", "parameters": [], "returntype": "Void", "offset": 5520, "safe": false }, + { "name": "f42", "parameters": [], "returntype": "Void", "offset": 5541, "safe": false }, + { "name": "f43", "parameters": [], "returntype": "Void", "offset": 5562, "safe": false }, + { "name": "f44", "parameters": [], "returntype": "Void", "offset": 5583, "safe": false }, + { "name": "f45", "parameters": [], "returntype": "Void", "offset": 5604, "safe": false }, + { "name": "f46", "parameters": [], "returntype": "Void", "offset": 5625, "safe": false }, + { "name": "f47", "parameters": [], "returntype": "Void", "offset": 5646, "safe": false }, + { "name": "f48", "parameters": [], "returntype": "Void", "offset": 5667, "safe": false }, + { "name": "f49", "parameters": [], "returntype": "Void", "offset": 5688, "safe": false }, + { "name": "f50", "parameters": [], "returntype": "Void", "offset": 5709, "safe": false }, + { + "name": "g", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 5730, + "safe": false + }, + { "name": "g0", "parameters": [], "returntype": "Integer", "offset": 5749, "safe": false }, + { "name": "g10", "parameters": [], "returntype": "Void", "offset": 5773, "safe": false }, + { "name": "g11", "parameters": [], "returntype": "Void", "offset": 5793, "safe": false }, + { "name": "g12", "parameters": [], "returntype": "Void", "offset": 5813, "safe": false }, + { "name": "g13", "parameters": [], "returntype": "Void", "offset": 5833, "safe": false }, + { "name": "g14", "parameters": [], "returntype": "Void", "offset": 5853, "safe": false }, + { "name": "g15", "parameters": [], "returntype": "Void", "offset": 5873, "safe": false }, + { "name": "g16", "parameters": [], "returntype": "Void", "offset": 5893, "safe": false }, + { "name": "g17", "parameters": [], "returntype": "Void", "offset": 5913, "safe": false }, + { "name": "g18", "parameters": [], "returntype": "Void", "offset": 5934, "safe": false }, + { "name": "g19", "parameters": [], "returntype": "Void", "offset": 5955, "safe": false }, + { "name": "g20", "parameters": [], "returntype": "Void", "offset": 5976, "safe": false }, + { "name": "g21", "parameters": [], "returntype": "Void", "offset": 5997, "safe": false }, + { "name": "g22", "parameters": [], "returntype": "Void", "offset": 6018, "safe": false }, + { "name": "g23", "parameters": [], "returntype": "Void", "offset": 6039, "safe": false }, + { "name": "g24", "parameters": [], "returntype": "Void", "offset": 6060, "safe": false }, + { "name": "g25", "parameters": [], "returntype": "Void", "offset": 6081, "safe": false }, + { "name": "g26", "parameters": [], "returntype": "Void", "offset": 6102, "safe": false }, + { "name": "g27", "parameters": [], "returntype": "Void", "offset": 6123, "safe": false }, + { "name": "g28", "parameters": [], "returntype": "Void", "offset": 6144, "safe": false }, + { "name": "g29", "parameters": [], "returntype": "Void", "offset": 6165, "safe": false }, + { "name": "g30", "parameters": [], "returntype": "Void", "offset": 6186, "safe": false }, + { "name": "g31", "parameters": [], "returntype": "Void", "offset": 6207, "safe": false }, + { "name": "g32", "parameters": [], "returntype": "Void", "offset": 6228, "safe": false }, + { "name": "g33", "parameters": [], "returntype": "Void", "offset": 6249, "safe": false }, + { "name": "g34", "parameters": [], "returntype": "Void", "offset": 6270, "safe": false }, + { "name": "g35", "parameters": [], "returntype": "Void", "offset": 6291, "safe": false }, + { "name": "g36", "parameters": [], "returntype": "Void", "offset": 6312, "safe": false }, + { "name": "g37", "parameters": [], "returntype": "Void", "offset": 6333, "safe": false }, + { "name": "g38", "parameters": [], "returntype": "Void", "offset": 6354, "safe": false }, + { "name": "g39", "parameters": [], "returntype": "Void", "offset": 6375, "safe": false }, + { "name": "g40", "parameters": [], "returntype": "Void", "offset": 6396, "safe": false }, + { "name": "g41", "parameters": [], "returntype": "Void", "offset": 6417, "safe": false }, + { "name": "g42", "parameters": [], "returntype": "Void", "offset": 6438, "safe": false }, + { "name": "g43", "parameters": [], "returntype": "Void", "offset": 6459, "safe": false }, + { "name": "g44", "parameters": [], "returntype": "Void", "offset": 6480, "safe": false }, + { "name": "g45", "parameters": [], "returntype": "Void", "offset": 6501, "safe": false }, + { "name": "g46", "parameters": [], "returntype": "Void", "offset": 6522, "safe": false }, + { "name": "g47", "parameters": [], "returntype": "Void", "offset": 6543, "safe": false }, + { "name": "g48", "parameters": [], "returntype": "Void", "offset": 6564, "safe": false }, + { "name": "g49", "parameters": [], "returntype": "Void", "offset": 6585, "safe": false }, + { "name": "g50", "parameters": [], "returntype": "Void", "offset": 6606, "safe": false }, + { + "name": "h", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 6627, + "safe": false + }, + { "name": "h0", "parameters": [], "returntype": "Integer", "offset": 6646, "safe": false }, + { "name": "h10", "parameters": [], "returntype": "Void", "offset": 6670, "safe": false }, + { "name": "h11", "parameters": [], "returntype": "Void", "offset": 6690, "safe": false }, + { "name": "h12", "parameters": [], "returntype": "Void", "offset": 6710, "safe": false }, + { "name": "h13", "parameters": [], "returntype": "Void", "offset": 6730, "safe": false }, + { "name": "h14", "parameters": [], "returntype": "Void", "offset": 6750, "safe": false }, + { "name": "h15", "parameters": [], "returntype": "Void", "offset": 6770, "safe": false }, + { "name": "h16", "parameters": [], "returntype": "Void", "offset": 6790, "safe": false }, + { "name": "h17", "parameters": [], "returntype": "Void", "offset": 6810, "safe": false }, + { "name": "h18", "parameters": [], "returntype": "Void", "offset": 6831, "safe": false }, + { "name": "h19", "parameters": [], "returntype": "Void", "offset": 6852, "safe": false }, + { "name": "h20", "parameters": [], "returntype": "Void", "offset": 6873, "safe": false }, + { "name": "h21", "parameters": [], "returntype": "Void", "offset": 6894, "safe": false }, + { "name": "h22", "parameters": [], "returntype": "Void", "offset": 6915, "safe": false }, + { "name": "h23", "parameters": [], "returntype": "Void", "offset": 6936, "safe": false }, + { "name": "h24", "parameters": [], "returntype": "Void", "offset": 6957, "safe": false }, + { "name": "h25", "parameters": [], "returntype": "Void", "offset": 6978, "safe": false }, + { "name": "h26", "parameters": [], "returntype": "Void", "offset": 6999, "safe": false }, + { "name": "h27", "parameters": [], "returntype": "Void", "offset": 7020, "safe": false }, + { "name": "h28", "parameters": [], "returntype": "Void", "offset": 7041, "safe": false }, + { "name": "h29", "parameters": [], "returntype": "Void", "offset": 7062, "safe": false }, + { "name": "h30", "parameters": [], "returntype": "Void", "offset": 7083, "safe": false }, + { "name": "h31", "parameters": [], "returntype": "Void", "offset": 7104, "safe": false }, + { "name": "h32", "parameters": [], "returntype": "Void", "offset": 7125, "safe": false }, + { "name": "h33", "parameters": [], "returntype": "Void", "offset": 7146, "safe": false }, + { "name": "h34", "parameters": [], "returntype": "Void", "offset": 7167, "safe": false }, + { "name": "h35", "parameters": [], "returntype": "Void", "offset": 7188, "safe": false }, + { "name": "h36", "parameters": [], "returntype": "Void", "offset": 7209, "safe": false }, + { "name": "h37", "parameters": [], "returntype": "Void", "offset": 7230, "safe": false }, + { "name": "h38", "parameters": [], "returntype": "Void", "offset": 7251, "safe": false }, + { "name": "h39", "parameters": [], "returntype": "Void", "offset": 7272, "safe": false }, + { "name": "h40", "parameters": [], "returntype": "Void", "offset": 7293, "safe": false }, + { "name": "h41", "parameters": [], "returntype": "Void", "offset": 7314, "safe": false }, + { "name": "h42", "parameters": [], "returntype": "Void", "offset": 7335, "safe": false }, + { "name": "h43", "parameters": [], "returntype": "Void", "offset": 7356, "safe": false }, + { "name": "h44", "parameters": [], "returntype": "Void", "offset": 7377, "safe": false }, + { "name": "h45", "parameters": [], "returntype": "Void", "offset": 7398, "safe": false }, + { "name": "h46", "parameters": [], "returntype": "Void", "offset": 7419, "safe": false }, + { "name": "h47", "parameters": [], "returntype": "Void", "offset": 7440, "safe": false }, + { "name": "h48", "parameters": [], "returntype": "Void", "offset": 7461, "safe": false }, + { "name": "h49", "parameters": [], "returntype": "Void", "offset": 7482, "safe": false }, + { "name": "h50", "parameters": [], "returntype": "Void", "offset": 7503, "safe": false }, + { + "name": "i", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 7524, + "safe": false + }, + { "name": "i0", "parameters": [], "returntype": "Integer", "offset": 7545, "safe": false }, + { "name": "i10", "parameters": [], "returntype": "Void", "offset": 7571, "safe": false }, + { "name": "i11", "parameters": [], "returntype": "Void", "offset": 7593, "safe": false }, + { "name": "i12", "parameters": [], "returntype": "Void", "offset": 7615, "safe": false }, + { "name": "i13", "parameters": [], "returntype": "Void", "offset": 7637, "safe": false }, + { "name": "i14", "parameters": [], "returntype": "Void", "offset": 7659, "safe": false }, + { "name": "i15", "parameters": [], "returntype": "Void", "offset": 7681, "safe": false }, + { "name": "i16", "parameters": [], "returntype": "Void", "offset": 7703, "safe": false }, + { "name": "i17", "parameters": [], "returntype": "Void", "offset": 7725, "safe": false }, + { "name": "i18", "parameters": [], "returntype": "Void", "offset": 7748, "safe": false }, + { "name": "i19", "parameters": [], "returntype": "Void", "offset": 7771, "safe": false }, + { "name": "i20", "parameters": [], "returntype": "Void", "offset": 7794, "safe": false }, + { "name": "i21", "parameters": [], "returntype": "Void", "offset": 7817, "safe": false }, + { "name": "i22", "parameters": [], "returntype": "Void", "offset": 7840, "safe": false }, + { "name": "i23", "parameters": [], "returntype": "Void", "offset": 7863, "safe": false }, + { "name": "i24", "parameters": [], "returntype": "Void", "offset": 7886, "safe": false }, + { "name": "i25", "parameters": [], "returntype": "Void", "offset": 7909, "safe": false }, + { "name": "i26", "parameters": [], "returntype": "Void", "offset": 7932, "safe": false }, + { "name": "i27", "parameters": [], "returntype": "Void", "offset": 7955, "safe": false }, + { "name": "i28", "parameters": [], "returntype": "Void", "offset": 7978, "safe": false }, + { "name": "i29", "parameters": [], "returntype": "Void", "offset": 8001, "safe": false }, + { "name": "i30", "parameters": [], "returntype": "Void", "offset": 8024, "safe": false }, + { "name": "i31", "parameters": [], "returntype": "Void", "offset": 8047, "safe": false }, + { "name": "i32", "parameters": [], "returntype": "Void", "offset": 8070, "safe": false }, + { "name": "i33", "parameters": [], "returntype": "Void", "offset": 8093, "safe": false }, + { "name": "i34", "parameters": [], "returntype": "Void", "offset": 8116, "safe": false }, + { "name": "i35", "parameters": [], "returntype": "Void", "offset": 8139, "safe": false }, + { "name": "i36", "parameters": [], "returntype": "Void", "offset": 8162, "safe": false }, + { "name": "i37", "parameters": [], "returntype": "Void", "offset": 8185, "safe": false }, + { "name": "i38", "parameters": [], "returntype": "Void", "offset": 8208, "safe": false }, + { "name": "i39", "parameters": [], "returntype": "Void", "offset": 8231, "safe": false }, + { "name": "i40", "parameters": [], "returntype": "Void", "offset": 8254, "safe": false }, + { "name": "i41", "parameters": [], "returntype": "Void", "offset": 8277, "safe": false }, + { "name": "i42", "parameters": [], "returntype": "Void", "offset": 8300, "safe": false }, + { "name": "i43", "parameters": [], "returntype": "Void", "offset": 8323, "safe": false }, + { "name": "i44", "parameters": [], "returntype": "Void", "offset": 8346, "safe": false }, + { "name": "i45", "parameters": [], "returntype": "Void", "offset": 8369, "safe": false }, + { "name": "i46", "parameters": [], "returntype": "Void", "offset": 8392, "safe": false }, + { "name": "i47", "parameters": [], "returntype": "Void", "offset": 8415, "safe": false }, + { "name": "i48", "parameters": [], "returntype": "Void", "offset": 8438, "safe": false }, + { "name": "i49", "parameters": [], "returntype": "Void", "offset": 8461, "safe": false }, + { "name": "i50", "parameters": [], "returntype": "Void", "offset": 8484, "safe": false }, + { + "name": "update", + "parameters": [ + { + "name": "nefFile", + "type": "ByteArray" + }, + { + "name": "manifest", + "type": "String" + } + ], + "returntype": "Void", + "offset": 8511, + "safe": false + }, + { + "name": "j", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 8578, + "safe": false + }, + { "name": "j0", "parameters": [], "returntype": "Integer", "offset": 8599, "safe": false }, + { "name": "j10", "parameters": [], "returntype": "Void", "offset": 8625, "safe": false }, + { "name": "j11", "parameters": [], "returntype": "Void", "offset": 8647, "safe": false }, + { "name": "j12", "parameters": [], "returntype": "Void", "offset": 8669, "safe": false }, + { "name": "j13", "parameters": [], "returntype": "Void", "offset": 8691, "safe": false }, + { "name": "j14", "parameters": [], "returntype": "Void", "offset": 8713, "safe": false }, + { "name": "j15", "parameters": [], "returntype": "Void", "offset": 8735, "safe": false }, + { "name": "j16", "parameters": [], "returntype": "Void", "offset": 8757, "safe": false }, + { "name": "j17", "parameters": [], "returntype": "Void", "offset": 8779, "safe": false }, + { "name": "j18", "parameters": [], "returntype": "Void", "offset": 8802, "safe": false }, + { "name": "j19", "parameters": [], "returntype": "Void", "offset": 8825, "safe": false }, + { "name": "j20", "parameters": [], "returntype": "Void", "offset": 8848, "safe": false }, + { "name": "j21", "parameters": [], "returntype": "Void", "offset": 8871, "safe": false }, + { "name": "j22", "parameters": [], "returntype": "Void", "offset": 8894, "safe": false }, + { "name": "j23", "parameters": [], "returntype": "Void", "offset": 8917, "safe": false }, + { "name": "j24", "parameters": [], "returntype": "Void", "offset": 8940, "safe": false }, + { "name": "j25", "parameters": [], "returntype": "Void", "offset": 8963, "safe": false }, + { "name": "j26", "parameters": [], "returntype": "Void", "offset": 8986, "safe": false }, + { "name": "j27", "parameters": [], "returntype": "Void", "offset": 9009, "safe": false }, + { "name": "j28", "parameters": [], "returntype": "Void", "offset": 9032, "safe": false }, + { "name": "j29", "parameters": [], "returntype": "Void", "offset": 9055, "safe": false }, + { "name": "j30", "parameters": [], "returntype": "Void", "offset": 9078, "safe": false }, + { "name": "j31", "parameters": [], "returntype": "Void", "offset": 9101, "safe": false }, + { "name": "j32", "parameters": [], "returntype": "Void", "offset": 9124, "safe": false }, + { "name": "j33", "parameters": [], "returntype": "Void", "offset": 9147, "safe": false }, + { "name": "j34", "parameters": [], "returntype": "Void", "offset": 9170, "safe": false }, + { "name": "j35", "parameters": [], "returntype": "Void", "offset": 9193, "safe": false }, + { "name": "j36", "parameters": [], "returntype": "Void", "offset": 9216, "safe": false }, + { "name": "j37", "parameters": [], "returntype": "Void", "offset": 9239, "safe": false }, + { "name": "j38", "parameters": [], "returntype": "Void", "offset": 9262, "safe": false }, + { "name": "j39", "parameters": [], "returntype": "Void", "offset": 9285, "safe": false }, + { "name": "j40", "parameters": [], "returntype": "Void", "offset": 9308, "safe": false }, + { "name": "j41", "parameters": [], "returntype": "Void", "offset": 9331, "safe": false }, + { "name": "j42", "parameters": [], "returntype": "Void", "offset": 9354, "safe": false }, + { "name": "j43", "parameters": [], "returntype": "Void", "offset": 9377, "safe": false }, + { "name": "j44", "parameters": [], "returntype": "Void", "offset": 9400, "safe": false }, + { "name": "j45", "parameters": [], "returntype": "Void", "offset": 9423, "safe": false }, + { "name": "j46", "parameters": [], "returntype": "Void", "offset": 9446, "safe": false }, + { "name": "j47", "parameters": [], "returntype": "Void", "offset": 9469, "safe": false }, + { "name": "j48", "parameters": [], "returntype": "Void", "offset": 9492, "safe": false }, + { "name": "j49", "parameters": [], "returntype": "Void", "offset": 9515, "safe": false }, + { "name": "j50", "parameters": [], "returntype": "Void", "offset": 9538, "safe": false }, + { + "name": "k", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 9561, + "safe": false + }, + { "name": "k0", "parameters": [], "returntype": "Integer", "offset": 9601, "safe": false }, + { "name": "k10", "parameters": [], "returntype": "Void", "offset": 9646, "safe": false }, + { "name": "k11", "parameters": [], "returntype": "Void", "offset": 9687, "safe": false }, + { "name": "k12", "parameters": [], "returntype": "Void", "offset": 9728, "safe": false }, + { "name": "k13", "parameters": [], "returntype": "Void", "offset": 9769, "safe": false }, + { "name": "k14", "parameters": [], "returntype": "Void", "offset": 9810, "safe": false }, + { "name": "k15", "parameters": [], "returntype": "Void", "offset": 9851, "safe": false }, + { "name": "k16", "parameters": [], "returntype": "Void", "offset": 9892, "safe": false }, + { "name": "k17", "parameters": [], "returntype": "Void", "offset": 9933, "safe": false }, + { "name": "k18", "parameters": [], "returntype": "Void", "offset": 9975, "safe": false }, + { "name": "k19", "parameters": [], "returntype": "Void", "offset": 10017, "safe": false }, + { "name": "k20", "parameters": [], "returntype": "Void", "offset": 10059, "safe": false }, + { "name": "k21", "parameters": [], "returntype": "Void", "offset": 10101, "safe": false }, + { "name": "k22", "parameters": [], "returntype": "Void", "offset": 10143, "safe": false }, + { "name": "k23", "parameters": [], "returntype": "Void", "offset": 10185, "safe": false }, + { "name": "k24", "parameters": [], "returntype": "Void", "offset": 10227, "safe": false }, + { "name": "k25", "parameters": [], "returntype": "Void", "offset": 10269, "safe": false }, + { "name": "k26", "parameters": [], "returntype": "Void", "offset": 10311, "safe": false }, + { "name": "k27", "parameters": [], "returntype": "Void", "offset": 10353, "safe": false }, + { "name": "k28", "parameters": [], "returntype": "Void", "offset": 10395, "safe": false }, + { "name": "k29", "parameters": [], "returntype": "Void", "offset": 10437, "safe": false }, + { "name": "k30", "parameters": [], "returntype": "Void", "offset": 10479, "safe": false }, + { "name": "k31", "parameters": [], "returntype": "Void", "offset": 10521, "safe": false }, + { "name": "k32", "parameters": [], "returntype": "Void", "offset": 10563, "safe": false }, + { "name": "k33", "parameters": [], "returntype": "Void", "offset": 10605, "safe": false }, + { "name": "k34", "parameters": [], "returntype": "Void", "offset": 10647, "safe": false }, + { "name": "k35", "parameters": [], "returntype": "Void", "offset": 10689, "safe": false }, + { "name": "k36", "parameters": [], "returntype": "Void", "offset": 10731, "safe": false }, + { "name": "k37", "parameters": [], "returntype": "Void", "offset": 10773, "safe": false }, + { "name": "k38", "parameters": [], "returntype": "Void", "offset": 10815, "safe": false }, + { "name": "k39", "parameters": [], "returntype": "Void", "offset": 10857, "safe": false }, + { "name": "k40", "parameters": [], "returntype": "Void", "offset": 10899, "safe": false }, + { "name": "k41", "parameters": [], "returntype": "Void", "offset": 10941, "safe": false }, + { "name": "k42", "parameters": [], "returntype": "Void", "offset": 10983, "safe": false }, + { "name": "k43", "parameters": [], "returntype": "Void", "offset": 11025, "safe": false }, + { "name": "k44", "parameters": [], "returntype": "Void", "offset": 11067, "safe": false }, + { "name": "k45", "parameters": [], "returntype": "Void", "offset": 11109, "safe": false }, + { "name": "k46", "parameters": [], "returntype": "Void", "offset": 11151, "safe": false }, + { "name": "k47", "parameters": [], "returntype": "Void", "offset": 11193, "safe": false }, + { "name": "k48", "parameters": [], "returntype": "Void", "offset": 11235, "safe": false }, + { "name": "k49", "parameters": [], "returntype": "Void", "offset": 11277, "safe": false }, + { "name": "k50", "parameters": [], "returntype": "Void", "offset": 11319, "safe": false }, + { + "name": "l", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 11361, + "safe": false + }, + { "name": "l0", "parameters": [], "returntype": "Integer", "offset": 11401, "safe": false }, + { "name": "l10", "parameters": [], "returntype": "Void", "offset": 11446, "safe": false }, + { "name": "l11", "parameters": [], "returntype": "Void", "offset": 11487, "safe": false }, + { "name": "l12", "parameters": [], "returntype": "Void", "offset": 11528, "safe": false }, + { "name": "l13", "parameters": [], "returntype": "Void", "offset": 11569, "safe": false }, + { "name": "l14", "parameters": [], "returntype": "Void", "offset": 11610, "safe": false }, + { "name": "l15", "parameters": [], "returntype": "Void", "offset": 11651, "safe": false }, + { "name": "l16", "parameters": [], "returntype": "Void", "offset": 11692, "safe": false }, + { "name": "l17", "parameters": [], "returntype": "Void", "offset": 11733, "safe": false }, + { "name": "l18", "parameters": [], "returntype": "Void", "offset": 11775, "safe": false }, + { "name": "l19", "parameters": [], "returntype": "Void", "offset": 11817, "safe": false }, + { "name": "l20", "parameters": [], "returntype": "Void", "offset": 11859, "safe": false }, + { "name": "l21", "parameters": [], "returntype": "Void", "offset": 11901, "safe": false }, + { "name": "l22", "parameters": [], "returntype": "Void", "offset": 11943, "safe": false }, + { "name": "l23", "parameters": [], "returntype": "Void", "offset": 11985, "safe": false }, + { "name": "l24", "parameters": [], "returntype": "Void", "offset": 12027, "safe": false }, + { "name": "l25", "parameters": [], "returntype": "Void", "offset": 12069, "safe": false }, + { "name": "l26", "parameters": [], "returntype": "Void", "offset": 12111, "safe": false }, + { "name": "l27", "parameters": [], "returntype": "Void", "offset": 12153, "safe": false }, + { "name": "l28", "parameters": [], "returntype": "Void", "offset": 12195, "safe": false }, + { "name": "l29", "parameters": [], "returntype": "Void", "offset": 12237, "safe": false }, + { "name": "l30", "parameters": [], "returntype": "Void", "offset": 12279, "safe": false }, + { "name": "l31", "parameters": [], "returntype": "Void", "offset": 12321, "safe": false }, + { "name": "l32", "parameters": [], "returntype": "Void", "offset": 12363, "safe": false }, + { "name": "l33", "parameters": [], "returntype": "Void", "offset": 12405, "safe": false }, + { "name": "l34", "parameters": [], "returntype": "Void", "offset": 12447, "safe": false }, + { "name": "l35", "parameters": [], "returntype": "Void", "offset": 12489, "safe": false }, + { "name": "l36", "parameters": [], "returntype": "Void", "offset": 12531, "safe": false }, + { "name": "l37", "parameters": [], "returntype": "Void", "offset": 12573, "safe": false }, + { "name": "l38", "parameters": [], "returntype": "Void", "offset": 12615, "safe": false }, + { "name": "l39", "parameters": [], "returntype": "Void", "offset": 12657, "safe": false }, + { "name": "l40", "parameters": [], "returntype": "Void", "offset": 12699, "safe": false }, + { "name": "l41", "parameters": [], "returntype": "Void", "offset": 12741, "safe": false }, + { "name": "l42", "parameters": [], "returntype": "Void", "offset": 12783, "safe": false }, + { "name": "l43", "parameters": [], "returntype": "Void", "offset": 12825, "safe": false }, + { "name": "l44", "parameters": [], "returntype": "Void", "offset": 12867, "safe": false }, + { "name": "l45", "parameters": [], "returntype": "Void", "offset": 12909, "safe": false }, + { "name": "l46", "parameters": [], "returntype": "Void", "offset": 12951, "safe": false }, + { "name": "l47", "parameters": [], "returntype": "Void", "offset": 12993, "safe": false }, + { "name": "l48", "parameters": [], "returntype": "Void", "offset": 13035, "safe": false }, + { "name": "l49", "parameters": [], "returntype": "Void", "offset": 13077, "safe": false }, + { "name": "l50", "parameters": [], "returntype": "Void", "offset": 13119, "safe": false }, + { + "name": "m", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 13161, + "safe": false + }, + { "name": "m0", "parameters": [], "returntype": "Integer", "offset": 13181, "safe": false }, + { "name": "m10", "parameters": [], "returntype": "Void", "offset": 13206, "safe": false }, + { "name": "m11", "parameters": [], "returntype": "Void", "offset": 13227, "safe": false }, + { "name": "m12", "parameters": [], "returntype": "Void", "offset": 13248, "safe": false }, + { "name": "m13", "parameters": [], "returntype": "Void", "offset": 13269, "safe": false }, + { "name": "m14", "parameters": [], "returntype": "Void", "offset": 13290, "safe": false }, + { "name": "m15", "parameters": [], "returntype": "Void", "offset": 13311, "safe": false }, + { "name": "m16", "parameters": [], "returntype": "Void", "offset": 13332, "safe": false }, + { "name": "m17", "parameters": [], "returntype": "Void", "offset": 13353, "safe": false }, + { "name": "m18", "parameters": [], "returntype": "Void", "offset": 13375, "safe": false }, + { "name": "m19", "parameters": [], "returntype": "Void", "offset": 13397, "safe": false }, + { "name": "m20", "parameters": [], "returntype": "Void", "offset": 13419, "safe": false }, + { "name": "m21", "parameters": [], "returntype": "Void", "offset": 13441, "safe": false }, + { "name": "m22", "parameters": [], "returntype": "Void", "offset": 13463, "safe": false }, + { "name": "m23", "parameters": [], "returntype": "Void", "offset": 13485, "safe": false }, + { "name": "m24", "parameters": [], "returntype": "Void", "offset": 13507, "safe": false }, + { "name": "m25", "parameters": [], "returntype": "Void", "offset": 13529, "safe": false }, + { "name": "m26", "parameters": [], "returntype": "Void", "offset": 13551, "safe": false }, + { "name": "m27", "parameters": [], "returntype": "Void", "offset": 13573, "safe": false }, + { "name": "m28", "parameters": [], "returntype": "Void", "offset": 13595, "safe": false }, + { "name": "m29", "parameters": [], "returntype": "Void", "offset": 13617, "safe": false }, + { "name": "m30", "parameters": [], "returntype": "Void", "offset": 13639, "safe": false }, + { "name": "m31", "parameters": [], "returntype": "Void", "offset": 13661, "safe": false }, + { "name": "m32", "parameters": [], "returntype": "Void", "offset": 13683, "safe": false }, + { "name": "m33", "parameters": [], "returntype": "Void", "offset": 13705, "safe": false }, + { "name": "m34", "parameters": [], "returntype": "Void", "offset": 13727, "safe": false }, + { "name": "m35", "parameters": [], "returntype": "Void", "offset": 13749, "safe": false }, + { "name": "m36", "parameters": [], "returntype": "Void", "offset": 13771, "safe": false }, + { "name": "m37", "parameters": [], "returntype": "Void", "offset": 13793, "safe": false }, + { "name": "m38", "parameters": [], "returntype": "Void", "offset": 13815, "safe": false }, + { "name": "m39", "parameters": [], "returntype": "Void", "offset": 13837, "safe": false }, + { "name": "m40", "parameters": [], "returntype": "Void", "offset": 13859, "safe": false }, + { "name": "m41", "parameters": [], "returntype": "Void", "offset": 13881, "safe": false }, + { "name": "m42", "parameters": [], "returntype": "Void", "offset": 13903, "safe": false }, + { "name": "m43", "parameters": [], "returntype": "Void", "offset": 13925, "safe": false }, + { "name": "m44", "parameters": [], "returntype": "Void", "offset": 13947, "safe": false }, + { "name": "m45", "parameters": [], "returntype": "Void", "offset": 13969, "safe": false }, + { "name": "m46", "parameters": [], "returntype": "Void", "offset": 13991, "safe": false }, + { "name": "m47", "parameters": [], "returntype": "Void", "offset": 14013, "safe": false }, + { "name": "m48", "parameters": [], "returntype": "Void", "offset": 14035, "safe": false }, + { "name": "m49", "parameters": [], "returntype": "Void", "offset": 14057, "safe": false }, + { "name": "m50", "parameters": [], "returntype": "Void", "offset": 14079, "safe": false }, + { + "name": "n", + "parameters": [{"name": "amountIn","type": "Integer"}], + "returntype": "Void", + "offset": 14101, + "safe": false + }, + { "name": "n0", "parameters": [], "returntype": "Integer", "offset": 14121, "safe": false }, + { "name": "n10", "parameters": [], "returntype": "Void", "offset": 14146, "safe": false }, + { "name": "n11", "parameters": [], "returntype": "Void", "offset": 14167, "safe": false }, + { "name": "n12", "parameters": [], "returntype": "Void", "offset": 14188, "safe": false }, + { "name": "n13", "parameters": [], "returntype": "Void", "offset": 14209, "safe": false }, + { "name": "n14", "parameters": [], "returntype": "Void", "offset": 14230, "safe": false }, + { "name": "n15", "parameters": [], "returntype": "Void", "offset": 14251, "safe": false }, + { "name": "n16", "parameters": [], "returntype": "Void", "offset": 14272, "safe": false }, + { "name": "n17", "parameters": [], "returntype": "Void", "offset": 14293, "safe": false }, + { "name": "n18", "parameters": [], "returntype": "Void", "offset": 14315, "safe": false }, + { "name": "n19", "parameters": [], "returntype": "Void", "offset": 14337, "safe": false }, + { "name": "n20", "parameters": [], "returntype": "Void", "offset": 14359, "safe": false }, + { "name": "n21", "parameters": [], "returntype": "Void", "offset": 14381, "safe": false }, + { "name": "n22", "parameters": [], "returntype": "Void", "offset": 14403, "safe": false }, + { "name": "n23", "parameters": [], "returntype": "Void", "offset": 14425, "safe": false }, + { "name": "n24", "parameters": [], "returntype": "Void", "offset": 14447, "safe": false }, + { "name": "n25", "parameters": [], "returntype": "Void", "offset": 14469, "safe": false }, + { "name": "n26", "parameters": [], "returntype": "Void", "offset": 14491, "safe": false }, + { "name": "n27", "parameters": [], "returntype": "Void", "offset": 14513, "safe": false }, + { "name": "n28", "parameters": [], "returntype": "Void", "offset": 14535, "safe": false }, + { "name": "n29", "parameters": [], "returntype": "Void", "offset": 14557, "safe": false }, + { "name": "n30", "parameters": [], "returntype": "Void", "offset": 14579, "safe": false }, + { "name": "n31", "parameters": [], "returntype": "Void", "offset": 14601, "safe": false }, + { "name": "n32", "parameters": [], "returntype": "Void", "offset": 14623, "safe": false }, + { "name": "n33", "parameters": [], "returntype": "Void", "offset": 14645, "safe": false }, + { "name": "n34", "parameters": [], "returntype": "Void", "offset": 14667, "safe": false }, + { "name": "n35", "parameters": [], "returntype": "Void", "offset": 14689, "safe": false }, + { "name": "n36", "parameters": [], "returntype": "Void", "offset": 14711, "safe": false }, + { "name": "n37", "parameters": [], "returntype": "Void", "offset": 14733, "safe": false }, + { "name": "n38", "parameters": [], "returntype": "Void", "offset": 14755, "safe": false }, + { "name": "n39", "parameters": [], "returntype": "Void", "offset": 14777, "safe": false }, + { "name": "n40", "parameters": [], "returntype": "Void", "offset": 14799, "safe": false }, + { "name": "n41", "parameters": [], "returntype": "Void", "offset": 14821, "safe": false }, + { "name": "n42", "parameters": [], "returntype": "Void", "offset": 14843, "safe": false }, + { "name": "n43", "parameters": [], "returntype": "Void", "offset": 14865, "safe": false }, + { "name": "n44", "parameters": [], "returntype": "Void", "offset": 14887, "safe": false }, + { "name": "n45", "parameters": [], "returntype": "Void", "offset": 14909, "safe": false }, + { "name": "n46", "parameters": [], "returntype": "Void", "offset": 14931, "safe": false }, + { "name": "n47", "parameters": [], "returntype": "Void", "offset": 14953, "safe": false }, + { "name": "n48", "parameters": [], "returntype": "Void", "offset": 14975, "safe": false }, + { "name": "n49", "parameters": [], "returntype": "Void", "offset": 14997, "safe": false }, + { "name": "n50", "parameters": [], "returntype": "Void", "offset": 15019, "safe": false }, + { "name": "_initialize", "parameters": [], "returntype": "Void", "offset": 15041, "safe": false } + ], + "events": [] + }, + "permissions": [{"contract": "*", "methods": "*"} ], + "trusts": [], + "extra": { + "Author": "Test", + "Email": "Test@Test", + "Description": "This is a Test Contract" + } +} \ No newline at end of file diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleContractCall.manifest.json b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleContractCall.manifest.json new file mode 100644 index 0000000000..3f05a9422b --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleContractCall.manifest.json @@ -0,0 +1,46 @@ +{ + "name": "SampleContractCall", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "onNEP17Payment", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 0, + "safe": false + }, + { + "name": "_initialize", + "parameters": [], + "returntype": "Void", + "offset": 91, + "safe": false + } + ], + "events": [] + }, + "permissions": [], + "trusts": [], + "extra": { + "Author": "core-dev", + "Version": "0.0.1", + "Description": "A sample contract to demonstrate how to call a contract", + "Sourcecode": "https://github.com/neo-project/neo-devpack-dotnet/tree/master/examples/" + } +} \ No newline at end of file diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleEvent.manifest.json b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleEvent.manifest.json new file mode 100644 index 0000000000..f9726eff87 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleEvent.manifest.json @@ -0,0 +1,62 @@ +{ + "name": "SampleEvent", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "main", + "parameters": [], + "returntype": "Boolean", + "offset": 0, + "safe": false + } + ], + "events": [ + { + "name": "new_event_name", + "parameters": [ + { + "name": "arg1", + "type": "ByteArray" + }, + { + "name": "arg2", + "type": "String" + }, + { + "name": "arg3", + "type": "Integer" + } + ] + }, + { + "name": "event2", + "parameters": [ + { + "name": "arg1", + "type": "ByteArray" + }, + { + "name": "arg2", + "type": "Integer" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": { + "Author": "code-dev", + "Description": "A sample contract that demonstrates how to use Events", + "Version": "0.0.1", + "Sourcecode": "https://github.com/neo-project/neo-devpack-dotnet/tree/master/examples/" + } +} \ No newline at end of file diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleException.manifest.json b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleException.manifest.json new file mode 100644 index 0000000000..0ade2ff9b0 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleException.manifest.json @@ -0,0 +1,164 @@ +{ + "name": "SampleException", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "try01", + "parameters": [], + "returntype": "Any", + "offset": 0, + "safe": false + }, + { + "name": "try02", + "parameters": [], + "returntype": "Any", + "offset": 77, + "safe": false + }, + { + "name": "try03", + "parameters": [], + "returntype": "Any", + "offset": 166, + "safe": false + }, + { + "name": "tryNest", + "parameters": [], + "returntype": "Any", + "offset": 259, + "safe": false + }, + { + "name": "tryFinally", + "parameters": [], + "returntype": "Any", + "offset": 404, + "safe": false + }, + { + "name": "tryFinallyAndRethrow", + "parameters": [], + "returntype": "Any", + "offset": 474, + "safe": false + }, + { + "name": "tryCatch", + "parameters": [], + "returntype": "Any", + "offset": 550, + "safe": false + }, + { + "name": "tryWithTwoFinally", + "parameters": [], + "returntype": "Any", + "offset": 628, + "safe": false + }, + { + "name": "tryecpointCast", + "parameters": [], + "returntype": "Any", + "offset": 920, + "safe": false + }, + { + "name": "tryvalidByteString2Ecpoint", + "parameters": [], + "returntype": "Any", + "offset": 1010, + "safe": false + }, + { + "name": "tryinvalidByteArray2UInt160", + "parameters": [], + "returntype": "Any", + "offset": 1100, + "safe": false + }, + { + "name": "tryvalidByteArray2UInt160", + "parameters": [], + "returntype": "Any", + "offset": 1190, + "safe": false + }, + { + "name": "tryinvalidByteArray2UInt256", + "parameters": [], + "returntype": "Any", + "offset": 1280, + "safe": false + }, + { + "name": "tryvalidByteArray2UInt256", + "parameters": [], + "returntype": "Any", + "offset": 1370, + "safe": false + }, + { + "name": "tryNULL2Ecpoint_1", + "parameters": [], + "returntype": "Array", + "offset": 1476, + "safe": false + }, + { + "name": "tryNULL2Uint160_1", + "parameters": [], + "returntype": "Array", + "offset": 1652, + "safe": false + }, + { + "name": "tryNULL2Uint256_1", + "parameters": [], + "returntype": "Array", + "offset": 1828, + "safe": false + }, + { + "name": "tryNULL2Bytestring_1", + "parameters": [], + "returntype": "Array", + "offset": 1990, + "safe": false + }, + { + "name": "tryUncatchableException", + "parameters": [], + "returntype": "Any", + "offset": 2141, + "safe": false + }, + { + "name": "_initialize", + "parameters": [], + "returntype": "Void", + "offset": 2219, + "safe": false + } + ], + "events": [] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": { + "Author": "core-dev", + "Description": "A sample contract to demonstrate how to handle exception", + "Version": "0.0.1", + "Sourcecode": "https://github.com/neo-project/neo-devpack-dotnet/tree/master/examples/" + } +} \ No newline at end of file diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleHelloWorld.manifest.json b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleHelloWorld.manifest.json new file mode 100644 index 0000000000..a0f38d55d3 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleHelloWorld.manifest.json @@ -0,0 +1,31 @@ +{ + "name": "SampleHelloWorld", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "sayHello", + "parameters": [], + "returntype": "String", + "offset": 0, + "safe": true + } + ], + "events": [] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": { + "Description": "A simple \u0060hello world\u0060 contract", + "E-mail": "dev@neo.org", + "Version": "0.0.1", + "Sourcecode": "https://github.com/neo-project/neo-devpack-dotnet/tree/master/examples/" + } +} \ No newline at end of file diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleNep17Token.manifest.json b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleNep17Token.manifest.json new file mode 100644 index 0000000000..507c64ea97 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleNep17Token.manifest.json @@ -0,0 +1,219 @@ +{ + "name": "SampleNep17Token", + "groups": [], + "features": {}, + "supportedstandards": [ + "NEP-17" + ], + "abi": { + "methods": [ + { + "name": "symbol", + "parameters": [], + "returntype": "String", + "offset": 1333, + "safe": true + }, + { + "name": "decimals", + "parameters": [], + "returntype": "Integer", + "offset": 1348, + "safe": true + }, + { + "name": "totalSupply", + "parameters": [], + "returntype": "Integer", + "offset": 52, + "safe": true + }, + { + "name": "balanceOf", + "parameters": [ + { + "name": "owner", + "type": "Hash160" + } + ], + "returntype": "Integer", + "offset": 98, + "safe": true + }, + { + "name": "transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + }, + { + "name": "data", + "type": "Any" + } + ], + "returntype": "Boolean", + "offset": 362, + "safe": false + }, + { + "name": "getOwner", + "parameters": [], + "returntype": "Hash160", + "offset": 808, + "safe": true + }, + { + "name": "setOwner", + "parameters": [ + { + "name": "newOwner", + "type": "Any" + } + ], + "returntype": "Void", + "offset": 877, + "safe": false + }, + { + "name": "getMinter", + "parameters": [], + "returntype": "Hash160", + "offset": 980, + "safe": true + }, + { + "name": "setMinter", + "parameters": [ + { + "name": "newMinter", + "type": "Hash160" + } + ], + "returntype": "Void", + "offset": 1025, + "safe": false + }, + { + "name": "mint", + "parameters": [ + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 1103, + "safe": false + }, + { + "name": "burn", + "parameters": [ + { + "name": "account", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ], + "returntype": "Void", + "offset": 1158, + "safe": false + }, + { + "name": "verify", + "parameters": [], + "returntype": "Boolean", + "offset": 1216, + "safe": true + }, + { + "name": "update", + "parameters": [ + { + "name": "nefFile", + "type": "ByteArray" + }, + { + "name": "manifest", + "type": "String" + } + ], + "returntype": "Boolean", + "offset": 1222, + "safe": false + }, + { + "name": "_initialize", + "parameters": [], + "returntype": "Void", + "offset": 1271, + "safe": false + } + ], + "events": [ + { + "name": "Transfer", + "parameters": [ + { + "name": "from", + "type": "Hash160" + }, + { + "name": "to", + "type": "Hash160" + }, + { + "name": "amount", + "type": "Integer" + } + ] + }, + { + "name": "SetOwner", + "parameters": [ + { + "name": "newOwner", + "type": "Hash160" + } + ] + }, + { + "name": "SetMinter", + "parameters": [ + { + "name": "newMinter", + "type": "Hash160" + } + ] + } + ] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": { + "Author": "core-dev", + "Version": "0.0.1", + "Description": "A sample NEP-17 token", + "Sourcecode": "https://github.com/neo-project/neo-devpack-dotnet/tree/master/examples/" + } +} \ No newline at end of file diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleOracle.manifest.json b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleOracle.manifest.json new file mode 100644 index 0000000000..b5dd1bd2c0 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/TestFile/SampleOracle.manifest.json @@ -0,0 +1,62 @@ +{ + "name": "SampleOracle", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + { + "name": "getResponse", + "parameters": [], + "returntype": "String", + "offset": 0, + "safe": true + }, + { + "name": "doRequest", + "parameters": [], + "returntype": "Void", + "offset": 35, + "safe": false + }, + { + "name": "onOracleResponse", + "parameters": [ + { + "name": "requestedUrl", + "type": "String" + }, + { + "name": "userData", + "type": "Any" + }, + { + "name": "oracleResponse", + "type": "Integer" + }, + { + "name": "jsonString", + "type": "String" + } + ], + "returntype": "Void", + "offset": 333, + "safe": false + } + ], + "events": [] + }, + "permissions": [ + { + "contract": "*", + "methods": "*" + } + ], + "trusts": [], + "extra": { + "Author": "code-dev", + "Description": "A sample contract to demonstrate how to use Example.SmartContract.Oracle Service", + "Version": "0.0.1", + "Sourcecode": "https://github.com/neo-project/neo-devpack-dotnet/tree/master/examples/" + } +} \ No newline at end of file diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractEventDescriptor.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractEventDescriptor.cs new file mode 100644 index 0000000000..4580f73145 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractEventDescriptor.cs @@ -0,0 +1,31 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractEventDescriptor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.SmartContract.Manifest; + +namespace Neo.UnitTests.SmartContract.Manifest; + +[TestClass] +public class UT_ContractEventDescriptor +{ + [TestMethod] + public void TestFromJson() + { + var expected = new ContractEventDescriptor + { + Name = "AAA", + Parameters = [], + }; + var actual = ContractEventDescriptor.FromJson(expected.ToJson()); + Assert.AreEqual(expected.Name, actual.Name); + Assert.IsEmpty(actual.Parameters); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractGroup.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractGroup.cs new file mode 100644 index 0000000000..b8cbaf1802 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractGroup.cs @@ -0,0 +1,65 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractGroup.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.Wallets; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.SmartContract.Manifest; + +[TestClass] +public class UT_ContractGroup +{ + [TestMethod] + public void TestClone() + { + byte[] privateKey = RandomNumberFactory.NextBytes(32); + KeyPair keyPair = new(privateKey); + ContractGroup contractGroup = new() + { + PubKey = keyPair.PublicKey, + Signature = new byte[20] + }; + + ContractGroup clone = (ContractGroup)RuntimeHelpers.GetUninitializedObject(typeof(ContractGroup)); + ((IInteroperable)clone).FromStackItem(contractGroup.ToStackItem(null)); + Assert.AreEqual(clone.ToJson().ToString(), contractGroup.ToJson().ToString()); + } + + [TestMethod] + public void TestIsValid() + { + var privateKey = RandomNumberFactory.NextBytes(32); + KeyPair keyPair = new(privateKey); + ContractGroup contractGroup = new() + { + PubKey = keyPair.PublicKey, + Signature = new byte[64] + }; + Assert.IsFalse(contractGroup.IsValid(UInt160.Zero)); + + + var message = new byte[] { 0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01 }; + var signature = Crypto.Sign(message, keyPair.PrivateKey); + contractGroup = new ContractGroup + { + PubKey = keyPair.PublicKey, + Signature = signature + }; + Assert.IsTrue(contractGroup.IsValid(new UInt160(message))); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs new file mode 100644 index 0000000000..55816a3b7b --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs @@ -0,0 +1,391 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractManifest.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.Json; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.VM; +using Neo.VM.Types; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Array = Neo.VM.Types.Array; + +namespace Neo.UnitTests.SmartContract.Manifest; + +[TestClass] +public class UT_ContractManifest +{ + [TestMethod] + public void TestMainnetContract() + { + // 0x6f1837723768f27a6f6a14452977e3e0e264f2cc in mainnet + // read json from TestFile/SampleContract.manifest.json + var path = Path.Combine("SmartContract", "Manifest", "TestFile", "SampleContract.manifest.json"); + var json = File.ReadAllText(path); + var manifest = ContractManifest.Parse(json); + + var counter = new ReferenceCounter(); + var item = manifest.ToStackItem(counter); + var data = BinarySerializer.Serialize(item, 1024 * 1024, 4096); + + Assert.ThrowsExactly(() => _ = BinarySerializer.Deserialize(data, ExecutionEngineLimits.Default, counter)); + Assert.ThrowsExactly(() => _ = BinarySerializer.Serialize(item, 1024 * 1024, 2048)); + + item = BinarySerializer.Deserialize(data, ExecutionEngineLimits.Default with { MaxStackSize = 4096 }, counter); + var copy = item.ToInteroperable(); + + Assert.AreEqual(manifest.ToJson().ToString(false), copy.ToJson().ToString(false)); + } + + [TestMethod] + public void ParseFromJson_Default() + { + var json = """ + { + "name": "testManifest", + "groups": [], + "features": {}, + "supportedstandards": [], + "abi": { + "methods": [ + {"name":"testMethod","parameters":[],"returntype":"Void","offset":0,"safe":true} + ], + "events":[] + }, + "permissions": [{"contract":"*","methods":"*"}], + "trusts": [], + "extra": null + } + """; + + json = Regex.Replace(json, @"\s+", ""); + var manifest = ContractManifest.Parse(json); + + Assert.AreEqual(manifest.ToJson().ToString(), json); + Assert.AreEqual(manifest.ToJson().ToString(), TestUtils.CreateDefaultManifest().ToJson().ToString()); + Assert.IsTrue(manifest.IsValid(ExecutionEngineLimits.Default, UInt160.Zero)); + } + + [TestMethod] + public void ParseFromJson_Permissions() + { + var json = """ + { + "name":"testManifest", + "groups":[], + "features":{}, + "supportedstandards":[], + "abi":{ + "methods":[ + {"name":"testMethod","parameters":[],"returntype":"Void","offset":0,"safe":true} + ], + "events":[] + }, + "permissions":[ + {"contract":"0x0000000000000000000000000000000000000000","methods":["method1","method2"]} + ], + "trusts": [], + "extra": null + } + """; + json = Regex.Replace(json, @"\s+", ""); + var manifest = ContractManifest.Parse(json); + Assert.AreEqual(manifest.ToJson().ToString(), json); + + var check = TestUtils.CreateDefaultManifest(); + check.Permissions = [ + new ContractPermission() + { + Contract = ContractPermissionDescriptor.Create(UInt160.Zero), + Methods = WildcardContainer.Create("method1", "method2") + } + ]; + Assert.AreEqual(manifest.ToJson().ToString(), check.ToJson().ToString()); + } + + [TestMethod] + public void EqualTests() + { + var json = """ + { + "name":"testManifest", + "groups":[], + "features":{}, + "supportedstandards":[], + "abi":{ + "methods":[ + {"name":"testMethod","parameters":[],"returntype":"Void","offset":0,"safe":true} + ], + "events":[] + }, + "permissions":[ + {"contract":"0x0000000000000000000000000000000000000000","methods":["method1","method2"]} + ], + "trusts":[], + "extra":null + } + """; + json = Regex.Replace(json, @"\s+", ""); + var manifestA = ContractManifest.Parse(json); + var manifestB = ContractManifest.Parse(json); + + Assert.IsTrue(manifestA.Abi.Methods.SequenceEqual(manifestB.Abi.Methods)); + + for (int x = 0; x < manifestA.Abi.Methods.Length; x++) + { + Assert.IsTrue(manifestA.Abi.Methods[x] == manifestB.Abi.Methods[x]); + Assert.IsFalse(manifestA.Abi.Methods[x] != manifestB.Abi.Methods[x]); + } + } + + [TestMethod] + public void ParseFromJson_SafeMethods() + { + var json = """ + { + "name":"testManifest", + "groups":[], + "features":{}, + "supportedstandards":[], + "abi":{ + "methods":[ + {"name":"testMethod","parameters":[],"returntype":"Void","offset":0,"safe":true} + ], + "events":[] + }, + "permissions":[ + {"contract":"*","methods":"*"} + ], + "trusts":[], + "extra": null + } + """; + json = Regex.Replace(json, @"\s+", ""); + var manifest = ContractManifest.Parse(json); + Assert.AreEqual(manifest.ToJson().ToString(), json); + + var check = TestUtils.CreateDefaultManifest(); + Assert.AreEqual(manifest.ToJson().ToString(), check.ToJson().ToString()); + } + + [TestMethod] + public void ParseFromJson_Trust() + { + var json = """ + { + "name":"testManifest", + "groups":[], + "features":{}, + "supportedstandards":[], + "abi":{ + "methods":[ + {"name":"testMethod","parameters":[],"returntype":"Void","offset":0,"safe":true} + ], + "events":[] + }, + "permissions":[ + {"contract":"*","methods":"*"} + ], + "trusts":["0x0000000000000000000000000000000000000001", "*"], + "extra":null + } + """; + json = Regex.Replace(json, @"\s+", ""); + + var manifest = ContractManifest.Parse(json); + Assert.AreEqual(manifest.ToJson().ToString(), json); + + var check = TestUtils.CreateDefaultManifest(); + check.Trusts = WildcardContainer.Create( + ContractPermissionDescriptor.Create(UInt160.Parse("0x0000000000000000000000000000000000000001")), + ContractPermissionDescriptor.CreateWildcard()); + Assert.AreEqual(manifest.ToJson().ToString(), check.ToJson().ToString()); + } + + [TestMethod] + public void ToInteroperable_Trust() + { + var json = """ + { + "name":"CallOracleContract-6", + "groups":[], + "features":{}, + "supportedstandards":[], + "abi":{ + "methods":[{ + "name":"request", + "parameters":[ + {"name":"url","type":"String"}, + {"name":"filter","type":"String"}, + {"name":"gasForResponse","type":"Integer"} + ], + "returntype":"Void", + "offset":0, + "safe":false + },{ + "name":"callback", + "parameters":[ + {"name":"url","type":"String"}, + {"name":"userData","type":"Any"}, + {"name":"responseCode","type":"Integer"}, + {"name":"response","type":"ByteArray"} + ], + "returntype":"Void", + "offset":86, + "safe":false + },{ + "name":"getStoredUrl", + "parameters":[], + "returntype":"String", + "offset":129, + "safe":false + },{ + "name":"getStoredResponseCode", + "parameters":[], + "returntype":"Integer", + "offset":142, + "safe":false + },{ + "name":"getStoredResponse", + "parameters":[], + "returntype":"ByteArray", + "offset":165, + "safe":false + }], + "events":[] + }, + "permissions":[ + {"contract":"0xfe924b7cfe89ddd271abaf7210a80a7e11178758","methods":"*"}, + {"contract":"*","methods":"*"} + ], + "trusts":["0xfe924b7cfe89ddd271abaf7210a80a7e11178758", "*"], + "extra":{} + } + """; + json = Regex.Replace(json, @"\s+", ""); + var manifest = ContractManifest.Parse(json); + var s = (Struct)manifest.ToStackItem(new ReferenceCounter()); + manifest = s.ToInteroperable(); + + Assert.IsFalse(manifest.Permissions[0].Contract.IsWildcard); + Assert.IsTrue(manifest.Permissions[0].Methods.IsWildcard); + Assert.IsTrue(manifest.Permissions[1].Contract.IsWildcard); + Assert.IsTrue(manifest.Permissions[1].Methods.IsWildcard); + + Assert.IsFalse(manifest.Trusts[0].IsWildcard); + Assert.IsTrue(manifest.Trusts[1].IsWildcard); + } + + [TestMethod] + public void ParseFromJson_Groups() + { + var json = """ + { + "name":"testManifest", + "groups":[{ + "pubkey":"03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", + "signature":"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==" + }], + "features":{}, + "supportedstandards":[], + "abi":{ + "methods":[ + {"name":"testMethod","parameters":[],"returntype":"Void","offset":0,"safe":true} + ], + "events":[] + }, + "permissions":[ + {"contract":"*","methods":"*"} + ], + "trusts":[], + "extra":null + } + """; + json = Regex.Replace(json, @"\s+", ""); + var manifest = ContractManifest.Parse(json); + Assert.AreEqual(manifest.ToJson().ToString(), json); + + var signature = string.Concat(Enumerable.Repeat("41", 64)); + var check = TestUtils.CreateDefaultManifest(); + check.Groups = [ + new ContractGroup() { + PubKey = ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1), + Signature = signature.HexToBytes() + } + ]; + Assert.AreEqual(manifest.ToJson().ToString(), check.ToJson().ToString()); + } + + [TestMethod] + public void ParseFromJson_Extra() + { + var json = """ + { + "name":"testManifest", + "groups":[], + "features":{}, + "supportedstandards":[], + "abi":{ + "methods":[ + {"name":"testMethod","parameters":[],"returntype":"Void","offset":0,"safe":true} + ], + "events":[] + }, + "permissions":[{"contract":"*","methods":"*"}], + "trusts":[], + "extra":{"key":"value"} + } + """; + json = Regex.Replace(json, @"\s+", ""); + var manifest = ContractManifest.Parse(json); + Assert.AreEqual(json, manifest.ToJson().ToString()); + Assert.AreEqual("value", manifest.Extra!["key"]!.AsString(), false); + } + + [TestMethod] + public void TestDeserializeAndSerialize() + { + var expected = TestUtils.CreateDefaultManifest(); + expected.Extra = (JObject)JToken.Parse(@"{""a"":123}")!; + + var clone = (ContractManifest)RuntimeHelpers.GetUninitializedObject(typeof(ContractManifest)); + ((IInteroperable)clone).FromStackItem(expected.ToStackItem(null)); + + Assert.AreEqual(@"{""a"":123}", expected.Extra.ToString()); + Assert.AreEqual(expected.ToString(), clone.ToString()); + + expected.Extra = null; + clone = (ContractManifest)RuntimeHelpers.GetUninitializedObject(typeof(ContractManifest)); + ((IInteroperable)clone).FromStackItem(expected.ToStackItem(null)); + + Assert.AreEqual(expected.Extra, clone.Extra); + Assert.AreEqual(expected.ToString(), clone.ToString()); + } + + [TestMethod] + public void TestSerializeTrusts() + { + var check = TestUtils.CreateDefaultManifest(); + check.Trusts = WildcardContainer.Create( + ContractPermissionDescriptor.Create(UInt160.Parse("0x0000000000000000000000000000000000000001")), + ContractPermissionDescriptor.CreateWildcard()); + var si = check.ToStackItem(null); + + var actualTrusts = ((Array)si)[6]; + + Assert.HasCount(2, (Array)actualTrusts); + Assert.AreEqual(((Array)actualTrusts)[0], new ByteString(UInt160.Parse("0x0000000000000000000000000000000000000001").ToArray())); + // Wildcard trust should be represented as Null stackitem (not as zero-length ByteString): + Assert.AreEqual(((Array)actualTrusts)[1], StackItem.Null); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractPermission.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractPermission.cs new file mode 100644 index 0000000000..8eff08cc29 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractPermission.cs @@ -0,0 +1,85 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractPermission.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.UnitTests.SmartContract.Manifest; + +[TestClass] +public class UT_ContractPermission +{ + [TestMethod] + public void TestDeserialize() + { + // null + ContractPermission contractPermission = ContractPermission.DefaultPermission; + Struct s = (Struct)contractPermission.ToStackItem(new ReferenceCounter()); + + contractPermission = s.ToInteroperable(); + Assert.IsTrue(contractPermission.Contract.IsWildcard); + Assert.IsTrue(contractPermission.Methods.IsWildcard); + + // not null + contractPermission = new ContractPermission() + { + Contract = ContractPermissionDescriptor.Create(UInt160.Zero), + Methods = WildcardContainer.Create("test") + }; + s = (Struct)contractPermission.ToStackItem(new ReferenceCounter()); + + contractPermission = s.ToInteroperable(); + Assert.IsFalse(contractPermission.Contract.IsWildcard); + Assert.IsFalse(contractPermission.Methods.IsWildcard); + Assert.AreEqual(UInt160.Zero, contractPermission.Contract.Hash); + Assert.AreEqual("test", contractPermission.Methods[0]); + } + + [TestMethod] + public void TestIsAllowed() + { + ContractManifest contractManifest1 = TestUtils.CreateDefaultManifest(); + ContractPermission contractPermission1 = ContractPermission.DefaultPermission; + contractPermission1.Contract = ContractPermissionDescriptor.Create(UInt160.Zero); + Assert.IsTrue(contractPermission1.IsAllowed(new() { Hash = UInt160.Zero, Nef = null!, Manifest = contractManifest1 }, "AAA")); + contractPermission1.Contract = ContractPermissionDescriptor.CreateWildcard(); + + ContractManifest contractManifest2 = TestUtils.CreateDefaultManifest(); + ContractPermission contractPermission2 = ContractPermission.DefaultPermission; + contractPermission2.Contract = ContractPermissionDescriptor.Create(UInt160.Parse("0x0000000000000000000000000000000000000001")); + Assert.IsFalse(contractPermission2.IsAllowed(new() { Hash = UInt160.Zero, Nef = null!, Manifest = contractManifest2 }, "AAA")); + contractPermission2.Contract = ContractPermissionDescriptor.CreateWildcard(); + + byte[] privateKey3 = RandomNumberFactory.NextBytes(32); + ECPoint publicKey3 = ECCurve.Secp256r1.G * privateKey3; + ContractManifest contractManifest3 = TestUtils.CreateDefaultManifest(); + contractManifest3.Groups = [new ContractGroup() { PubKey = publicKey3, Signature = null! }]; + ContractPermission contractPermission3 = ContractPermission.DefaultPermission; + contractPermission3.Contract = ContractPermissionDescriptor.Create(publicKey3); + Assert.IsTrue(contractPermission3.IsAllowed(new() { Hash = UInt160.Zero, Nef = null!, Manifest = contractManifest3 }, "AAA")); + contractPermission3.Contract = ContractPermissionDescriptor.CreateWildcard(); + + byte[] privateKey41 = RandomNumberFactory.NextBytes(32); + ECPoint publicKey41 = ECCurve.Secp256r1.G * privateKey41; + byte[] privateKey42 = RandomNumberFactory.NextBytes(32); + ECPoint publicKey42 = ECCurve.Secp256r1.G * privateKey42; + ContractManifest contractManifest4 = TestUtils.CreateDefaultManifest(); + contractManifest4.Groups = [new ContractGroup() { PubKey = publicKey42, Signature = null! }]; + ContractPermission contractPermission4 = ContractPermission.DefaultPermission; + contractPermission4.Contract = ContractPermissionDescriptor.Create(publicKey41); + Assert.IsFalse(contractPermission4.IsAllowed(new() { Hash = UInt160.Zero, Nef = null!, Manifest = contractManifest4 }, "AAA")); + contractPermission4.Contract = ContractPermissionDescriptor.CreateWildcard(); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractPermissionDescriptor.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractPermissionDescriptor.cs new file mode 100644 index 0000000000..26eba744fe --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_ContractPermissionDescriptor.cs @@ -0,0 +1,75 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractPermissionDescriptor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Manifest; +using Neo.SmartContract.Native; +using Neo.Wallets; +using System.Security.Cryptography; + +namespace Neo.UnitTests.SmartContract.Manifest; + +[TestClass] +public class UT_ContractPermissionDescriptor +{ + [TestMethod] + public void TestCreateByECPointAndIsWildcard() + { + byte[] privateKey = new byte[32]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetBytes(privateKey); + var key = new KeyPair(privateKey); + ContractPermissionDescriptor contractPermissionDescriptor = ContractPermissionDescriptor.Create(key.PublicKey); + Assert.IsNotNull(contractPermissionDescriptor); + Assert.AreEqual(key.PublicKey, contractPermissionDescriptor.Group); + Assert.IsFalse(contractPermissionDescriptor.IsWildcard); + } + + [TestMethod] + public void TestContractPermissionDescriptorFromAndToJson() + { + byte[] privateKey = new byte[32]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetBytes(privateKey); + var key = new KeyPair(privateKey); + ContractPermissionDescriptor temp = ContractPermissionDescriptor.Create(key.PublicKey); + ContractPermissionDescriptor result = ContractPermissionDescriptor.FromJson(temp.ToJson()); + Assert.IsNull(result.Hash); + Assert.AreEqual(result.Group, result.Group); + Assert.ThrowsExactly(() => _ = ContractPermissionDescriptor.FromJson(string.Empty)); + } + + [TestMethod] + public void TestContractManifestFromJson() + { + Assert.ThrowsExactly(() => _ = ContractManifest.FromJson(new JObject())); + var jsonFiles = Directory.GetFiles(Path.Combine("SmartContract", "Manifest", "TestFile")); + foreach (var item in jsonFiles) + { + var json = (JObject)JToken.Parse(File.ReadAllText(item))!; + var manifest = ContractManifest.FromJson(json); + Assert.AreEqual(manifest.ToJson().ToString(), json.ToString()); + } + } + + [TestMethod] + public void TestEquals() + { + var descriptor1 = ContractPermissionDescriptor.CreateWildcard(); + var descriptor2 = ContractPermissionDescriptor.Create(NativeContract.NEO.Hash); + + Assert.AreNotEqual(descriptor1, descriptor2); + + var descriptor3 = ContractPermissionDescriptor.Create(NativeContract.NEO.Hash); + + Assert.AreEqual(descriptor2, descriptor3); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Manifest/UT_WildCardContainer.cs b/tests/Neo.UnitTests/SmartContract/Manifest/UT_WildCardContainer.cs new file mode 100644 index 0000000000..42723ada99 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Manifest/UT_WildCardContainer.cs @@ -0,0 +1,95 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_WildCardContainer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Manifest; +using System.Collections; + +namespace Neo.UnitTests.SmartContract.Manifest; + +[TestClass] +public class UT_WildCardContainer +{ + [TestMethod] + public void TestFromJson() + { + var jstring = new JString("*"); + var s = WildcardContainer.FromJson(jstring, u => u.AsString()); + Assert.IsTrue(s.IsWildcard); + Assert.IsEmpty(s); + + jstring = new JString("hello world"); + Assert.ThrowsExactly(() => _ = WildcardContainer.FromJson(jstring, u => u.AsString())); + + var alice = new JObject() + { + ["name"] = "alice", + ["age"] = 30 + }; + var jarray = new JArray { alice }; + var r = WildcardContainer.FromJson(jarray, u => u.AsString()); + Assert.AreEqual("{\"name\":\"alice\",\"age\":30}", r[0]); + + var jbool = new JBoolean(); + Assert.ThrowsExactly(() => _ = WildcardContainer.FromJson(jbool, u => u.AsString())); + } + + [TestMethod] + public void TestGetCount() + { + string[]? s = ["hello", "world"]; + var container = WildcardContainer.Create(s); + Assert.HasCount(2, container); + + container = WildcardContainer.CreateWildcard(); + Assert.IsEmpty(container); + } + + [TestMethod] + public void TestGetItem() + { + string[] s = ["hello", "world"]; + WildcardContainer container = WildcardContainer.Create(s); + Assert.AreEqual("hello", container[0]); + Assert.AreEqual("world", container[1]); + } + + [TestMethod] + public void TestGetEnumerator() + { + WildcardContainer container = WildcardContainer.CreateWildcard(); + IEnumerator enumerator = container.GetEnumerator(); + Assert.IsFalse(enumerator.MoveNext()); + + string[] s = ["hello", "world"]; + container = WildcardContainer.Create(s); + enumerator = container.GetEnumerator(); + foreach (string _ in s) + { + enumerator.MoveNext(); + Assert.AreEqual(_, enumerator.Current); + } + } + + [TestMethod] + public void TestIEnumerableGetEnumerator() + { + string[] s = ["hello", "world"]; + var container = WildcardContainer.Create(s); + IEnumerable enumerable = container; + var enumerator = enumerable.GetEnumerator(); + foreach (string _ in s) + { + enumerator.MoveNext(); + Assert.AreEqual(_, enumerator.Current); + } + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_ContractEventAttribute.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_ContractEventAttribute.cs new file mode 100644 index 0000000000..83ef885e53 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_ContractEventAttribute.cs @@ -0,0 +1,149 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractEventAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract.Native; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_ContractEventAttribute +{ + [TestMethod] + public void TestConstructorOneArg() + { + var arg = new ContractEventAttribute(0, "1", "a1", ContractParameterType.String); + + Assert.IsNull(arg.ActiveIn); + Assert.AreEqual(0, arg.Order); + Assert.AreEqual("1", arg.Descriptor.Name); + Assert.HasCount(1, arg.Descriptor.Parameters); + Assert.AreEqual("a1", arg.Descriptor.Parameters[0].Name); + Assert.AreEqual(ContractParameterType.String, arg.Descriptor.Parameters[0].Type); + + arg = new ContractEventAttribute(1, "1", "a1", ContractParameterType.String); + + Assert.IsNull(arg.ActiveIn); + Assert.AreEqual(1, arg.Order); + Assert.AreEqual("1", arg.Descriptor.Name); + Assert.HasCount(1, arg.Descriptor.Parameters); + Assert.AreEqual("a1", arg.Descriptor.Parameters[0].Name); + Assert.AreEqual(ContractParameterType.String, arg.Descriptor.Parameters[0].Type); + } + + [TestMethod] + public void TestConstructorTwoArg() + { + var arg = new ContractEventAttribute(0, "2", + "a1", ContractParameterType.String, + "a2", ContractParameterType.Integer); + + Assert.IsNull(arg.ActiveIn); + Assert.AreEqual(0, arg.Order); + Assert.AreEqual("2", arg.Descriptor.Name); + Assert.HasCount(2, arg.Descriptor.Parameters); + Assert.AreEqual("a1", arg.Descriptor.Parameters[0].Name); + Assert.AreEqual(ContractParameterType.String, arg.Descriptor.Parameters[0].Type); + Assert.AreEqual("a2", arg.Descriptor.Parameters[1].Name); + Assert.AreEqual(ContractParameterType.Integer, arg.Descriptor.Parameters[1].Type); + + arg = new ContractEventAttribute(1, "2", + "a1", ContractParameterType.String, + "a2", ContractParameterType.Integer); + + Assert.IsNull(arg.ActiveIn); + Assert.AreEqual(1, arg.Order); + Assert.AreEqual("2", arg.Descriptor.Name); + Assert.HasCount(2, arg.Descriptor.Parameters); + Assert.AreEqual("a1", arg.Descriptor.Parameters[0].Name); + Assert.AreEqual(ContractParameterType.String, arg.Descriptor.Parameters[0].Type); + Assert.AreEqual("a2", arg.Descriptor.Parameters[1].Name); + Assert.AreEqual(ContractParameterType.Integer, arg.Descriptor.Parameters[1].Type); + } + + [TestMethod] + public void TestConstructorThreeArg() + { + var arg = new ContractEventAttribute(0, "3", + "a1", ContractParameterType.String, + "a2", ContractParameterType.Integer, + "a3", ContractParameterType.Boolean); + + Assert.IsNull(arg.ActiveIn); + Assert.AreEqual(0, arg.Order); + Assert.AreEqual("3", arg.Descriptor.Name); + Assert.HasCount(3, arg.Descriptor.Parameters); + Assert.AreEqual("a1", arg.Descriptor.Parameters[0].Name); + Assert.AreEqual(ContractParameterType.String, arg.Descriptor.Parameters[0].Type); + Assert.AreEqual("a2", arg.Descriptor.Parameters[1].Name); + Assert.AreEqual(ContractParameterType.Integer, arg.Descriptor.Parameters[1].Type); + Assert.AreEqual("a3", arg.Descriptor.Parameters[2].Name); + Assert.AreEqual(ContractParameterType.Boolean, arg.Descriptor.Parameters[2].Type); + + arg = new ContractEventAttribute(1, "3", + "a1", ContractParameterType.String, + "a2", ContractParameterType.Integer, + "a3", ContractParameterType.Boolean); + + Assert.IsNull(arg.ActiveIn); + Assert.AreEqual(1, arg.Order); + Assert.AreEqual("3", arg.Descriptor.Name); + Assert.HasCount(3, arg.Descriptor.Parameters); + Assert.AreEqual("a1", arg.Descriptor.Parameters[0].Name); + Assert.AreEqual(ContractParameterType.String, arg.Descriptor.Parameters[0].Type); + Assert.AreEqual("a2", arg.Descriptor.Parameters[1].Name); + Assert.AreEqual(ContractParameterType.Integer, arg.Descriptor.Parameters[1].Type); + Assert.AreEqual("a3", arg.Descriptor.Parameters[2].Name); + Assert.AreEqual(ContractParameterType.Boolean, arg.Descriptor.Parameters[2].Type); + } + + [TestMethod] + public void TestConstructorFourArg() + { + var arg = new ContractEventAttribute(0, "4", + "a1", ContractParameterType.String, + "a2", ContractParameterType.Integer, + "a3", ContractParameterType.Boolean, + "a4", ContractParameterType.Array); + + Assert.IsNull(arg.ActiveIn); + Assert.AreEqual(0, arg.Order); + Assert.AreEqual("4", arg.Descriptor.Name); + Assert.HasCount(4, arg.Descriptor.Parameters); + Assert.AreEqual("a1", arg.Descriptor.Parameters[0].Name); + Assert.AreEqual(ContractParameterType.String, arg.Descriptor.Parameters[0].Type); + Assert.AreEqual("a2", arg.Descriptor.Parameters[1].Name); + Assert.AreEqual(ContractParameterType.Integer, arg.Descriptor.Parameters[1].Type); + Assert.AreEqual("a3", arg.Descriptor.Parameters[2].Name); + Assert.AreEqual(ContractParameterType.Boolean, arg.Descriptor.Parameters[2].Type); + Assert.AreEqual("a4", arg.Descriptor.Parameters[3].Name); + Assert.AreEqual(ContractParameterType.Array, arg.Descriptor.Parameters[3].Type); + + arg = new ContractEventAttribute(1, "4", + "a1", ContractParameterType.String, + "a2", ContractParameterType.Integer, + "a3", ContractParameterType.Boolean, + "a4", ContractParameterType.Array); + + Assert.IsNull(arg.ActiveIn); + Assert.AreEqual(1, arg.Order); + Assert.AreEqual("4", arg.Descriptor.Name); + Assert.HasCount(4, arg.Descriptor.Parameters); + Assert.AreEqual("a1", arg.Descriptor.Parameters[0].Name); + Assert.AreEqual(ContractParameterType.String, arg.Descriptor.Parameters[0].Type); + Assert.AreEqual("a2", arg.Descriptor.Parameters[1].Name); + Assert.AreEqual(ContractParameterType.Integer, arg.Descriptor.Parameters[1].Type); + Assert.AreEqual("a3", arg.Descriptor.Parameters[2].Name); + Assert.AreEqual(ContractParameterType.Boolean, arg.Descriptor.Parameters[2].Type); + Assert.AreEqual("a4", arg.Descriptor.Parameters[3].Name); + Assert.AreEqual(ContractParameterType.Array, arg.Descriptor.Parameters[3].Type); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_ContractMethodAttribute.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_ContractMethodAttribute.cs new file mode 100644 index 0000000000..ee9d46a5e7 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_ContractMethodAttribute.cs @@ -0,0 +1,71 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractMethodAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using System.Reflection; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_ContractMethodAttribute +{ + [TestMethod] + public void TestConstructorOneArg() + { + var arg = new ContractMethodAttribute(); + Assert.IsNull(arg.ActiveIn); + } + + class NeedSnapshot + { + [ContractMethod] + public static bool MethodReadOnlyStoreView(IReadOnlyStore view) => view is null; + + [ContractMethod] + public static bool MethodDataCache(DataCache dataCache) => dataCache is null; + } + + class NoNeedSnapshot + { + [ContractMethod] + public static bool MethodTwo(ApplicationEngine engine, UInt160 account) + => engine is null || account is null; + + [ContractMethod] + public static bool MethodOne(ApplicationEngine engine) => engine is null; + } + + [TestMethod] + public void TestNeedSnapshot() + { + var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; + foreach (var member in typeof(NeedSnapshot).GetMembers(flags)) + { + foreach (var attribute in member.GetCustomAttributes()) + { + var metadata = new ContractMethodMetadata(member, attribute); + Assert.IsTrue(metadata.NeedSnapshot); + } + } + + foreach (var member in typeof(NoNeedSnapshot).GetMembers(flags)) + { + foreach (var attribute in member.GetCustomAttributes()) + { + var metadata = new ContractMethodMetadata(member, attribute); + Assert.IsFalse(metadata.NeedSnapshot); + Assert.IsTrue(metadata.NeedApplicationEngine); + } + } + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs new file mode 100644 index 0000000000..705952bda5 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -0,0 +1,1227 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_CryptoLib.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.BLS12_381; +using Neo.Cryptography.ECC; +using Neo.Extensions.VM; +using Neo.Ledger; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Org.BouncyCastle.Utilities.Encoders; +using System.Text; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_CryptoLib +{ + private static readonly string s_g1Hex = + "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb"; + + private static readonly string s_g2Hex = + "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e" + + "024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8"; + + private static readonly string s_gtHex = + "0f41e58663bf08cf068672cbd01a7ec73baca4d72ca93544deff686bfd6df543d48eaa24afe47e1efde449383b676631" + + "04c581234d086a9902249b64728ffd21a189e87935a954051c7cdba7b3872629a4fafc05066245cb9108f0242d0fe3ef" + + "03350f55a7aefcd3c31b4fcB6ce5771cc6a0e9786ab5973320c806ad360829107ba810c5a09ffdd9be2291a0c25a99a2" + + "11b8b424cd48bf38fcef68083b0b0ec5c81a93b330ee1a677d0d15ff7b984e8978ef48881e32fac91b93b47333e2ba57" + + "06fba23eb7c5af0d9f80940ca771b6ffd5857baaf222eb95a7d2809d61bfe02e1bfd1b68ff02f0b8102ae1c2d5d5ab1a" + + "19f26337d205fb469cd6bd15c3d5a04dc88784fbb3d0b2dbdea54d43b2b73f2cbb12d58386a8703e0f948226e47ee89d" + + "018107154f25a764bd3c79937a45b84546da634b8f6be14a8061e55cceba478b23f7dacaa35c8ca78beae9624045b4b6" + + "01b2f522473d171391125ba84dc4007cfbf2f8da752f7c74185203fcca589ac719c34dffbbaad8431dad1c1fb597aaa5" + + "193502b86edb8857c273fa075a50512937e0794e1e65a7617c90d8bd66065b1fffe51d7a579973b1315021ec3c19934f" + + "1368bb445c7c2d209703f239689ce34c0378a68e72a6b3b216da0e22a5031b54ddff57309396b38c881c4c849ec23e87" + + "089a1c5b46e5110b86750ec6a532348868a84045483c92b7af5af689452eafabf1a8943e50439f1d59882a98eaa0170f" + + "1250ebd871fc0a92a7b2d83168d0d727272d441befa15c503dd8e90ce98db3e7b6d194f60839c508a84305aaca1789b6"; + + private readonly byte[] g1 = s_g1Hex.HexToBytes(); + private readonly byte[] g2 = s_g2Hex.HexToBytes(); + private readonly byte[] gt = s_gtHex.HexToBytes(); + + + private readonly byte[] notG1 = + "8123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + .HexToBytes(); + + private readonly byte[] notG2 = + ("8123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + .HexToBytes(); + + [TestMethod] + public void TestG1() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g1); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + var result = engine.ResultStack.Pop(); + Assert.AreEqual(s_g1Hex, result.GetInterface().ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestG2() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g2); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + var result = engine.ResultStack.Pop(); + Assert.AreEqual(s_g2Hex, result.GetInterface().ToCompressed().ToHexString()); + } + + [TestMethod] + public void TestNotG1() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", notG1); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.FAULT, engine.Execute()); + } + + [TestMethod] + public void TestNotG2() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", notG2); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.FAULT, engine.Execute()); + } + + [TestMethod] + public void TestBls12381Add() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); + script.EmitPush(2); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush("bls12381Add"); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + var result = engine.ResultStack.Pop(); + var expected = + "079AB7B345EB23C944C957A36A6B74C37537163D4CBF73BAD9751DE1DD9C68EF72CB21447E259880F72A871C3EDA1B0C" + + "017F1C95CF79B22B459599EA57E613E00CB75E35DE1F837814A93B443C54241015AC9761F8FB20A44512FF5CFC04AC7F" + + "0F6B8B52B2B5D0661CBF232820A257B8C5594309C01C2A45E64C6A7142301E4FB36E6E16B5A85BD2E437599D103C3ACE" + + "06D8046C6B3424C4CD2D72CE98D279F2290A28A87E8664CB0040580D0C485F34DF45267F8C215DCBCD862787AB555C7E" + + "113286DEE21C9C63A458898BEB35914DC8DAAAC453441E7114B21AF7B5F47D559879D477CF2A9CBD5B40C86BECD07128" + + "0900410BB2751D0A6AF0FE175DCF9D864ECAAC463C6218745B543F9E06289922434EE446030923A3E4C4473B4E3B1914" + + "081ABD33A78D31EB8D4C1BB3BAAB0529BB7BAF1103D848B4CEAD1A8E0AA7A7B260FBE79C67DBE41CA4D65BA8A54A72B6" + + "1692A61CE5F4D7A093B2C46AA4BCA6C4A66CF873D405EBC9C35D8AA639763720177B23BEFFAF522D5E41D3C5310EA333" + + "1409CEBEF9EF393AA00F2AC64673675521E8FC8FDDAF90976E607E62A740AC59C3DDDF95A6DE4FBA15BEB30C43D4E3F8" + + "03A3734DBEB064BF4BC4A03F945A4921E49D04AB8D45FD753A28B8FA082616B4B17BBCB685E455FF3BF8F60C3BD32A0C" + + "185EF728CF41A1B7B700B7E445F0B372BC29E370BC227D443C70AE9DBCF73FEE8ACEDBD317A286A53266562D817269C0" + + "04FB0F149DD925D2C590A960936763E519C2B62E14C7759F96672CD852194325904197B0B19C6B528AB33566946AF39B"; + Assert.AreEqual(expected.ToLower(), result.GetInterface().ToArray().ToHexString()); + } + + [TestMethod] + public void TestBls12381Mul() + { + var data = new byte[32]; + data[0] = 0x03; + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using (ScriptBuilder script = new()) + { + script.EmitPush(false); + script.EmitPush(data); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); + script.EmitPush(3); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush("bls12381Mul"); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + var result = engine.ResultStack.Pop(); + var expected = + "18B2DB6B3286BAEA116CCAD8F5554D170A69B329A6DE5B24C50B8834965242001A1C58089FD872B211ACD3263897FA66" + + "0B117248D69D8AC745283A3E6A4CCEC607F6CF7CEDEE919575D4B7C8AE14C36001F76BE5FCA50ADC296EF8DF4926FA7F" + + "0B55A75F255FE61FC2DA7CFFE56ADC8775AAAB54C50D0C4952AD919D90FB0EB221C41ABB9F2352A11BE2D7F176ABE41E" + + "0E30AFB34FC2CE16136DE66900D92068F30011E9882C0A56E7E7B30F08442BE9E58D093E1888151136259D059FB53921" + + "0D635BC491D5244A16CA28FDCF10546EC0F7104D3A419DDC081BA30ECB0CD2289010C2D385946229B7A9735ADC827369" + + "14FE61AD26C6C38B787775DE3B939105DE055F8D7004358272A0823F6F1787A7ABB6C3C59C8C9CBD1674AC9005126328" + + "18CDD273F0D38833C07467EAF77743B70C924D43975D3821D47110A358757F926FCF970660FBDD74EF15D93B81E3AA29" + + "0C78F59CBC6ED0C1E0DCBADFD11A73EB7137850D29EFEB6FA321330D0CF70F5C7F6B004BCF86AC99125F8FECF8315793" + + "0BEC2AF89F8B378C6D7F63B0A07B3651F5207A84F62CEE929D574DA154EBE795D519B661086F069C9F061BA3B53DC491" + + "0EA1614C87B114E2F9EF328AC94E93D00440B412D5AE5A3C396D52D26C0CDF2156EBD3D3F60EA500C42120A7CE1F7EF8" + + "0F15323118956B17C09E80E96ED4E1572461D604CDE2533330C684F86680406B1D3EE830CBAFE6D29C9A0A2F41E03E26" + + "095B713EB7E782144DB1EC6B53047FCB606B7B665B3DD1F52E95FCF2AE59C4AB159C3F98468C0A43C36C022B548189B6"; + Assert.AreEqual(expected.ToLower(), result.GetInterface().ToArray().ToHexString()); + } + using (ScriptBuilder script = new()) + { + script.EmitPush(true); + script.EmitPush(data); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", gt); + script.EmitPush(3); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush("bls12381Mul"); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + var result = engine.ResultStack.Pop(); + var expected = + "014E367F06F92BB039AEDCDD4DF65FC05A0D985B4CA6B79AA2254A6C605EB424048FA7F6117B8D4DA8522CD9C767B045" + + "0EEF9FA162E25BD305F36D77D8FEDE115C807C0805968129F15C1AD8489C32C41CB49418B4AEF52390900720B6D8B02C" + + "0EAB6A8B1420007A88412AB65DE0D04FEECCA0302E7806761483410365B5E771FCE7E5431230AD5E9E1C280E8953C68D" + + "0BD06236E9BD188437ADC14D42728C6E7177399B6B5908687F491F91EE6CCA3A391EF6C098CBEAEE83D962FA604A718A" + + "0C9DB625A7AAC25034517EB8743B5868A3803B37B94374E35F152F922BA423FB8E9B3D2B2BBF9DD602558CA5237D3742" + + "0502B03D12B9230ED2A431D807B81BD18671EBF78380DD3CF490506187996E7C72F53C3914C76342A38A536FFAED4783" + + "18CDD273F0D38833C07467EAF77743B70C924D43975D3821D47110A358757F926FCF970660FBDD74EF15D93B81E3AA29" + + "0C78F59CBC6ED0C1E0DCBADFD11A73EB7137850D29EFEB6FA321330D0CF70F5C7F6B004BCF86AC99125F8FECF8315793" + + "0BEC2AF89F8B378C6D7F63B0A07B3651F5207A84F62CEE929D574DA154EBE795D519B661086F069C9F061BA3B53DC491" + + "0EA1614C87B114E2F9EF328AC94E93D00440B412D5AE5A3C396D52D26C0CDF2156EBD3D3F60EA500C42120A7CE1F7EF8" + + "0F15323118956B17C09E80E96ED4E1572461D604CDE2533330C684F86680406B1D3EE830CBAFE6D29C9A0A2F41E03E26" + + "095B713EB7E782144DB1EC6B53047FCB606B7B665B3DD1F52E95FCF2AE59C4AB159C3F98468C0A43C36C022B548189B6"; + Assert.AreEqual(expected.ToLower(), result.GetInterface().ToArray().ToHexString()); + } + } + + [TestMethod] + public void TestBls12381Pairing() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g2); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g1); + script.EmitPush(2); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush("bls12381Pairing"); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + var result = engine.ResultStack.Pop(); + var expected = + "0F41E58663BF08CF068672CBD01A7EC73BACA4D72CA93544DEFF686BFD6DF543D48EAA24AFE47E1EFDE449383B676631" + + "04C581234D086A9902249B64728FFD21A189E87935A954051C7CDBA7B3872629A4FAFC05066245CB9108F0242D0FE3EF" + + "03350F55A7AEFCD3C31B4FCB6CE5771CC6A0E9786AB5973320C806AD360829107BA810C5A09FFDD9BE2291A0C25A99A2" + + "11B8B424CD48BF38FCEF68083B0B0EC5C81A93B330EE1A677D0D15FF7B984E8978EF48881E32FAC91B93B47333E2BA57" + + "06FBA23EB7C5AF0D9F80940CA771B6FFD5857BAAF222EB95A7D2809D61BFE02E1BFD1B68FF02F0B8102AE1C2D5D5AB1A" + + "19F26337D205FB469CD6BD15C3D5A04DC88784FBB3D0B2DBDEA54D43B2B73F2CBB12D58386A8703E0F948226E47EE89D" + + "018107154F25A764BD3C79937A45B84546DA634B8F6BE14A8061E55CCEBA478B23F7DACAA35C8CA78BEAE9624045B4B6" + + "01B2F522473D171391125BA84DC4007CFBF2F8DA752F7C74185203FCCA589AC719C34DFFBBAAD8431DAD1C1FB597AAA5" + + "193502B86EDB8857C273FA075A50512937E0794E1E65A7617C90D8BD66065B1FFFE51D7A579973B1315021EC3C19934F" + + "1368BB445C7C2D209703F239689CE34C0378A68E72A6B3B216DA0E22A5031B54DDFF57309396B38C881C4C849EC23E87" + + "089A1C5B46E5110B86750EC6A532348868A84045483C92B7AF5AF689452EAFABF1A8943E50439F1D59882A98EAA0170F" + + "1250EBD871FC0A92A7B2D83168D0D727272D441BEFA15C503DD8E90CE98DB3E7B6D194F60839C508A84305AACA1789B6"; + Assert.AreEqual(expected.ToLower(), result.GetInterface().ToArray().ToHexString()); + } + + [TestMethod] + public void Bls12381Equal() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g1); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", g1); + script.EmitPush(2); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush("bls12381Equal"); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + var result = engine.ResultStack.Pop(); + Assert.IsTrue(result.GetBoolean()); + } + + private enum BLS12381PointType : byte + { + G1Proj, + G2Proj, + GT + } + + private static void CheckBls12381ScalarMul_Compat(string point, string mul, bool negative, string expected, BLS12381PointType expectedType) + { + var data = new byte[32]; + data[0] = 0x03; + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitPush(negative); + script.EmitPush(mul.ToLower().HexToBytes()); + script.EmitDynamicCall(NativeContract.CryptoLib.Hash, "bls12381Deserialize", point.ToLower().HexToBytes()); + script.EmitPush(3); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush("bls12381Mul"); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + var result = engine.ResultStack.Pop(); + switch (expectedType) + { + case BLS12381PointType.G1Proj: + Assert.AreEqual(expected, new G1Affine(result.GetInterface()).ToCompressed().ToHexString()); + break; + case BLS12381PointType.G2Proj: + Assert.AreEqual(expected, new G2Affine(result.GetInterface()).ToCompressed().ToHexString()); + break; + case BLS12381PointType.GT: + Assert.AreEqual(expected, result.GetInterface().ToArray().ToHexString()); + break; + default: + Assert.Fail("Unknown result point type."); + break; + } + } + + [TestMethod] + public void TestBls12381ScalarMul_Compat() + { + // GT mul by positive scalar. + CheckBls12381ScalarMul_Compat( + "14fd52fe9bfd08bbe23fcdf1d3bc5390c62e75a8786a72f8a343123a30a7c5f8d18508a21a2bf902f4db2c068913bc1c" + + "130e7ce13260d601c89ee717acfd3d4e1d80f409dd2a5c38b176f0b64d3d0a224c502717270dfecf2b825ac24608215c" + + "0d7fcfdf3c1552ada42b7e0521bc2e7389436660c352ecbf2eedf30b77b6b501df302399e6240473af47abe56fc97478" + + "0c214542fcc0cf10e3001fa5e82d398f6ba1ddd1ccdf133bfd75e033eae50aec66bd5e884b8c74d4c1c6ac7c01278ac5" + + "164a54600cb2e24fec168f82542fbf98234dbb9ddf06503dc3c497da88b73db584ba19e685b1b398b51f40160e6c8f09" + + "17b4a68dedcc04674e5f5739cf0d845ba801263f712ed4ddda59c1d9909148e3f28124ae770682c9b19233bf0bcfa00d" + + "05bfe708d381b066b83a883ba8251ce2ea6772cbde51e1322d82b2c8a026a2153f4822e20cb69b8b05003ee74e09cb48" + + "1728d688caa8a671f90b55488e272f48c7c5ae32526d3635a5343eb02640358d9ac445c76a5d8f52f653bbaee04ba5ce" + + "03c68b88c25be6fd3611cc21c9968e4f87e541beeccc5170b8696a439bb666ad8a6608ab30ebc7dfe56eaf0dd9ab8439" + + "171a6e4e0d608e6e6c8ac5ddcf8d6d2a950d06051e6b6c4d3feb6dc8dac2acadd345cadfb890454a2101a112f7471f0e" + + "001701f60f3d4352c4d388c0f198854908c0e939719709c1b3f82d2a25cc7156a3838bc141e041c259849326fbd0839f" + + "15cea6a78b89349dcd1c03695a74e72d3657af4ee2cf267337bc96363ef4a1c5d5d7a673cc3a3c1a1350043f99537d62", + "8463159bd9a1d1e1fd815172177ec24c0c291353ed88b3d1838fd9d63b1efd0b", + false, + "03dc980ce0c037634816f9fc1edb2e1807e38a51f838e3a684f195d6c52c41d6a8a5b64d57d3fda507bebe3bd4b661af" + + "0e4f7c46754b373c955982b4d64a24838cbc010d04b6ceb499bf411d114dab77eaf70f96ab66c2868dcd63706b602b07" + + "010c487fc16c90b61e1c2ad33c31c8f3fc86d114a59b127ac584640f149f3597102c55dd1ed8a305a10c052c0a724e57" + + "0fc079e410123735a6144ccd88d9e4e91d7b889f80b18a1741eacd6f244fce3cf57795e619b6648b9238053b4b8e4ed6" + + "115c905fbcb61525370667ff43144e12b700662a7344ac1af97f11d09779ca6865973f95ff318b42ff00df7c6eb95816" + + "0947a0ab6cb25534af51ce1f0b076907c6eb5ce0760bd7670cab8814cc3308766eb6e52b5427dbf85d6424990fd33545" + + "15ab880358bc55075a08f36b855694c02ee0bd63adefe235ba4ee41dc600a1cae950c1dc760bf7b1edd8712e9e90eebb" + + "19de705e29f4feb870129441bd4b9e91c3d37e60c12fa79a5b1e4132ba9498044e6fbf2de37e4dd88b4e9095b46f1220" + + "19e73a561ba3967b32813c3ec74b8e1b6ab619eeab698e6638114cb29ca9c3d353192db3d392fee2b4dfdfd36b13db44" + + "0534dd754417cffcd470f4d4cfdcb6d7896181c27b8b30622d7a4ca0a05a7ea67ca011cab07738235b115bbd33023969" + + "1487d2de5d679a8cad2fe5c7fff16b0b0f3f929619c8005289c3d7ffe5bcd5ea19651bfc9366682a2790cab45ee9a988" + + "15bb7e58dc666e2209cd9d700546cf181ceb43fe719243930984b696b0d18d4cd1f5d960e149a2b753b1396e4f8f3b16", + BLS12381PointType.GT + ); + var testData = + "0e0c651ff4a57adebab1fa41aa8d1e53d1cf6a6cc554282a24bb460ea0dc169d3ede8b5a93a331698f3926d273a729aa" + + "18788543413f43ada55a6a7505e3514f0db7e14d58311c3211962a350bcf908b3af90fbae31ff536fe542328ad25cd3e" + + "044a796200c8a8ead7edbc3a8a37209c5d37433ca7d8b0e644d7aac9726b524c41fef1cf0d546c252d795dffc445ddee" + + "07041f57c4c9a673bd314294e280ab61390731c09ad904bdd7b8c087d0ce857ea86e78f2d98e75d9b5e377e5751d67cf" + + "1717cbce31bc7ea6df95132549bf6d284a68005c53228127671afa54ecfd4c5c4debc437c4c6d9b9aeeee8b4159a5691" + + "128c6dc68b309fd822b14f3ce8ff390bd6834d30147e8ab2edc59d0d7b14cc13c79e6eed5fd6cae1795ba3760345d59c" + + "0c585f79c900902515e3e95938d9929ad8310e71fc7fd54be9c7529f244af40dadaca0b3bd8afd911f24b261079de48b" + + "161dd8f340d42bd84e717275193a0375d9e10fbe048bbea30abd64d3fe085c15b9be192f7baaa0b3a9658bcbb4292a0c" + + "0149beb30e54b065a75df45e5da77583f4471e3454cea90a00b5a9a224c15e2ebe01f0ab8aa86591c1012c618d41fdce" + + "07ecfcaddc8dc408b7176b79d8711a4161a56f41a5be6714cbcaa70e53387ab049826ac9e636640bc6da919e52f86f32" + + "09572b62d9bfd48bd2b5ef217932237b90a70d40167623d0f25a73b753e3214310bc5b6e017aebc1a9ca0c8067a97da6" + + "162c70cc754f1b2ac3b05ba834712758c8de4641ef09237edf588989182ab3047ee42da2b840fd3633fa0f34d46ad961"; + // GT mul by positive scalar. + CheckBls12381ScalarMul_Compat( + testData, + "06c93a0ebbc8b5cd3af798b8f72442a67aa885b395452a08e48ec80b4e9f1b3f", + false, + "0d6d91f120ab61e14a3163601ce584f053f1de9dc0a548b6fbf37a776ec7b6ce6b866e8c8b0fc0ac8d32a9a9747c98bf" + + "0e6aee5bddd058313958bfc3ac1ed75284628f92bb9b99fee101e1bee9d74bad7812287ea76bdbe07f20ff9998d6e9f0" + + "16689be1cfc4337433644a679945d5c34a6d4dd984c56d6c28428438268b385cb1d86f69b0377b18f9b084e1d0b65962" + + "13233d559a1b5caaba38be853f667fc3b1f9f2c4c9020584502ff5f370b0aba7768a1a4ca4328bc3c7be2bc9c3949f5e" + + "16fd3bfc16b11da41b7393e56e777640b000db15b6e6192e5c59dfece90c6fc0b6071fdeef7061974b5e967c5b88b1db" + + "09f7c92077c16f56aff9e9627f5e09928e965daee17d05ef3fdc0c502b649db473b5b2bba867d829b04d32cfeab73876" + + "14190b265382378f75e4e085a5537d4f200fe56b74b7c52c5546b30d51862e1ac1f60eba157880090a42ea9b0295529f" + + "134c1fc90f19a4c20dc0be105b07e0c67218b2f5619a66d8d770d539658eb74c255743e5847bc437fef3077d0a6c4f17" + + "198d63cf17e6957f2ad9449269af009635697e92254a3f67be9b8760fd9f974826a1829fedb4cf66968b7c63b0c88c51" + + "0da12e6d52255256757afa03ad29b5c1624292ef7eb463eb4bc81ac7426f36db3fe1513bdd31bc138bfe903bbb0c5207" + + "001335f708c16cea15ef6b77c3215326a779e927b8c2081b15adffe71ba75164e376665533c5bb59373b27dbe93a0a0e" + + "1796d821a1b9ff01846446c5ad53064cb9b941f97aa870285395e1a44c9f6e5144ea5a0cf57b9fdd962a5ec3ff1f72fe", + BLS12381PointType.GT + ); + // GT mul by positive scalar. + CheckBls12381ScalarMul_Compat( + testData, + "b0010000000000005e0000000000000071f30400000000006d9189c813000000", + false, + "0919ad29cdbe0b6bbd636fbe3c8930a1b959e5aa37294a6cc7d018e2776580768bb98bf91ce1bc97f2e6fa647e7dad7b" + + "15db564645d2e4868129ed414b7e369e831b8ff93997a22b6ca0e2ba288783f535aed4b44cf3e952897db1536da18a12" + + "0a70da2b9dd901bd12a5a7047d3b6346ba1aea53b642b7355a91f957687fccd840ef24af100d0ada6b49e35183456ec3" + + "0b505098526b975477b6ca0273d3a841c85e4a8319b950e76ec217a4f939844baa6b875a4046a30c618636fe9b25c620" + + "030f31044f883789945c2bcb75d7d4099b2bc97665e75c1bee27bc3864e7e5e2ccb57a9da0b57be1a6aca217a6cfda09" + + "0c4fd222f7b8cfdc32969da4fe8828a59ee1314546efdf99ef7ede1a42df6e7a126fe83b4c41b5e70a56bd9ab499f7e8" + + "0e27a08884be05f1d2a527417fc6e30448333c0724463bf92d722ef5fd6f06949e294e6f941976d24c856038b55a2ec2" + + "00d14d958a688f23b572993bd0f18cbbc20defe88e423b262c552dcc4d9f63ad78e85efbcea9449f81f39e1a887eb79b" + + "07056bb5a672444e240660617ba7a40985a622c687c1d05c12cee7b086abfc5f39a83a5ad7638ee559f710013b772d42" + + "07924687cb30100bcd4e8c83c9fa19dce7785bf3ae7681a0968fd9661c990e2dace05902dceeed65aacf51a04e72f0fd" + + "04858ea70fb72f2a3807dc1839a385d85b536abfd3ec76d4931b3bc5ec4d90e2ebc0342567c9507abdfafa602fc6983f" + + "13f20eb26b4169dc3908109fe3c1887db4be8f30edad989dc8caa234f9818ac488b110ad30a30f769277168650b6910e", + BLS12381PointType.GT + ); + // GT mul by negative scalar. + CheckBls12381ScalarMul_Compat( + "0bdbfc3b68e7067630a1908de2ce15e1890d57b855ffc2ee0fe765293581c304d0507254fd9921d8ff4bff3185b1e8ae" + + "017091a6b9e243c3108b4302f30e2f4cb452c4574d23d06942cf915fb0b64c3546aa0bfbba5182dc42b63ebd09cd950f" + + "06ebf85ff360032e63d5422fed5969b80ed4abaf58d29317d9cf8e5a55744993ffc0ccc586a187c63f9c47d4b41870aa" + + "0fd73e13a4f7d3b072407a3bfa6539f8d56856542b17326ab77833df274e61a41c237a6dbf20a333698a675fded6ab1a" + + "114891795eabbedcb81590ff9bfb4b23b66c8b8376a69cf58511c80f3ac83d52c0c950be8c30d01108479f232d8e4e89" + + "19d869dc85db0b9d6ccf40eb8f8ab08e43a910c341737a55e751fa4a097ee82c5ac83d38c543d957bd9850af16039d1a" + + "00c96575d2ee24e9990b3401153446aa6593d3afb6ce7ca57d6432b8dda31aaa1a08834ad38deae5a807d11663adc5c2" + + "0ae7227a2cbb7917d1489175b89ed1ba415e4fc55b7d0a286caf2f5f40b0dd39cdd8fc8c271d8a7ae952fe6ece5f7c10" + + "19bfab0167af86314a73bfa37fd16bc6edff6d9ee75610a4eec1818c668ef9f509b1cdd54542e73dc0e343a4fd6e3bb6" + + "18540c1d060b60b63b645a895105425eb813b08b6ac91be3145da04040f2a45ffcf06e96b685519fca93b0f15238dc0e" + + "030c2199127ba82fa8a193f5f01ae24270e9669923653db38cae711d68169aa25df51a8915f3f8219892f4f5e67d550b" + + "00910011685017dcc1777a9d48689ce590d57c1fc942d49cfad0ed7efc0169a95d7e7378af26bafb90d1619bcdab64cd", + "688e58217305c1fd2fe0637cbd8e7414d4d0a2113314eb05592f97930d23b34d", + true, + "056fdc84f044148950c0b7c4c0613f5710fcaeb1b023b9d8f814dc39d48702db70ce41aa276566960e37237f22b086b0" + + "17b9ed0e264e2b7872c8a7affb8b9f847a528d092a038dab4ac58d3a33d30e2e5078b5e39ebb7441c56ae7556b63ecd6" + + "139ed9be1c5eb9f987cc704c913c1e23d44d2e04377347f6c471edc40cdb2cd4e32c396194363cd21ceff9bedbd164a4" + + "1050e701012f0456383210f8054e76c0906e3f37e10d4a3d6342e79e39d566ea785b385bb692cddbd6c16456dfabf19f" + + "0f84c27ec4bce096af0369ac070747cd89d97bc287afe5ed5e495ed2d743adbd8eec47df6c3a69628e803e23d8248458" + + "00e44a8d874756a7541128892e55e9df1d1fe0583ef967db6740617a9ff50766866c0fa631aed8639cd0c13d3d6f6f21" + + "0b340ee315caec4cc31c916d651db5e002e259fca081fb605258ccf692d786bd5bb45a054c4d8498ac2a7fa241870df6" + + "0ba0fd8a2b063740af11e7530db1e758a8e2858a443104b8337e18c083035768a0e93126f116bb9c50c8cebe30e0ceaa" + + "0c0b53eb2b6a1f96b34b6cc36f3417edda184e19ae1790d255337f14315323e1d2d7382b344bdc0b6b2cfab5837c24c9" + + "16640ca351539d5459389a9c7f9b0d79e04e4a8392e0c2495dcecf7d48b10c7043825b7c6709108d81856ebf98385f0d" + + "099e6521714c48b8eb5d2e97665375175f47c57d427d35a9dc44064a99d1c079028e36d34540baba947333ab3c8976b8" + + "01ea48578159f041e740ea5bf73c1de3c1043a6e03311d0f2463b72694249ccc5d603e4a93cfd8a6713fb0470383c23f", + BLS12381PointType.GT + ); + + // GT mul by zero scalar. + CheckBls12381ScalarMul_Compat( + "176ec726aa447f1791e69fc70a71103c84b17385094ef06a9a0235ac7241f6635377f55ad486c216c8701d61ea2ace3e" + + "05ca1605f238dc8f29f868b795e45645c6f7ff8d9d8ffd77b5e149b0325c2a8f24dde40e80a3381ae72a9a1104ef02d7" + + "0af7cf8f2fe6ff38961b352b0fde6f8536424fc9aa5805b8e12313bdfc01d5c1db1c0a37654c307fbd252c265dcbfc04" + + "0ee5605ffd6ac20aab15b0343e47831f4157a20ecedd7350d2cf070c0c7d423786fd97aa7236b99f4462fb23e1735288" + + "15bf2cf3ccbfc38303fa8154d70ee5e1e3158cbb14d5c87a773cbe948a5cfec2763c5e7129940906920aed344453b0f8" + + "01760fd3eac8e254ce8e0ae4edd30c914bea9e2935acd4a6a9d42d185a9a6e786c8e462b769b2112423f6591b0933477" + + "18897438ba918b9e4525888194b20ee17709f7dea319cfd053bb1c222783340326953fd3763eb6feaaa4d1458ee6ca00" + + "1818ad88222a97e43a71dca8d2abaef70657b9ff7b94ca422d0c50ddb4265fa35514ed534217ce2f0219c6985ec2827a" + + "0ee1dc17940926551072d693d89e36e6d14162f414b52587e5612ed4a562c9ac15df9d5fa68ccf61d52fea64b2f5d7a6" + + "00e0a8fa735105bc9a2ecb69b6d9161e55a4ccdc2285164c6846fa5bdc106d1e0693ebd5fe86432e5e88c55f0159ec32" + + "17332c8492332dfbd93970f002a6a05f23484e081f38815785e766779c843765d58b2444295a87939ad7f8fa4c11e853" + + "0a62426063c9a57cf3481a00372e443dc014fd6ef4723dd4636105d7ce7b96c4b2b3b641c3a2b6e0fa9be6187e5bfaf9", + "0000000000000000000000000000000000000000000000000000000000000000", + false, + string.Concat(Enumerable.Repeat("0", 1151)) + "1", + BLS12381PointType.GT + ); + // G1Affine mul by positive scalar. + CheckBls12381ScalarMul_Compat( + "a1f9855f7670a63e4c80d64dfe6ddedc2ed2bfaebae27e4da82d71ba474987a39808e8921d3df97df6e5d4b979234de8", + "8463159bd9a1d1e1fd815172177ec24c0c291353ed88b3d1838fd9d63b1efd0b", + false, + "ae85e3e2d677c9e3424ed79b5a7554262c3d6849202b84d2e7024e4b1f2e9dd3f7cf20b807a9f2a67d87e47e9e94d361", + BLS12381PointType.G1Proj + ); + // G1Affine mul by negative scalar. + CheckBls12381ScalarMul_Compat( + "a1f9855f7670a63e4c80d64dfe6ddedc2ed2bfaebae27e4da82d71ba474987a39808e8921d3df97df6e5d4b979234de8", + "8463159bd9a1d1e1fd815172177ec24c0c291353ed88b3d1838fd9d63b1efd0b", + true, + "8e85e3e2d677c9e3424ed79b5a7554262c3d6849202b84d2e7024e4b1f2e9dd3f7cf20b807a9f2a67d87e47e9e94d361", + BLS12381PointType.G1Proj + ); + // G1Affine mul by zero scalar. + CheckBls12381ScalarMul_Compat( + "a1f9855f7670a63e4c80d64dfe6ddedc2ed2bfaebae27e4da82d71ba474987a39808e8921d3df97df6e5d4b979234de8", + "0000000000000000000000000000000000000000000000000000000000000000", + false, + "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + BLS12381PointType.G1Proj + ); + // G2Affine mul by positive scalar. + CheckBls12381ScalarMul_Compat( + "a41e586fdd58d39616fea921a855e65417a5732809afc35e28466e3acaeed3d53dd4b97ca398b2f29bf6bbcaca026a66" + + "09a42bdeaaeef42813ae225e35c23c61c293e6ecb6759048fb76ac648ba3bc49f0fcf62f73fca38cdc5e7fa5bf511365", + "cbfffe3e37e53e31306addde1a1725641fbe88cd047ee7477966c44a3f764b47", + false, + "88ae9bba988e854877c66dfb7ff84aa5e107861aa51d1a2a8dac2414d716a7e219bc4b0239e4b12d2182f57b5eea8283" + + "0639f2e6713098ae8d4b4c3942f366614bac35c91c83ecb57fa90fe03094aca1ecd3555a7a6fdfa2417b5bb06917732e", + BLS12381PointType.G2Proj + ); + // G2Affine mul by negative scalar. + CheckBls12381ScalarMul_Compat( + "a41e586fdd58d39616fea921a855e65417a5732809afc35e28466e3acaeed3d53dd4b97ca398b2f29bf6bbcaca026a66" + + "09a42bdeaaeef42813ae225e35c23c61c293e6ecb6759048fb76ac648ba3bc49f0fcf62f73fca38cdc5e7fa5bf511365", + "cbfffe3e37e53e31306addde1a1725641fbe88cd047ee7477966c44a3f764b47", + true, + "a8ae9bba988e854877c66dfb7ff84aa5e107861aa51d1a2a8dac2414d716a7e219bc4b0239e4b12d2182f57b5eea8283" + + "0639f2e6713098ae8d4b4c3942f366614bac35c91c83ecb57fa90fe03094aca1ecd3555a7a6fdfa2417b5bb06917732e", + BLS12381PointType.G2Proj + ); + // G2Affine mul by negative scalar. + CheckBls12381ScalarMul_Compat( + "a41e586fdd58d39616fea921a855e65417a5732809afc35e28466e3acaeed3d53dd4b97ca398b2f29bf6bbcaca026a66" + + "09a42bdeaaeef42813ae225e35c23c61c293e6ecb6759048fb76ac648ba3bc49f0fcf62f73fca38cdc5e7fa5bf511365", + "0000000000000000000000000000000000000000000000000000000000000000", + false, + "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + BLS12381PointType.G2Proj + ); + } + + /// + /// Keccak256 cases are verified in https://emn178.github.io/online-tools/keccak_256.html + /// + [TestMethod] + public void TestKeccak256_HelloWorld() + { + // Arrange + byte[] inputData = "Hello, World!"u8.ToArray(); + string expectedHashHex = "acaf3289d7b601cbd114fb36c4d29c85bbfd5e133f14cb355c3fd8d99367964f"; + + // Act + byte[] outputData = CryptoLib.Keccak256(inputData); + string outputHashHex = Hex.ToHexString(outputData); + + // Assert + Assert.AreEqual(expectedHashHex, outputHashHex, "Keccak256 hash did not match expected value for 'Hello, World!'."); + } + [TestMethod] + public void TestKeccak256_Keccak() + { + // Arrange + byte[] inputData = "Keccak"u8.ToArray(); + string expectedHashHex = "868c016b666c7d3698636ee1bd023f3f065621514ab61bf26f062c175fdbe7f2"; + + // Act + byte[] outputData = CryptoLib.Keccak256(inputData); + string outputHashHex = Hex.ToHexString(outputData); + + // Assert + Assert.AreEqual(expectedHashHex, outputHashHex, "Keccak256 hash did not match expected value for 'Keccak'."); + } + + [TestMethod] + public void TestKeccak256_Cryptography() + { + // Arrange + byte[] inputData = "Cryptography"u8.ToArray(); + string expectedHashHex = "53d49d225dd2cfe77d8c5e2112bcc9efe77bea1c7aa5e5ede5798a36e99e2d29"; + + // Act + byte[] outputData = CryptoLib.Keccak256(inputData); + string outputHashHex = Hex.ToHexString(outputData); + + // Assert + Assert.AreEqual(expectedHashHex, outputHashHex, "Keccak256 hash did not match expected value for 'Cryptography'."); + } + + [TestMethod] + public void TestKeccak256_Testing123() + { + // Arrange + byte[] inputData = "Testing123"u8.ToArray(); + string expectedHashHex = "3f82db7b16b0818a1c6b2c6152e265f682d5ebcf497c9aad776ad38bc39cb6ca"; + + // Act + byte[] outputData = CryptoLib.Keccak256(inputData); + string outputHashHex = Hex.ToHexString(outputData); + + // Assert + Assert.AreEqual(expectedHashHex, outputHashHex, "Keccak256 hash did not match expected value for 'Testing123'."); + } + + [TestMethod] + public void TestKeccak256_LongString() + { + // Arrange + byte[] inputData = "This is a longer string for Keccak256 testing purposes."u8.ToArray(); + string expectedHashHex = "24115e5c2359f85f6840b42acd2f7ea47bc239583e576d766fa173bf711bdd2f"; + + // Act + byte[] outputData = CryptoLib.Keccak256(inputData); + string outputHashHex = Hex.ToHexString(outputData); + + // Assert + Assert.AreEqual(expectedHashHex, outputHashHex, "Keccak256 hash did not match expected value for the longer string."); + } + + [TestMethod] + public void TestKeccak256_BlankString() + { + // Arrange + byte[] inputData = ""u8.ToArray(); + string expectedHashHex = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; + + // Act + byte[] outputData = CryptoLib.Keccak256(inputData); + string outputHashHex = Hex.ToHexString(outputData); + + // Assert + Assert.AreEqual(expectedHashHex, outputHashHex, "Keccak256 hash did not match expected value for blank string."); + } + + // TestVerifyWithECDsa_CustomTxWitness_SingleSig builds custom witness verification script for single Koblitz public key + // and ensures witness check is passed for the following message: + // + // keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]) + // + // The proposed witness verification script has 110 bytes length, verification costs 2154270 * 10e-8GAS including Invocation script execution. + // The user has to sign the keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]). + [TestMethod] + public void TestVerifyWithECDsa_CustomTxWitness_SingleSig() + { + byte[] privkey = "7177f0d04c79fa0b8c91fe90c1cf1d44772d1fba6e5eb9b281a22cd3aafb51fe".HexToBytes(); + var pubHex = "04" + "fd0a8c1ce5ae5570fdd46e7599c16b175bf0ebdfe9c178f1ab848fb16dac74a5" + + "d301b0534c7bcf1b3760881f0c420d17084907edd771e1c9c8e941bbf6ff9108"; + ECPoint pubKey = ECPoint.Parse(pubHex, ECCurve.Secp256k1); + + // vrf is a builder of witness verification script corresponding to the public key. + using ScriptBuilder vrf = new(); + vrf.EmitPush((byte)NamedCurveHash.secp256k1Keccak256); // push Koblitz curve identifier and Keccak256 hasher. + vrf.Emit(OpCode.SWAP); // swap curve identifier with the signature. + vrf.EmitPush(pubKey.EncodePoint(true)); // emit the caller's public key. + + // Construct and push the signed message. The signed message is effectively the network-dependent transaction hash, + // i.e. msg = [4-network-magic-bytes-LE, tx-hash-BE] + // Firstly, retrieve network magic (it's uint32 wrapped into BigInteger and represented as Integer stackitem on stack). + vrf.EmitSysCall(ApplicationEngine.System_Runtime_GetNetwork); // push network magic (Integer stackitem), can have 0-5 bytes length serialized. + + // Convert network magic to 4-bytes-length LE byte array representation. + vrf.EmitPush(0x100000000); // push 0x100000000. + vrf.Emit(OpCode.ADD, // the result is some new number that is 5 bytes at least when serialized, but first 4 bytes are intact network value (LE). + OpCode.PUSH4, // cut the first 4 bytes out of a number that is at least 5 bytes long, + OpCode.LEFT); // the result is 4-bytes-length LE network representation. + + // Retrieve executing transaction hash. + vrf.EmitSysCall(ApplicationEngine.System_Runtime_GetScriptContainer); // push the script container (executing transaction, actually). + vrf.Emit(OpCode.PUSH0, OpCode.PICKITEM); // pick 0-th transaction item (the transaction hash). + + // Concatenate network magic and transaction hash. + vrf.Emit(OpCode.CAT); // this instruction will convert network magic to bytes using BigInteger rules of conversion. + + // Continue construction of 'verifyWithECDsa' call. + vrf.Emit(OpCode.PUSH4, OpCode.PACK); // pack arguments for 'verifyWithECDsa' call. + EmitAppCallNoArgs(vrf, NativeContract.CryptoLib.Hash, "verifyWithECDsa", CallFlags.None); // emit the call to 'verifyWithECDsa' itself. + + // Account is a hash of verification script. + var vrfScript = vrf.ToArray(); + var acc = vrfScript.ToScriptHash(); + + var tx = new Transaction + { + Attributes = [], + NetworkFee = 1_0000_0000, + Nonce = (uint)Environment.TickCount, + Script = new byte[Transaction.MaxTransactionSize / 100], + Signers = [new Signer { Account = acc }], + SystemFee = 0, + ValidUntilBlock = 10, + Version = 0, + Witnesses = [] + }; + var signData = tx.GetSignData(TestProtocolSettings.Default.Network); + var txSignature = Crypto.Sign(signData, privkey, ECCurve.Secp256k1, HashAlgorithm.Keccak256); + + // inv is a builder of witness invocation script corresponding to the public key. + using ScriptBuilder inv = new(); + inv.EmitPush(txSignature); // push signature. + + tx.Witnesses = + [ + new Witness { InvocationScript = inv.ToArray(), VerificationScript = vrfScript } + ]; + + Assert.AreEqual(VerifyResult.Succeed, tx.VerifyStateIndependent(TestProtocolSettings.Default)); + + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + // Create fake balance to pay the fees. + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default, gas: long.MaxValue); + _ = NativeContract.GAS.Mint(engine, acc, 5_0000_0000, false); + snapshotCache.Commit(); + + Assert.AreEqual(VerifyResult.Succeed, tx.VerifyStateDependent(TestProtocolSettings.Default, snapshotCache, new(), [])); + + // The resulting witness verification cost is 2154270 * 10e-8GAS. + // The resulting witness Invocation script (66 bytes length): + // NEO-VM > loadbase64 DEARoaaEjM/3VulrBDUod7eiZgWQS2iXIM0+I24iyJYmffhosZoQjfnnRymF/7+FaBPb9qvQwxLLSVo9ROlrdFdC + // READY: loaded 66 instructions + // NEO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 PUSHDATA1 11a1a6848ccff756e96b04352877b7a26605904b689720cd3e236e22c896267df868b19a108df9e7472985ffbf856813dbf6abd0c312cb495a3d44e96b745742 << + // + // + // The resulting witness verificaiton script (110 bytes): + // NEO-VM 0 > loadbase64: + // "ABhQDCEC/QqMHOWuVXD91G51mcFrF1vw69/pwXjxq4SPsW2sdKVBxfug4AMAAAAAAQAAAJ4UjUEtUQgwEM6LFMAfDA92ZXJpZnlX" + + // "aXRoRUNEc2EMFBv1dasRiWiEE2EKNaEohs3gtmxyQWJ9W1I=" + // READY: loaded 110 instructions + // NEO-VM 0 > pos + // Error: No help topic for 'pos' + // NEO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 PUSHINT8 122 (7a) << + // 2 SWAP + // 3 PUSHDATA1 02fd0a8c1ce5ae5570fdd46e7599c16b175bf0ebdfe9c178f1ab848fb16dac74a5 + // 38 SYSCALL System.Runtime.GetNetwork (c5fba0e0) + // 43 PUSHINT64 4294967296 (0000000001000000) + // 52 ADD + // 53 PUSH4 + // 54 LEFT + // 55 SYSCALL System.Runtime.GetScriptContainer (2d510830) + // 60 PUSH0 + // 61 PICKITEM + // 62 CAT + // 63 PUSH4 + // 64 PACK + // 65 PUSH0 + // 66 PUSHDATA1 766572696679576974684543447361 ("verifyWithECDsa") + // 83 PUSHDATA1 1bf575ab1189688413610a35a12886cde0b66c72 ("NNToUmdQBe5n8o53BTzjTFAnSEcpouyy3B", "0x726cb6e0cd8628a1350a611384688911ab75f51b") + // 105 SYSCALL System.Contract.Call (627d5b52) + } + + // TestVerifyWithECDsa_CustomTxWitness_MultiSig builds custom multisignature witness verification script for Koblitz public keys + // and ensures witness check is passed for the M out of N multisignature of message: + // + // keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]) + // + // The proposed witness verification script has 264 bytes length, verification costs 8390070 * 10e-8GAS including Invocation script execution. + // The users have to sign the keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]). + [TestMethod] + public void TestVerifyWithECDsa_CustomTxWitness_MultiSig() + { + var privkey1 = "b2dde592bfce654ef03f1ceea452d2b0112e90f9f52099bcd86697a2bd0a2b60".HexToBytes(); + var pubKey1 = ECPoint.Parse("04" + + "0486468683c112125978ffe876245b2006bfe739aca8539b67335079262cb27a" + + "d0dedc9e5583f99b61c6f46bf80b97eaec3654b87add0e5bd7106c69922a229d", ECCurve.Secp256k1); + + var privkey2 = "b9879e26941872ee6c9e6f01045681496d8170ed2cc4a54ce617b39ae1891b3a".HexToBytes(); + var pubKey2 = ECPoint.Parse("04" + + "0d26fc2ad3b1aae20f040b5f83380670f8ef5c2b2ac921ba3bdd79fd0af05251" + + "77715fd4370b1012ddd10579698d186ab342c223da3e884ece9cab9b6638c7bb", ECCurve.Secp256k1); + + var privkey3 = "4e1fe2561a6da01ee030589d504d62b23c26bfd56c5e07dfc9b8b74e4602832a".HexToBytes(); + var pubKey3 = ECPoint.Parse("04" + + "7b4e72ae854b6a0955b3e02d92651ab7fa641a936066776ad438f95bb674a269" + + "a63ff98544691663d91a6cfcd215831f01bfb7a226363a6c5c67ef14541dba07", ECCurve.Secp256k1); + + var privkey4 = "6dfd066bb989d3786043aa5c1f0476215d6f5c44f5fc3392dd15e2599b67a728".HexToBytes(); + var pubKey4 = ECPoint.Parse("04" + + "b62ac4c8a352a892feceb18d7e2e3a62c8c1ecbaae5523d89d747b0219276e22" + + "5be2556a137e0e806e4915762d816cdb43f572730d23bb1b1cba750011c4edc6", ECCurve.Secp256k1); + + // Public keys must be sorted, exactly like for standard CreateMultiSigRedeemScript. + var keys = new List<(byte[], ECPoint)> + { + (privkey1, pubKey1), + (privkey2, pubKey2), + (privkey3, pubKey3), + (privkey4, pubKey4), + }.OrderBy(k => k.Item2).ToList(); + + // Consider 4 users willing to sign 3/4 multisignature transaction with their Secp256k1 private keys. + var m = 3; + var n = keys.Count; + + // Must ensure the following conditions are met before verification script construction: + Assert.IsGreaterThan(0, n); + Assert.IsLessThanOrEqualTo(n, m); + Assert.AreEqual(n, keys.Select(k => k.Item2).Distinct().Count()); + + // In fact, the following algorithm is implemented via NeoVM instructions: + // + // func Check(sigs []interop.Signature) bool { + // if m != len(sigs) { + // return false + // } + // var pubs []interop.PublicKey = []interop.PublicKey{...} + // msg := append(convert.ToBytes(runtime.GetNetwork()), runtime.GetScriptContainer().Hash...) + // var sigCnt = 0 + // var pubCnt = 0 + // for ; sigCnt < m && pubCnt < n; { // sigs must be sorted by pub + // sigCnt += crypto.VerifyWithECDsa(msg, pubs[pubCnt], sigs[sigCnt], crypto.Secp256k1Keccak256) + // pubCnt++ + // } + // return sigCnt == m + // } + + // vrf is a builder of M out of N multisig witness verification script corresponding to the public keys. + using ScriptBuilder vrf = new(); + + // Start the same way as regular multisig script. + vrf.EmitPush(m); // push m. + foreach (var tuple in keys) + { + vrf.EmitPush(tuple.Item2.EncodePoint(true)); // push public keys in compressed form. + } + vrf.EmitPush(n); // push n. + + // Initialize slots for local variables. Locals slot scheme: + // LOC0 -> sigs + // LOC1 -> pubs + // LOC2 -> msg (ByteString) + // LOC3 -> sigCnt (Integer) + // LOC4 -> pubCnt (Integer) + // LOC5 -> n + // LOC6 -> m + vrf.Emit(OpCode.INITSLOT, new ReadOnlySpan([7, 0])); // 7 locals, no args. + + // Store n. + vrf.Emit(OpCode.STLOC5); + + // Pack public keys and store at LOC1. + vrf.Emit(OpCode.LDLOC5, // load n. + OpCode.PACK, OpCode.STLOC1); // pack pubs and store. + + // Store m. + vrf.Emit(OpCode.STLOC6); + + // Check the number of signatures is m. Abort the execution if not. + vrf.Emit(OpCode.DEPTH); // push the number of signatures onto stack. + vrf.Emit(OpCode.LDLOC6); // load m. + vrf.Emit(OpCode.JMPEQ, new ReadOnlySpan([0])); // here and below short jumps are sufficient. Offset will be filled later. + var sigsLenCheckEndOffset = vrf.Length; + vrf.Emit(OpCode.ABORT); // abort the execution if length of the signatures not equal to m. + + // Start the verification itself. + var checkStartOffset = vrf.Length; + + // Pack signatures and store at LOC0. + vrf.Emit(OpCode.LDLOC6); // load m. + vrf.Emit(OpCode.PACK, OpCode.STLOC0); + + // Get message and store it at LOC2. + // msg = [4-network-magic-bytes-LE, tx-hash-BE] + vrf.EmitSysCall(ApplicationEngine.System_Runtime_GetNetwork); // push network magic (Integer stackitem), can have 0-5 bytes length serialized. + // Convert network magic to 4-bytes-length LE byte array representation. + vrf.EmitPush(0x100000000); // push 0x100000000. + vrf.Emit(OpCode.ADD, // the result is some new number that is 5 bytes at least when serialized, but first 4 bytes are intact network value (LE). + OpCode.PUSH4, // cut the first 4 bytes out of a number that is at least 5 bytes long, + OpCode.LEFT); // the result is 4-bytes-length LE network representation. + // Retrieve executing transaction hash. + vrf.EmitSysCall(ApplicationEngine.System_Runtime_GetScriptContainer); // push the script container (executing transaction, actually). + vrf.Emit(OpCode.PUSH0, OpCode.PICKITEM); // pick 0-th transaction item (the transaction hash). + // Concatenate network magic and transaction hash. + vrf.Emit(OpCode.CAT); // this instruction will convert network magic to bytes using BigInteger rules of conversion. + vrf.Emit(OpCode.STLOC2); // store msg as a local variable #2. + + // Initialize local variables: sigCnt, pubCnt. + vrf.Emit(OpCode.PUSH0, OpCode.STLOC3, // initialize sigCnt. + OpCode.PUSH0, OpCode.STLOC4); // initialize pubCnt. + + // Loop condition check. + var loopStartOffset = vrf.Length; + vrf.Emit(OpCode.LDLOC3); // load sigCnt. + vrf.Emit(OpCode.LDLOC6); // load m. + vrf.Emit(OpCode.GE, // sigCnt >= m + OpCode.LDLOC4); // load pubCnt + vrf.Emit(OpCode.LDLOC5); // load n. + vrf.Emit(OpCode.GE, // pubCnt >= n + OpCode.OR); // sigCnt >= m || pubCnt >= n + vrf.Emit(OpCode.JMPIF, new ReadOnlySpan([0])); // jump to the end of the script if (sigCnt >= m || pubCnt >= n). + var loopConditionOffset = vrf.Length; + + // Loop start. Prepare arguments and call CryptoLib's verifyWithECDsa. + vrf.EmitPush((byte)NamedCurveHash.secp256k1Keccak256); // push Koblitz curve identifier and Keccak256 hasher. + vrf.Emit(OpCode.LDLOC0, // load signatures. + OpCode.LDLOC3, // load sigCnt. + OpCode.PICKITEM, // pick signature at index sigCnt. + OpCode.LDLOC1, // load pubs. + OpCode.LDLOC4, // load pubCnt. + OpCode.PICKITEM, // pick pub at index pubCnt. + OpCode.LDLOC2, // load msg. + OpCode.PUSH4, OpCode.PACK); // pack 4 arguments for 'verifyWithECDsa' call. + EmitAppCallNoArgs(vrf, NativeContract.CryptoLib.Hash, "verifyWithECDsa", CallFlags.None); // emit the call to 'verifyWithECDsa' itself. + + // Update loop variables. + vrf.Emit(OpCode.LDLOC3, OpCode.ADD, OpCode.STLOC3, // increment sigCnt if signature is valid. + OpCode.LDLOC4, OpCode.INC, OpCode.STLOC4); // increment pubCnt. + + // End of the loop. + vrf.Emit(OpCode.JMP, new ReadOnlySpan([0])); // jump to the start of cycle. + var loopEndOffset = vrf.Length; + // Return condition: the number of valid signatures should be equal to m. + var progRetOffset = vrf.Length; + vrf.Emit(OpCode.LDLOC3); // load sigCnt. + vrf.Emit(OpCode.LDLOC6); // load m. + vrf.Emit(OpCode.NUMEQUAL); // push m == sigCnt. + + var vrfScript = vrf.ToArray(); + + // Set JMP* instructions offsets. "-1" is for short JMP parameter offset. JMP parameters + // are relative offsets. + vrfScript[sigsLenCheckEndOffset - 1] = (byte)(checkStartOffset - sigsLenCheckEndOffset + 2); + vrfScript[loopEndOffset - 1] = (byte)(loopStartOffset - loopEndOffset + 2); + vrfScript[loopConditionOffset - 1] = (byte)(progRetOffset - loopConditionOffset + 2); + + // Account is a hash of verification script. + var acc = vrfScript.ToScriptHash(); + + var tx = new Transaction + { + Attributes = [], + NetworkFee = 1_0000_0000, + Nonce = (uint)Environment.TickCount, + Script = new byte[Transaction.MaxTransactionSize / 100], + Signers = [new Signer { Account = acc }], + SystemFee = 0, + ValidUntilBlock = 10, + Version = 0, + Witnesses = [] + }; + // inv is a builder of witness invocation script corresponding to the public key. + using ScriptBuilder inv = new(); + for (var i = 0; i < n; i++) + { + if (i == 1) // Skip one key since we need only 3 signatures. + continue; + var signData = tx.GetSignData(TestProtocolSettings.Default.Network); + var sig = Crypto.Sign(signData, keys[i].Item1, ECCurve.Secp256k1, HashAlgorithm.Keccak256); + inv.EmitPush(sig); + } + + tx.Witnesses = + [ + new Witness { InvocationScript = inv.ToArray(), VerificationScript = vrfScript } + ]; + + Assert.AreEqual(VerifyResult.Succeed, tx.VerifyStateIndependent(TestProtocolSettings.Default)); + + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + // Create fake balance to pay the fees. + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, + settings: TestProtocolSettings.Default, gas: long.MaxValue); + _ = NativeContract.GAS.Mint(engine, acc, 5_0000_0000, false); + + // We should not use commit here cause once its committed, the value we get from the snapshot can be different + // from the underline storage. Thought there isn't any issue triggered here, its wrong to use it this way. + // We should either ignore the commit, or get a new snapshot of the store after the commit. + // snapshot.Commit(); + + // Check that witness verification passes. + var txVrfContext = new TransactionVerificationContext(); + var conflicts = new List(); + Assert.AreEqual(VerifyResult.Succeed, tx.VerifyStateDependent(TestProtocolSettings.Default, snapshotCache, txVrfContext, conflicts)); + + // The resulting witness verification cost for 3/4 multisig is 8389470 * 10e-8GAS. Cost depends on M/N. + // The resulting witness Invocation script (198 bytes for 3 signatures): + // NEO-VM 0 > loadbase64" + // "DEDM23XByPvDK9XRAHRhfGH7/Mp5jdaci3/GpTZ3D9SZx2Zw89tAaOtmQSIutXbCxRQA1kSeUD4AteJGoNXFhFzIDECgeHoey0rY" + + // "dlFyTVfDJSsuS+VwzC5OtYGCVR2V/MttmLXWA/FWZH/MjmU0obgQXa9zoBxqYQUUJKefivZFxVcTDEAZT6L6ZFybeXbm8+RlVNS7" + + // "KshusT54d2ImQ6vFvxETphhJOwcQ0yNL6qJKsrLAKAnzicY4az3ct0G35mI17/gQ" + // READY: loaded 198 instructions + // NEO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 PUSHDATA1 ccdb75c1c8fbc32bd5d10074617c61fbfcca798dd69c8b7fc6a536770fd499c76670f3db4068eb6641222eb576c2c51400d6449e503e00b5e246a0d5c5845cc8 << + // 66 PUSHDATA1 a0787a1ecb4ad87651724d57c3252b2e4be570cc2e4eb58182551d95fccb6d98b5d603f156647fcc8e6534a1b8105daf73a01c6a61051424a79f8af645c55713 + // 132 PUSHDATA1 194fa2fa645c9b7976e6f3e46554d4bb2ac86eb13e7877622643abc5bf1113a618493b0710d3234beaa24ab2b2c02809f389c6386b3ddcb741b7e66235eff810 + // + // + // Resulting witness verification script (266 bytes for 3/4 multisig): + // NEO-VM 0 > loadbase64: + // "EwwhAwSGRoaDwRISWXj/6HYkWyAGv+c5rKhTm2czUHkmLLJ6DCEDDSb8KtOxquIPBAtfgzgGcPjvXCsqySG6O915/QrwUlEMIQN7" + + // "TnKuhUtqCVWz4C2SZRq3+mQak2Bmd2rUOPlbtnSiaQwhArYqxMijUqiS/s6xjX4uOmLIwey6rlUj2J10ewIZJ24iFFcHAHVtwHF2" + + // "Q24oAzhuwHBBxfug4AMAAAAAAQAAAJ4UjUEtUQgwEM6LchBzEHRrbrhsbbiSJEIAGGhrzmlszmoUwB8MD3ZlcmlmeVdpdGhFQ0R" + + // "zYQwUG/V1qxGJaIQTYQo1oSiGzeC2bHJBYn1bUmuec2ycdCK5a26z" + // READY: loaded 264 instructions + // NEO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 PUSH3 << + // 1 PUSHDATA1 030486468683c112125978ffe876245b2006bfe739aca8539b67335079262cb27a + // 36 PUSHDATA1 030d26fc2ad3b1aae20f040b5f83380670f8ef5c2b2ac921ba3bdd79fd0af05251 + // 71 PUSHDATA1 037b4e72ae854b6a0955b3e02d92651ab7fa641a936066776ad438f95bb674a269 + // 106 PUSHDATA1 02b62ac4c8a352a892feceb18d7e2e3a62c8c1ecbaae5523d89d747b0219276e22 + // 141 PUSH4 + // 142 INITSLOT 7 local, 0 arg + // 145 STLOC5 + // 146 LDLOC5 + // 147 PACK + // 148 STLOC1 + // 149 STLOC6 + // 150 DEPTH + // 151 LDLOC6 + // 152 JMPEQ 155 (3/03) + // 154 ABORT + // 155 LDLOC6 + // 156 PACK + // 157 STLOC0 + // 158 SYSCALL System.Runtime.GetNetwork (c5fba0e0) + // 163 PUSHINT64 4294967296 (0000000001000000) + // 172 ADD + // 173 PUSH4 + // 174 LEFT + // 175 SYSCALL System.Runtime.GetScriptContainer (2d510830) + // 180 PUSH0 + // 181 PICKITEM + // 182 CAT + // 183 STLOC2 + // 184 PUSH0 + // 185 STLOC3 + // 186 PUSH0 + // 187 STLOC4 + // 188 LDLOC3 + // 189 LDLOC6 + // 190 GE + // 191 LDLOC4 + // 192 LDLOC5 + // 193 GE + // 194 OR + // 195 JMPIF 261 (66/42) + // 197 PUSHINT8 122 (7a) + // 199 LDLOC0 + // 200 LDLOC3 + // 201 PICKITEM + // 202 LDLOC1 + // 203 LDLOC4 + // 204 PICKITEM + // 205 LDLOC2 + // 206 PUSH4 + // 207 PACK + // 208 PUSH0 + // 209 PUSHDATA1 766572696679576974684543447361 ("verifyWithECDsa") + // 226 PUSHDATA1 1bf575ab1189688413610a35a12886cde0b66c72 ("NNToUmdQBe5n8o53BTzjTFAnSEcpouyy3B", "0x726cb6e0cd8628a1350a611384688911ab75f51b") + // 248 SYSCALL System.Contract.Call (627d5b52) + // 253 LDLOC3 + // 254 ADD + // 255 STLOC3 + // 256 LDLOC4 + // 257 INC + // 258 STLOC4 + // 259 JMP 188 (-71/b9) + // 261 LDLOC3 + // 262 LDLOC6 + // 263 NUMEQUAL + } + + // EmitAppCallNoArgs is a helper method that emits all parameters of System.Contract.Call interop + // except the method arguments. + private static ScriptBuilder EmitAppCallNoArgs(ScriptBuilder builder, UInt160 contractHash, string method, CallFlags f) + { + builder.EmitPush((byte)f); + builder.EmitPush(method); + builder.EmitPush(contractHash); + builder.EmitSysCall(ApplicationEngine.System_Contract_Call); + return builder; + } + + [TestMethod] + public void TestVerifyWithECDsa() + { + byte[] privR1 = "6e63fda41e9e3aba9bb5696d58a75731f044a9bdc48fe546da571543b2fa460e".HexToBytes(); + ECPoint pubR1 = ECPoint.Parse("04" + + "cae768e1cf58d50260cab808da8d6d83d5d3ab91eac41cdce577ce5862d73641" + + "3643bdecd6d21c3b66f122ab080f9219204b10aa8bbceb86c1896974768648f3", ECCurve.Secp256r1); + + byte[] privK1 = "0b5fb3a050385196b327be7d86cbce6e40a04c8832445af83ad19c82103b3ed9".HexToBytes(); + ECPoint pubK1 = ECPoint.Parse("04" + + "b6363b353c3ee1620c5af58594458aa00abf43a6d134d7c4cb2d901dc0f474fd" + + "74c94740bd7169aa0b1ef7bc657e824b1d7f4283c547e7ec18c8576acf84418a", ECCurve.Secp256k1); + + byte[] message = Encoding.Default.GetBytes("HelloWorld"); + + // secp256r1 + SHA256 + byte[] signature = Crypto.Sign(message, privR1, ECCurve.Secp256r1, HashAlgorithm.SHA256); + Assert.IsTrue(Crypto.VerifySignature(message, signature, pubR1)); // SHA256 hash is used by default. + Assert.IsTrue(CallVerifyWithECDsa(message, pubR1, signature, NamedCurveHash.secp256r1SHA256)); + + // secp256r1 + Keccak256 + signature = Crypto.Sign(message, privR1, ECCurve.Secp256r1, HashAlgorithm.Keccak256); + Assert.IsTrue(Crypto.VerifySignature(message, signature, pubR1, HashAlgorithm.Keccak256)); + Assert.IsTrue(CallVerifyWithECDsa(message, pubR1, signature, NamedCurveHash.secp256r1Keccak256)); + + // secp256k1 + SHA256 + signature = Crypto.Sign(message, privK1, ECCurve.Secp256k1, HashAlgorithm.SHA256); + Assert.IsTrue(Crypto.VerifySignature(message, signature, pubK1)); // SHA256 hash is used by default. + Assert.IsTrue(CallVerifyWithECDsa(message, pubK1, signature, NamedCurveHash.secp256k1SHA256)); + + // secp256k1 + Keccak256 + signature = Crypto.Sign(message, privK1, ECCurve.Secp256k1, HashAlgorithm.Keccak256); + Assert.IsTrue(Crypto.VerifySignature(message, signature, pubK1, HashAlgorithm.Keccak256)); + Assert.IsTrue(CallVerifyWithECDsa(message, pubK1, signature, NamedCurveHash.secp256k1Keccak256)); + } + + [TestMethod] + public void TestVerifyWithECDsaInvalidParameters() + { + var message = "hello world"u8.ToArray(); + var privateKey = "6e63fda41e9e3aba9bb5696d58a75731f044a9bdc48fe546da571543b2fa460e".HexToBytes(); + var publicKey = ECPoint.Parse("04" + + "cae768e1cf58d50260cab808da8d6d83d5d3ab91eac41cdce577ce5862d73641" + + "3643bdecd6d21c3b66f122ab080f9219204b10aa8bbceb86c1896974768648f3", ECCurve.Secp256r1); + + var sign = Crypto.Sign(message, privateKey, ECCurve.Secp256r1, HashAlgorithm.SHA256); + + // IndexOutOfRangeException, but should be FormatException + Assert.ThrowsExactly(() => CryptoLib.VerifyWithECDsa(message, null!, sign, NamedCurveHash.secp256r1SHA256)); + + // IndexOutOfRangeException, but should be FormatException + Assert.ThrowsExactly(() => CryptoLib.VerifyWithECDsa(message, [], sign, NamedCurveHash.secp256r1SHA256)); + + // KeyNotFoundException, but should be ArgumentException + Assert.ThrowsExactly(() => CryptoLib.VerifyWithECDsa(message, [], sign, (NamedCurveHash)99)); + + // FormatException if the signature is empty + Assert.ThrowsExactly(() => CryptoLib.VerifyWithECDsa(message, [0x01], sign, NamedCurveHash.secp256r1SHA256)); + + Assert.ThrowsExactly(() => CryptoLib.VerifyWithECDsa(message, publicKey.EncodePoint(true), [], NamedCurveHash.secp256r1SHA256)); + + bool ok = CryptoLib.VerifyWithECDsa(message, publicKey.EncodePoint(true), sign, NamedCurveHash.secp256r1SHA256); + Assert.IsTrue(ok); + + ok = CryptoLib.VerifyWithECDsa(message, publicKey.EncodePoint(false), sign, NamedCurveHash.secp256r1SHA256); + Assert.IsTrue(ok); + + Assert.ThrowsExactly(() => CryptoLib.VerifyWithECDsa(message, publicKey.EncodePoint(false), sign, NamedCurveHash.secp256k1SHA256)); + + // ArithmeticException, but should be ArgumentException + byte[] invalidPublicKey = [0x03, .. Enumerable.Repeat(0x03, 32)]; + Assert.ThrowsExactly(() => CryptoLib.VerifyWithECDsa(message, invalidPublicKey, sign, NamedCurveHash.secp256k1SHA256)); + + // null messsage and signature is valid, result is true + sign = Crypto.Sign([], privateKey, ECCurve.Secp256r1, HashAlgorithm.SHA256); + ok = CryptoLib.VerifyWithECDsa(null!, publicKey.EncodePoint(true), sign, NamedCurveHash.secp256r1SHA256); + Assert.IsTrue(ok); + } + + private static bool CallVerifyWithECDsa(byte[] message, ECPoint pub, byte[] signature, NamedCurveHash curveHash) + { + var snapshot = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitPush((int)curveHash); + script.EmitPush(signature); + script.EmitPush(pub.EncodePoint(true)); + script.EmitPush(message); + script.EmitPush(4); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush("verifyWithECDsa"); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + return engine.ResultStack.Pop().GetBoolean(); + } + + [TestMethod] + public void TestVerifyWithEd25519() + { + // byte[] privateKey = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60".HexToBytes(); + byte[] publicKey = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a".HexToBytes(); + byte[] message = Array.Empty(); + byte[] signature = ("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155" + + "5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b").HexToBytes(); + + // Verify using Ed25519 directly + Assert.IsTrue(Ed25519.Verify(publicKey, message, signature)); + + // Verify using CryptoLib.VerifyWithEd25519 + Assert.IsTrue(CallVerifyWithEd25519(message, publicKey, signature)); + + // Test with a different message + byte[] differentMessage = Encoding.UTF8.GetBytes("Different message"); + Assert.IsFalse(CallVerifyWithEd25519(differentMessage, publicKey, signature)); + + // Test with an invalid signature + byte[] invalidSignature = new byte[signature.Length]; + Array.Copy(signature, invalidSignature, signature.Length); + invalidSignature[0] ^= 0x01; // Flip one bit + Assert.IsFalse(CallVerifyWithEd25519(message, publicKey, invalidSignature)); + + // Test with an invalid public key + byte[] invalidPublicKey = new byte[publicKey.Length]; + Array.Copy(publicKey, invalidPublicKey, publicKey.Length); + invalidPublicKey[0] ^= 0x01; // Flip one bit + Assert.ThrowsExactly(() => CallVerifyWithEd25519(message, invalidPublicKey, signature)); + } + + [TestMethod] + public void TestVerifyWithEd25519InvalidParameters() + { + var message = "hello world"u8.ToArray(); + var privateKey = Ed25519.GenerateKeyPair(); + var publicKey = Ed25519.GetPublicKey(privateKey); + var sign = Ed25519.Sign(privateKey, message); + + Assert.ThrowsExactly(() => CryptoLib.VerifyWithEd25519(message, [], sign)); + + Assert.ThrowsExactly(() => CryptoLib.VerifyWithEd25519(message, publicKey, [])); + + bool ok = CryptoLib.VerifyWithEd25519(message, publicKey, sign); + Assert.IsTrue(ok); + } + + private static bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] signature) + { + var snapshot = TestBlockchain.GetTestSnapshotCache(); + using ScriptBuilder script = new(); + script.EmitPush(signature); + script.EmitPush(publicKey); + script.EmitPush(message); + script.EmitPush(3); + script.Emit(OpCode.PACK); + script.EmitPush(CallFlags.All); + script.EmitPush("verifyWithEd25519"); + script.EmitPush(NativeContract.CryptoLib.Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, + settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + if (engine.Execute() != VMState.HALT) + throw new InvalidOperationException(null, engine.FaultException); + return engine.ResultStack.Pop().GetBoolean(); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_FungibleToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_FungibleToken.cs new file mode 100644 index 0000000000..c7efd8836f --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_FungibleToken.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_FungibleToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.MsTest; +using Neo.SmartContract.Native; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_FungibleToken : TestKit +{ + [TestMethod] + public void TestTotalSupply() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + Assert.AreEqual(5200000050000000, NativeContract.GAS.TotalSupply(snapshotCache)); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs new file mode 100644 index 0000000000..7ef203bd80 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs @@ -0,0 +1,158 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_GasToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.UnitTests.Extensions; +using System.Numerics; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_GasToken +{ + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + } + + [TestMethod] + public void Check_Name() => Assert.AreEqual(nameof(GasToken), NativeContract.GAS.Name); + + [TestMethod] + public void Check_Symbol() => Assert.AreEqual("GAS", NativeContract.GAS.Symbol(_snapshotCache)); + + [TestMethod] + public void Check_Decimals() => Assert.AreEqual(8, NativeContract.GAS.Decimals(_snapshotCache)); + + [TestMethod] + public async Task Check_BalanceOfTransferAndBurn() + { + var snapshot = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + byte[] from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray(); + byte[] to = new byte[20]; + var supply = NativeContract.GAS.TotalSupply(snapshot); + Assert.AreEqual(5200000050000000, supply); // 3000000000000000 + 50000000 (neo holder reward) + + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + snapshot.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + var keyCount = snapshot.GetChangeSet().Count(); + // Check unclaim + + var unclaim = UT_NeoToken.Check_UnclaimedGas(snapshot, from, persistingBlock); + Assert.AreEqual(new BigInteger(0.5 * 1000 * 100000000L), unclaim.Value); + Assert.IsTrue(unclaim.State); + + // Transfer + + Assert.IsTrue(NativeContract.NEO.Transfer(snapshot, from, to, BigInteger.Zero, true, persistingBlock)); + Assert.ThrowsExactly(() => _ = NativeContract.NEO.Transfer(snapshot, from, null, BigInteger.Zero, true, persistingBlock)); + Assert.ThrowsExactly(() => _ = NativeContract.NEO.Transfer(snapshot, null, to, BigInteger.Zero, false, persistingBlock)); + Assert.AreEqual(100000000, NativeContract.NEO.BalanceOf(snapshot, from)); + Assert.AreEqual(0, NativeContract.NEO.BalanceOf(snapshot, to)); + + Assert.AreEqual(52000500_00000000, NativeContract.GAS.BalanceOf(snapshot, from)); + Assert.AreEqual(0, NativeContract.GAS.BalanceOf(snapshot, to)); + + // Check unclaim + + unclaim = UT_NeoToken.Check_UnclaimedGas(snapshot, from, persistingBlock); + Assert.AreEqual(new BigInteger(0), unclaim.Value); + Assert.IsTrue(unclaim.State); + + supply = NativeContract.GAS.TotalSupply(snapshot); + Assert.AreEqual(5200050050000000, supply); + + Assert.AreEqual(keyCount + 3, snapshot.GetChangeSet().Count()); // Gas + + // Transfer + + keyCount = snapshot.GetChangeSet().Count(); + + Assert.IsFalse(NativeContract.GAS.Transfer(snapshot, from, to, 52000500_00000000, false, persistingBlock)); // Not signed + Assert.IsFalse(NativeContract.GAS.Transfer(snapshot, from, to, 52000500_00000001, true, persistingBlock)); // More than balance + Assert.IsTrue(NativeContract.GAS.Transfer(snapshot, from, to, 52000500_00000000, true, persistingBlock)); // All balance + + // Balance of + + Assert.AreEqual(52000500_00000000, NativeContract.GAS.BalanceOf(snapshot, to)); + Assert.AreEqual(0, NativeContract.GAS.BalanceOf(snapshot, from)); + + Assert.AreEqual(keyCount + 1, snapshot.GetChangeSet().Count()); // All + + // Burn + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default, gas: 0); + engine.LoadScript(Array.Empty()); + + await Assert.ThrowsExactlyAsync(async () => + await NativeContract.GAS.Burn(engine, new UInt160(to), BigInteger.MinusOne)); + + // Burn more than expected + + await Assert.ThrowsExactlyAsync(async () => + await NativeContract.GAS.Burn(engine, new UInt160(to), new BigInteger(52000500_00000001))); + + // Real burn + + await NativeContract.GAS.Burn(engine, new UInt160(to), new BigInteger(1)); + + Assert.AreEqual(5200049999999999, NativeContract.GAS.BalanceOf(engine.SnapshotCache, to)); + + Assert.AreEqual(2, engine.SnapshotCache.GetChangeSet().Count()); + + // Burn all + await NativeContract.GAS.Burn(engine, new UInt160(to), new BigInteger(5200049999999999)); + + Assert.AreEqual(keyCount - 2, engine.SnapshotCache.GetChangeSet().Count()); + + // Bad inputs + + Assert.ThrowsExactly(() => _ = NativeContract.GAS.Transfer(engine.SnapshotCache, from, to, BigInteger.MinusOne, true, persistingBlock)); + Assert.ThrowsExactly(() => _ = NativeContract.GAS.Transfer(engine.SnapshotCache, new byte[19], to, BigInteger.One, false, persistingBlock)); + Assert.ThrowsExactly(() => _ = NativeContract.GAS.Transfer(engine.SnapshotCache, from, new byte[19], BigInteger.One, false, persistingBlock)); + } + + internal static StorageKey CreateStorageKey(byte prefix, uint key) + { + return CreateStorageKey(prefix, BitConverter.GetBytes(key)); + } + + internal static StorageKey CreateStorageKey(byte prefix, byte[]? key = null) + { + byte[] buffer = GC.AllocateUninitializedArray(sizeof(byte) + (key?.Length ?? 0)); + buffer[0] = prefix; + key?.CopyTo(buffer.AsSpan(1)); + return new() + { + Id = NativeContract.GAS.Id, + Key = buffer + }; + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs new file mode 100644 index 0000000000..248c201f6c --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs @@ -0,0 +1,220 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NativeContract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.VM; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_NativeContract +{ + private DataCache _snapshotCache = null!; + /// + /// _nativeStates contains a mapping from native contract name to expected native contract state + /// constructed with all hardforks enabled and marshalled in JSON. + /// + private Dictionary _nativeStates = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + _nativeStates = new Dictionary + { + {"ContractManagement", """{"id":-1,"updatecounter":0,"hash":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":3581846399},"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":7,"safe":false},{"name":"destroy","parameters":[],"returntype":"Void","offset":14,"safe":false},{"name":"getContract","parameters":[{"name":"hash","type":"Hash160"}],"returntype":"Array","offset":21,"safe":true},{"name":"getContractById","parameters":[{"name":"id","type":"Integer"}],"returntype":"Array","offset":28,"safe":true},{"name":"getContractHashes","parameters":[],"returntype":"InteropInterface","offset":35,"safe":true},{"name":"getMinimumDeploymentFee","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"hasMethod","parameters":[{"name":"hash","type":"Hash160"},{"name":"method","type":"String"},{"name":"pcount","type":"Integer"}],"returntype":"Boolean","offset":49,"safe":true},{"name":"isContract","parameters":[{"name":"hash","type":"Hash160"}],"returntype":"Boolean","offset":56,"safe":true},{"name":"setMinimumDeploymentFee","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":63,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Void","offset":70,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","offset":77,"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}}""" }, + {"StdLib", """{"id":-2,"updatecounter":0,"hash":"0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":2426471238},"manifest":{"name":"StdLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"atoi","parameters":[{"name":"value","type":"String"}],"returntype":"Integer","offset":0,"safe":true},{"name":"atoi","parameters":[{"name":"value","type":"String"},{"name":"base","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"base58CheckDecode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":14,"safe":true},{"name":"base58CheckEncode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":21,"safe":true},{"name":"base58Decode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":28,"safe":true},{"name":"base58Encode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":35,"safe":true},{"name":"base64Decode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"base64Encode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":49,"safe":true},{"name":"base64UrlDecode","parameters":[{"name":"s","type":"String"}],"returntype":"String","offset":56,"safe":true},{"name":"base64UrlEncode","parameters":[{"name":"data","type":"String"}],"returntype":"String","offset":63,"safe":true},{"name":"deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"Any","offset":70,"safe":true},{"name":"hexDecode","parameters":[{"name":"str","type":"String"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"hexEncode","parameters":[{"name":"bytes","type":"ByteArray"}],"returntype":"String","offset":84,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"}],"returntype":"String","offset":91,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"},{"name":"base","type":"Integer"}],"returntype":"String","offset":98,"safe":true},{"name":"jsonDeserialize","parameters":[{"name":"json","type":"ByteArray"}],"returntype":"Any","offset":105,"safe":true},{"name":"jsonSerialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":112,"safe":true},{"name":"memoryCompare","parameters":[{"name":"str1","type":"ByteArray"},{"name":"str2","type":"ByteArray"}],"returntype":"Integer","offset":119,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"}],"returntype":"Integer","offset":126,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"}],"returntype":"Integer","offset":133,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"},{"name":"backward","type":"Boolean"}],"returntype":"Integer","offset":140,"safe":true},{"name":"serialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":147,"safe":true},{"name":"strLen","parameters":[{"name":"str","type":"String"}],"returntype":"Integer","offset":154,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"}],"returntype":"Array","offset":161,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"},{"name":"removeEmptyEntries","type":"Boolean"}],"returntype":"Array","offset":168,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"CryptoLib", """{"id":-3,"updatecounter":0,"hash":"0x726cb6e0cd8628a1350a611384688911ab75f51b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":174904780},"manifest":{"name":"CryptoLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"bls12381Add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":0,"safe":true},{"name":"bls12381Deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"InteropInterface","offset":7,"safe":true},{"name":"bls12381Equal","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"Boolean","offset":14,"safe":true},{"name":"bls12381Mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"bls12381Pairing","parameters":[{"name":"g1","type":"InteropInterface"},{"name":"g2","type":"InteropInterface"}],"returntype":"InteropInterface","offset":28,"safe":true},{"name":"bls12381Serialize","parameters":[{"name":"g","type":"InteropInterface"}],"returntype":"ByteArray","offset":35,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":49,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":70,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":77,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":84,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"LedgerContract", """{"id":-4,"updatecounter":0,"hash":"0xda65b600f7124ce6c79950c1772a36403104f2be","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"LedgerContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"currentHash","parameters":[],"returntype":"Hash256","offset":0,"safe":true},{"name":"currentIndex","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlock","parameters":[{"name":"indexOrHash","type":"ByteArray"}],"returntype":"Array","offset":14,"safe":true},{"name":"getTransaction","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":21,"safe":true},{"name":"getTransactionFromBlock","parameters":[{"name":"blockIndexOrHash","type":"ByteArray"},{"name":"txIndex","type":"Integer"}],"returntype":"Array","offset":28,"safe":true},{"name":"getTransactionHeight","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":35,"safe":true},{"name":"getTransactionSigners","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":42,"safe":true},{"name":"getTransactionVMState","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":49,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"NeoToken", """{"id":-5,"updatecounter":0,"hash":"0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1991619121},"manifest":{"name":"NeoToken","groups":[],"features":{},"supportedstandards":["NEP-17","NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getAccountState","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Array","offset":14,"safe":true},{"name":"getAllCandidates","parameters":[],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"getCandidateVote","parameters":[{"name":"pubKey","type":"PublicKey"}],"returntype":"Integer","offset":28,"safe":true},{"name":"getCandidates","parameters":[],"returntype":"Array","offset":35,"safe":true},{"name":"getCommittee","parameters":[],"returntype":"Array","offset":42,"safe":true},{"name":"getCommitteeAddress","parameters":[],"returntype":"Hash160","offset":49,"safe":true},{"name":"getGasPerBlock","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getNextBlockValidators","parameters":[],"returntype":"Array","offset":63,"safe":true},{"name":"getRegisterPrice","parameters":[],"returntype":"Integer","offset":70,"safe":true},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":77,"safe":false},{"name":"registerCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":84,"safe":false},{"name":"setGasPerBlock","parameters":[{"name":"gasPerBlock","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setRegisterPrice","parameters":[{"name":"registerPrice","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"symbol","parameters":[],"returntype":"String","offset":105,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":112,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":119,"safe":false},{"name":"unclaimedGas","parameters":[{"name":"account","type":"Hash160"},{"name":"end","type":"Integer"}],"returntype":"Integer","offset":126,"safe":true},{"name":"unregisterCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":133,"safe":false},{"name":"vote","parameters":[{"name":"account","type":"Hash160"},{"name":"voteTo","type":"PublicKey"}],"returntype":"Boolean","offset":140,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"CandidateStateChanged","parameters":[{"name":"pubkey","type":"PublicKey"},{"name":"registered","type":"Boolean"},{"name":"votes","type":"Integer"}]},{"name":"Vote","parameters":[{"name":"account","type":"Hash160"},{"name":"from","type":"PublicKey"},{"name":"to","type":"PublicKey"},{"name":"amount","type":"Integer"}]},{"name":"CommitteeChanged","parameters":[{"name":"old","type":"Array"},{"name":"new","type":"Array"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"GasToken", """{"id":-6,"updatecounter":0,"hash":"0xd2a4cff31913016155e38e474a2c06d08be276cf","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"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":7,"safe":true},{"name":"symbol","parameters":[],"returntype":"String","offset":14,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":28,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"PolicyContract", """{"id":-7,"updatecounter":0,"hash":"0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":341349534},"manifest":{"name":"PolicyContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"blockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":0,"safe":false},{"name":"getAttributeFee","parameters":[{"name":"attributeType","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlockedAccounts","parameters":[],"returntype":"InteropInterface","offset":14,"safe":true},{"name":"getExecFeeFactor","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getWhitelistFeeContracts","parameters":[],"returntype":"InteropInterface","offset":42,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":49,"safe":true},{"name":"removeWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"}],"returntype":"Void","offset":56,"safe":false},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":63,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":70,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":77,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fixedFee","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":98,"safe":false}],"events":[{"name":"WhitelistFeeChanged","parameters":[{"name":"contract","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fee","type":"Any"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"RoleManagement", """{"id":-8,"updatecounter":0,"hash":"0x49cf4e5378ffcd4dec034fd98a174c5491e395e2","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0A=","checksum":983638438},"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":7,"safe":true}],"events":[{"name":"Designation","parameters":[{"name":"Role","type":"Integer"},{"name":"BlockIndex","type":"Integer"},{"name":"Old","type":"Array"},{"name":"New","type":"Array"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"OracleContract", """{"id":-9,"updatecounter":0,"hash":"0xfe924b7cfe89ddd271abaf7210a80a7e11178758","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"OracleContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"finish","parameters":[],"returntype":"Void","offset":0,"safe":false},{"name":"getPrice","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"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":14,"safe":false},{"name":"setPrice","parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","offset":21,"safe":false},{"name":"verify","parameters":[],"returntype":"Boolean","offset":28,"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}}"""}, + {"Notary", """{"id":-10,"updatecounter":0,"hash":"0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"Notary","groups":[],"features":{},"supportedstandards":["NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"expirationOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getMaxNotValidBeforeDelta","parameters":[],"returntype":"Integer","offset":14,"safe":true},{"name":"lockDepositUntil","parameters":[{"name":"account","type":"Hash160"},{"name":"till","type":"Integer"}],"returntype":"Boolean","offset":21,"safe":false},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":28,"safe":false},{"name":"setMaxNotValidBeforeDelta","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":35,"safe":false},{"name":"verify","parameters":[{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":42,"safe":true},{"name":"withdraw","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"}],"returntype":"Boolean","offset":49,"safe":false}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"Treasury", """{"id":-11,"updatecounter":0,"hash":"0x156326f25b1b5d839a4d326aeaa75383c9563ac1","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1592866325},"manifest":{"name":"Treasury","groups":[],"features":{},"supportedstandards":["NEP-26","NEP-27"],"abi":{"methods":[{"name":"onNEP11Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenId","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","offset":0,"safe":true},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":7,"safe":true},{"name":"verify","parameters":[],"returntype":"Boolean","offset":14,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"TokenManagement", """{"id":-12,"updatecounter":0,"hash":"0xae00c57daeb20f9b6545f65a018f44a8a40e049f","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":1841570703},"manifest":{"name":"TokenManagement","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"burn","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"},{"name":"amount","type":"Integer"}],"returntype":"InteropInterface","offset":7,"safe":false},{"name":"burnNFT","parameters":[{"name":"uniqueId","type":"Hash160"}],"returntype":"InteropInterface","offset":14,"safe":false},{"name":"create","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"},{"name":"decimals","type":"Integer"}],"returntype":"Hash160","offset":21,"safe":false},{"name":"create","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"},{"name":"decimals","type":"Integer"},{"name":"maxSupply","type":"Integer"}],"returntype":"Hash160","offset":28,"safe":false},{"name":"createNonFungible","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"}],"returntype":"Hash160","offset":35,"safe":false},{"name":"createNonFungible","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"},{"name":"maxSupply","type":"Integer"}],"returntype":"Hash160","offset":42,"safe":false},{"name":"getNFTInfo","parameters":[{"name":"uniqueId","type":"Hash160"}],"returntype":"Array","offset":49,"safe":true},{"name":"getNFTs","parameters":[{"name":"assetId","type":"Hash160"}],"returntype":"InteropInterface","offset":56,"safe":true},{"name":"getNFTsOfOwner","parameters":[{"name":"account","type":"Hash160"}],"returntype":"InteropInterface","offset":63,"safe":true},{"name":"getTokenInfo","parameters":[{"name":"assetId","type":"Hash160"}],"returntype":"Array","offset":70,"safe":true},{"name":"mint","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"},{"name":"amount","type":"Integer"}],"returntype":"InteropInterface","offset":77,"safe":false},{"name":"mintNFT","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"}],"returntype":"InteropInterface","offset":84,"safe":false},{"name":"mintNFT","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"},{"name":"properties","type":"Map"}],"returntype":"InteropInterface","offset":91,"safe":false},{"name":"transfer","parameters":[{"name":"assetId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"InteropInterface","offset":98,"safe":false},{"name":"transferNFT","parameters":[{"name":"uniqueId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"data","type":"Any"}],"returntype":"InteropInterface","offset":105,"safe":false}],"events":[{"name":"Created","parameters":[{"name":"assetId","type":"Hash160"},{"name":"type","type":"Integer"}]},{"name":"Transfer","parameters":[{"name":"assetId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"NFTTransfer","parameters":[{"name":"uniqueId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}""" } + }; + } + + class Active : IHardforkActivable + { + public Hardfork? ActiveIn { get; init; } + public Hardfork? DeprecatedIn { get; init; } + } + + [TestMethod] + public void TestGetContract() + { + Assert.AreEqual(NativeContract.GetContract(NativeContract.NEO.Hash), NativeContract.NEO); + } + + [TestMethod] + public void TestGenesisNEP17Manifest() + { + var persistingBlock = new Block + { + Header = new Header + { + Index = 1, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero, + Witness = Witness.Empty, + }, + Transactions = [] + }; + var snapshot = _snapshotCache.CloneCache(); + + // Ensure that native NEP17 contracts contain proper supported standards and events declared + // in the manifest constructed for all hardforks enabled. Ref. https://github.com/neo-project/neo/pull/3195. + foreach (var h in new List() { NativeContract.GAS.Hash, NativeContract.NEO.Hash }) + { + var state = Call_GetContract(snapshot, h, persistingBlock); + Assert.IsTrue(state.Manifest.SupportedStandards.Contains("NEP-17")); + Assert.AreEqual(1, state.Manifest.Abi.Events.Where(e => e.Name == "Transfer").Count()); + } + } + + [TestMethod] + public void TestNativeContractId() + { + // native contract id is implicitly defined in NativeContract.cs(the defined order) + Assert.AreEqual(-1, NativeContract.ContractManagement.Id); + Assert.AreEqual(-2, NativeContract.StdLib.Id); + Assert.AreEqual(-3, NativeContract.CryptoLib.Id); + Assert.AreEqual(-4, NativeContract.Ledger.Id); + Assert.AreEqual(-5, NativeContract.NEO.Id); + Assert.AreEqual(-6, NativeContract.GAS.Id); + Assert.AreEqual(-7, NativeContract.Policy.Id); + Assert.AreEqual(-8, NativeContract.RoleManagement.Id); + Assert.AreEqual(-9, NativeContract.Oracle.Id); + Assert.AreEqual(-10, NativeContract.Notary.Id); + Assert.AreEqual(-11, NativeContract.Treasury.Id); + Assert.AreEqual(-12, NativeContract.TokenManagement.Id); + } + + class TestSpecialParameter + { + [ContractMethod] + public static void TestReadOnlyStoreView(UInt160 address, IReadOnlyStore view) { } + + [ContractMethod] + public static void TestDataCache(UInt160 address, DataCache cache) { } + + [ContractMethod] + public static void TestApplicationEngine(ApplicationEngine engine, IReadOnlyStore view) { } + + [ContractMethod] + public static void TestSnapshot(DataCache cache, ApplicationEngine engine) { } + } + + [TestMethod] + public void TestContractMethodWithSpecialParameter() + { + // If a contract method has ApplicationEngine, IReadOnlyStoreView or DataCache as a parameter, + // it should be the first parameter. + var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; + foreach (var contract in NativeContract.Contracts) + { + foreach (var member in typeof(Contract).GetMembers(flags)) + { + if (member.GetCustomAttributes().Any()) + CheckSpecialParameter(member); + } + } + + var test = new TestSpecialParameter(); + foreach (var method in typeof(TestSpecialParameter).GetMethods(flags)) + { + if (method.GetCustomAttributes().Any()) + { + // should be failed + Assert.ThrowsExactly(() => CheckSpecialParameter(method)); + } + } + } + + private static void CheckSpecialParameter(MemberInfo member) + { + var handler = member switch + { + MethodInfo m => m, + PropertyInfo p => p.GetMethod, + _ => null, + }; + Assert.IsNotNull(handler, $"handler is null, {member.Name}"); + + var parameters = handler.GetParameters(); + foreach (var param in parameters) + { + // ApplicationEngine or it's subclass + // Implementations of IReadOnlyStoreView + // DataCache or it's subclass + if (typeof(ApplicationEngine).IsAssignableFrom(param.ParameterType) || + typeof(IReadOnlyStore).IsAssignableFrom(param.ParameterType) || + typeof(DataCache).IsAssignableFrom(param.ParameterType)) + { + Assert.AreEqual(0, param.Position); + } + } + } + + [TestMethod] + public void TestGenesisNativeState() + { + var persistingBlock = new Block + { + Header = new Header + { + Index = 1, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero, + Witness = Witness.Empty, + }, + Transactions = [] + }; + var snapshot = _snapshotCache.CloneCache(); + + // Ensure that all native contracts have proper state generated with an assumption that + // all hardforks enabled. + foreach (var ctr in NativeContract.Contracts) + { + var state = Call_GetContract(snapshot, ctr.Hash, persistingBlock); + Assert.AreEqual(_nativeStates[ctr.Name], state.ToJson().ToString(), message: $"{ctr.Name} is wrong"); + } + } + + internal static ContractState Call_GetContract(DataCache snapshot, UInt160 address, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.ContractManagement.Hash, "getContract", address); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + var cs = (ContractState)RuntimeHelpers.GetUninitializedObject(typeof(ContractState)); + ((IInteroperable)cs).FromStackItem(result); + + return cs; + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs new file mode 100644 index 0000000000..15fae4b0e9 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs @@ -0,0 +1,1347 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NeoToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.Extensions.VM; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.UnitTests.Extensions; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; +using System.Runtime.CompilerServices; +using static Neo.SmartContract.Native.NeoToken; +using Array = System.Array; +using Boolean = Neo.VM.Types.Boolean; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_NeoToken +{ + private DataCache _snapshotCache = null!; + private Block _persistingBlock = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + _persistingBlock = new Block + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = Array.Empty() + }; + } + + [TestMethod] + public void Check_Name() => Assert.AreEqual(nameof(NeoToken), NativeContract.NEO.Name); + + [TestMethod] + public void Check_Symbol() => Assert.AreEqual("NEO", NativeContract.NEO.Symbol(_snapshotCache)); + + [TestMethod] + public void Check_Decimals() => Assert.AreEqual(0, NativeContract.NEO.Decimals(_snapshotCache)); + + [TestMethod] + public void Test_HF_EchidnaStates() + { + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = [] + }; + + foreach (var method in new string[] { "vote", "registerCandidate", "unregisterCandidate" }) + { + using (var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(UInt160.Zero), clonedCache, persistingBlock)) + { + var methods = NativeContract.NEO.GetContractMethods(engine); + var entries = methods.Values.Where(u => u.Name == method).ToArray(); + + Assert.HasCount(1, entries); + Assert.AreEqual(CallFlags.States | CallFlags.AllowNotify, entries[0].RequiredCallFlags); + } + } + } + + [TestMethod] + public void Check_Vote() + { + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + clonedCache.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + + byte[] from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray(); + + // No signature + + var ret = Check_Vote(clonedCache, from, null!, false, persistingBlock); + Assert.IsFalse(ret.Result); + Assert.IsTrue(ret.State); + + // Wrong address + + ret = Check_Vote(clonedCache, new byte[19], null!, false, persistingBlock); + Assert.IsFalse(ret.Result); + Assert.IsFalse(ret.State); + + // Wrong ec + + ret = Check_Vote(clonedCache, from, new byte[19], true, persistingBlock); + Assert.IsFalse(ret.Result); + Assert.IsFalse(ret.State); + + // no registered + + var fakeAddr = new byte[20]; + fakeAddr[0] = 0x5F; + fakeAddr[5] = 0xFF; + + ret = Check_Vote(clonedCache, fakeAddr, null!, true, persistingBlock); + Assert.IsFalse(ret.Result); + Assert.IsTrue(ret.State); + + // no registered + + var accountState = clonedCache.TryGet(CreateStorageKey(20, from))!.GetInteroperable(); + accountState.VoteTo = null; + ret = Check_Vote(clonedCache, from, ECCurve.Secp256r1.G.ToArray(), true, persistingBlock); + Assert.IsFalse(ret.Result); + Assert.IsTrue(ret.State); + Assert.IsNull(accountState.VoteTo); + + // normal case + + clonedCache.Add(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()), new StorageItem(new CandidateState() { Registered = true })); + ret = Check_Vote(clonedCache, from, ECCurve.Secp256r1.G.ToArray(), true, persistingBlock); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + accountState = clonedCache.TryGet(CreateStorageKey(20, from))!.GetInteroperable(); + Assert.AreEqual(ECCurve.Secp256r1.G, accountState.VoteTo); + } + + [TestMethod] + public void Check_Vote_Sameaccounts() + { + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + clonedCache.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + + byte[] from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray(); + var accountState = clonedCache.TryGet(CreateStorageKey(20, from))!.GetInteroperable(); + accountState.Balance = 100; + clonedCache.Add(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()), new StorageItem(new CandidateState() { Registered = true })); + var ret = Check_Vote(clonedCache, from, ECCurve.Secp256r1.G.ToArray(), true, persistingBlock); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + accountState = clonedCache.TryGet(CreateStorageKey(20, from))!.GetInteroperable(); + Assert.AreEqual(ECCurve.Secp256r1.G, accountState.VoteTo); + + //two account vote for the same account + var stateValidator = clonedCache.GetAndChange(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()))!.GetInteroperable(); + Assert.AreEqual(100, stateValidator.Votes); + var G_Account = Contract.CreateSignatureContract(ECCurve.Secp256r1.G).ScriptHash.ToArray(); + clonedCache.Add(CreateStorageKey(20, G_Account), new StorageItem(new NeoAccountState { Balance = 200 })); + var secondAccount = clonedCache.TryGet(CreateStorageKey(20, G_Account))!.GetInteroperable(); + Assert.AreEqual(200, secondAccount.Balance); + ret = Check_Vote(clonedCache, G_Account, ECCurve.Secp256r1.G.ToArray(), true, persistingBlock); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + stateValidator = clonedCache.GetAndChange(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()))!.GetInteroperable(); + Assert.AreEqual(300, stateValidator.Votes); + } + + [TestMethod] + public void Check_Vote_ChangeVote() + { + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + clonedCache.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + //from vote to G + byte[] from = TestProtocolSettings.Default.StandbyValidators[0].ToArray(); + var from_Account = Contract.CreateSignatureContract(TestProtocolSettings.Default.StandbyValidators[0]).ScriptHash.ToArray(); + clonedCache.Add(CreateStorageKey(20, from_Account), new StorageItem(new NeoAccountState())); + var accountState = clonedCache.TryGet(CreateStorageKey(20, from_Account))!.GetInteroperable(); + accountState.Balance = 100; + clonedCache.Add(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()), new StorageItem(new CandidateState() { Registered = true })); + var ret = Check_Vote(clonedCache, from_Account, ECCurve.Secp256r1.G.ToArray(), true, persistingBlock); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + accountState = clonedCache.TryGet(CreateStorageKey(20, from_Account))!.GetInteroperable(); + Assert.AreEqual(ECCurve.Secp256r1.G, accountState.VoteTo); + + //from change vote to itself + var G_stateValidator = clonedCache.GetAndChange(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()))!.GetInteroperable(); + Assert.AreEqual(100, G_stateValidator.Votes); + var G_Account = Contract.CreateSignatureContract(ECCurve.Secp256r1.G).ScriptHash.ToArray(); + clonedCache.Add(CreateStorageKey(20, G_Account), new StorageItem(new NeoAccountState { Balance = 200 })); + clonedCache.Add(CreateStorageKey(33, from), new StorageItem(new CandidateState() { Registered = true })); + ret = Check_Vote(clonedCache, from_Account, from, true, persistingBlock); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + G_stateValidator = clonedCache.GetAndChange(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()))!.GetInteroperable(); + Assert.AreEqual(0, G_stateValidator.Votes); + var from_stateValidator = clonedCache.GetAndChange(CreateStorageKey(33, from))!.GetInteroperable(); + Assert.AreEqual(100, from_stateValidator.Votes); + } + + [TestMethod] + public void Check_Vote_VoteToNull() + { + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + clonedCache.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + byte[] from = TestProtocolSettings.Default.StandbyValidators[0].ToArray(); + var from_Account = Contract.CreateSignatureContract(TestProtocolSettings.Default.StandbyValidators[0]).ScriptHash.ToArray(); + clonedCache.Add(CreateStorageKey(20, from_Account), new StorageItem(new NeoAccountState())); + var accountState = clonedCache.TryGet(CreateStorageKey(20, from_Account))!.GetInteroperable(); + accountState.Balance = 100; + clonedCache.Add(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()), new StorageItem(new CandidateState() { Registered = true })); + clonedCache.Add(CreateStorageKey(23, ECCurve.Secp256r1.G.ToArray()), new StorageItem(new BigInteger(100500))); + var ret = Check_Vote(clonedCache, from_Account, ECCurve.Secp256r1.G.ToArray(), true, persistingBlock); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + accountState = clonedCache.TryGet(CreateStorageKey(20, from_Account))!.GetInteroperable(); + Assert.AreEqual(ECCurve.Secp256r1.G, accountState.VoteTo); + Assert.AreEqual(100500, accountState.LastGasPerVote); + + //from vote to null account G votes becomes 0 + var G_stateValidator = clonedCache.GetAndChange(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()))!.GetInteroperable(); + Assert.AreEqual(100, G_stateValidator.Votes); + var G_Account = Contract.CreateSignatureContract(ECCurve.Secp256r1.G).ScriptHash.ToArray(); + clonedCache.Add(CreateStorageKey(20, G_Account), new StorageItem(new NeoAccountState { Balance = 200 })); + clonedCache.Add(CreateStorageKey(33, from), new StorageItem(new CandidateState() { Registered = true })); + ret = Check_Vote(clonedCache, from_Account, null!, true, persistingBlock); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + G_stateValidator = clonedCache.GetAndChange(CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()))!.GetInteroperable(); + Assert.AreEqual(0, G_stateValidator.Votes); + accountState = clonedCache.TryGet(CreateStorageKey(20, from_Account))!.GetInteroperable(); + Assert.IsNull(accountState.VoteTo); + Assert.AreEqual(0, accountState.LastGasPerVote); + } + + [TestMethod] + public void Check_UnclaimedGas() + { + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + clonedCache.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + + byte[] from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray(); + + var unclaim = Check_UnclaimedGas(clonedCache, from, persistingBlock); + Assert.AreEqual(new BigInteger(0.5 * 1000 * 100000000L), unclaim.Value); + Assert.IsTrue(unclaim.State); + + unclaim = Check_UnclaimedGas(clonedCache, new byte[19], persistingBlock); + Assert.AreEqual(BigInteger.Zero, unclaim.Value); + Assert.IsFalse(unclaim.State); + } + + [TestMethod] + public void Check_RegisterValidator() + { + var clonedCache = _snapshotCache.CloneCache(); + + var keyCount = clonedCache.GetChangeSet().Count(); + var point = (byte[])TestProtocolSettings.Default.StandbyValidators[0].EncodePoint(true).Clone(); + + var ret = Check_RegisterValidator(clonedCache, point, _persistingBlock); // Exists + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + + Assert.AreEqual(++keyCount, clonedCache.GetChangeSet().Count()); // No changes + + point[20]++; // fake point + ret = Check_RegisterValidator(clonedCache, point, _persistingBlock); // New + + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + + Assert.AreEqual(keyCount + 1, clonedCache.GetChangeSet().Count()); // New validator + + // Check GetRegisteredValidators + + var members = NativeContract.NEO.GetCandidatesInternal(clonedCache); + Assert.AreEqual(2, members.Count()); + } + + [TestMethod] + public void Check_RegisterValidatorViaNEP27() + { + var clonedCache = _snapshotCache.CloneCache(); + var point = ECPoint.Parse("021821807f923a3da004fb73871509d7635bcc05f41edef2a3ca5c941d8bbc1231", ECCurve.Secp256r1); + var pointData = point.EncodePoint(true); + + // Send some NEO, shouldn't be accepted + var ret = Check_RegisterValidatorViaNEP27(clonedCache, point, _persistingBlock, true, pointData, 1000_0000_0000); + Assert.IsFalse(ret.State); + + // Send improper amount of GAS, shouldn't be accepted. + ret = Check_RegisterValidatorViaNEP27(clonedCache, point, _persistingBlock, false, pointData, 1000_0000_0001); + Assert.IsFalse(ret.State); + + // Broken witness. + var badPoint = ECPoint.Parse("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", ECCurve.Secp256r1); + ret = Check_RegisterValidatorViaNEP27(clonedCache, point, _persistingBlock, false, badPoint.EncodePoint(true), 1000_0000_0000); + Assert.IsFalse(ret.State); + + // Successful case. + ret = Check_RegisterValidatorViaNEP27(clonedCache, point, _persistingBlock, false, pointData, 1000_0000_0000); + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + + // Check GetRegisteredValidators + var members = NativeContract.NEO.GetCandidatesInternal(clonedCache); + Assert.AreEqual(1, members.Count()); + Assert.AreEqual(point, members.First().PublicKey); + + // No GAS should be left on the NEO account. + Assert.AreEqual(0, NativeContract.GAS.BalanceOf(clonedCache, NativeContract.NEO.Hash)); + } + + [TestMethod] + public void Check_UnregisterCandidate() + { + var clonedCache = _snapshotCache.CloneCache(); + _persistingBlock.Header.Index = 1; + var keyCount = clonedCache.GetChangeSet().Count(); + var point = TestProtocolSettings.Default.StandbyValidators[0].EncodePoint(true); + + //without register + var ret = Check_UnregisterCandidate(clonedCache, point, _persistingBlock); + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + + Assert.AreEqual(keyCount, clonedCache.GetChangeSet().Count()); + + //register and then unregister + ret = Check_RegisterValidator(clonedCache, point, _persistingBlock); + StorageItem item = clonedCache.GetAndChange(CreateStorageKey(33, point))!; + Assert.AreEqual(7, item.Size); + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + + var members = NativeContract.NEO.GetCandidatesInternal(clonedCache); + Assert.AreEqual(1, members.Count()); + Assert.AreEqual(keyCount + 1, clonedCache.GetChangeSet().Count()); + StorageKey key = CreateStorageKey(33, point); + Assert.IsNotNull(clonedCache.TryGet(key)); + + ret = Check_UnregisterCandidate(clonedCache, point, _persistingBlock); + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + + Assert.AreEqual(keyCount, clonedCache.GetChangeSet().Count()); + + members = NativeContract.NEO.GetCandidatesInternal(clonedCache); + Assert.AreEqual(0, members.Count()); + Assert.IsNull(clonedCache.TryGet(key)); + + //register with votes, then unregister + ret = Check_RegisterValidator(clonedCache, point, _persistingBlock); + Assert.IsTrue(ret.State); + var G_Account = Contract.CreateSignatureContract(ECCurve.Secp256r1.G).ScriptHash.ToArray(); + clonedCache.Add(CreateStorageKey(20, G_Account), new StorageItem(new NeoAccountState())); + var accountState = clonedCache.TryGet(CreateStorageKey(20, G_Account))!.GetInteroperable(); + accountState.Balance = 100; + Check_Vote(clonedCache, G_Account, TestProtocolSettings.Default.StandbyValidators[0].ToArray(), true, _persistingBlock); + ret = Check_UnregisterCandidate(clonedCache, point, _persistingBlock); + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + Assert.IsNotNull(clonedCache.TryGet(key)); + StorageItem pointItem = clonedCache.TryGet(key)!; + CandidateState pointState = pointItem.GetInteroperable(); + Assert.IsFalse(pointState.Registered); + Assert.AreEqual(100, pointState.Votes); + + //vote fail + ret = Check_Vote(clonedCache, G_Account, TestProtocolSettings.Default.StandbyValidators[0].ToArray(), true, _persistingBlock); + Assert.IsTrue(ret.State); + Assert.IsFalse(ret.Result); + accountState = clonedCache.TryGet(CreateStorageKey(20, G_Account))!.GetInteroperable(); + Assert.AreEqual(TestProtocolSettings.Default.StandbyValidators[0], accountState.VoteTo); + } + + [TestMethod] + public void Check_GetCommittee() + { + var clonedCache = _snapshotCache.CloneCache(); + var keyCount = clonedCache.GetChangeSet().Count(); + var point = TestProtocolSettings.Default.StandbyValidators[0].EncodePoint(true); + var persistingBlock = _persistingBlock; + persistingBlock.Header.Index = 1; + //register with votes with 20000000 + var G_Account = Contract.CreateSignatureContract(ECCurve.Secp256r1.G).ScriptHash.ToArray(); + clonedCache.Add(CreateStorageKey(20, G_Account), new StorageItem(new NeoAccountState())); + var accountState = clonedCache.TryGet(CreateStorageKey(20, G_Account))!.GetInteroperable(); + accountState.Balance = 20000000; + var ret = Check_RegisterValidator(clonedCache, ECCurve.Secp256r1.G.ToArray(), persistingBlock); + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + ret = Check_Vote(clonedCache, G_Account, ECCurve.Secp256r1.G.ToArray(), true, persistingBlock); + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + + + var committeemembers = NativeContract.NEO.GetCommittee(clonedCache); + var defaultCommittee = TestProtocolSettings.Default.StandbyCommittee.OrderBy(p => p).ToArray(); + Assert.AreEqual(typeof(ECPoint[]), committeemembers.GetType()); + for (int i = 0; i < TestProtocolSettings.Default.CommitteeMembersCount; i++) + { + Assert.AreEqual(committeemembers[i], defaultCommittee[i]); + } + + //register more candidates, committee member change + persistingBlock = new Block + { + Header = new() + { + Index = (uint)TestProtocolSettings.Default.CommitteeMembersCount, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero, + Witness = Witness.Empty, + }, + Transactions = [], + }; + for (int i = 0; i < TestProtocolSettings.Default.CommitteeMembersCount - 1; i++) + { + ret = Check_RegisterValidator(clonedCache, TestProtocolSettings.Default.StandbyCommittee[i].ToArray(), persistingBlock); + Assert.IsTrue(ret.State); + Assert.IsTrue(ret.Result); + } + + Assert.IsTrue(Check_OnPersist(clonedCache, persistingBlock)); + + committeemembers = NativeContract.NEO.GetCommittee(clonedCache); + Assert.AreEqual(committeemembers.Length, TestProtocolSettings.Default.CommitteeMembersCount); + Assert.IsTrue(committeemembers.Contains(ECCurve.Secp256r1.G)); + for (int i = 0; i < TestProtocolSettings.Default.CommitteeMembersCount - 1; i++) + { + Assert.IsTrue(committeemembers.Contains(TestProtocolSettings.Default.StandbyCommittee[i])); + } + Assert.IsFalse(committeemembers.Contains(TestProtocolSettings.Default.StandbyCommittee[TestProtocolSettings.Default.CommitteeMembersCount - 1])); + } + + [TestMethod] + public void Check_Transfer() + { + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + + byte[] from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray(); + byte[] to = new byte[20]; + + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + clonedCache.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + var keyCount = clonedCache.GetChangeSet().Count(); + + // Check unclaim + + var unclaim = Check_UnclaimedGas(clonedCache, from, persistingBlock); + Assert.AreEqual(new BigInteger(0.5 * 1000 * 100000000L), unclaim.Value); + Assert.IsTrue(unclaim.State); + + // Transfer + + Assert.IsFalse(NativeContract.NEO.Transfer(clonedCache, from, to, BigInteger.One, false, persistingBlock)); // Not signed + Assert.IsTrue(NativeContract.NEO.Transfer(clonedCache, from, to, BigInteger.One, true, persistingBlock)); + Assert.AreEqual(99999999, NativeContract.NEO.BalanceOf(clonedCache, from)); + Assert.AreEqual(1, NativeContract.NEO.BalanceOf(clonedCache, to)); + + var (from_balance, _, _) = GetAccountState(clonedCache, new UInt160(from)); + var (to_balance, _, _) = GetAccountState(clonedCache, new UInt160(to)); + + Assert.AreEqual(99999999, from_balance); + Assert.AreEqual(1, to_balance); + + // Check unclaim + + unclaim = Check_UnclaimedGas(clonedCache, from, persistingBlock); + Assert.AreEqual(BigInteger.Zero, unclaim.Value); + Assert.IsTrue(unclaim.State); + + Assert.AreEqual(keyCount + 4, clonedCache.GetChangeSet().Count()); // Gas + new balance + + // Return balance + + keyCount = clonedCache.GetChangeSet().Count(); + + Assert.IsTrue(NativeContract.NEO.Transfer(clonedCache, to, from, BigInteger.One, true, persistingBlock)); + Assert.AreEqual(0, NativeContract.NEO.BalanceOf(clonedCache, to)); + Assert.AreEqual(keyCount - 1, clonedCache.GetChangeSet().Count()); // Remove neo balance from address two + + // Bad inputs + + Assert.ThrowsExactly(() => _ = NativeContract.NEO.Transfer(clonedCache, from, to, BigInteger.MinusOne, true, persistingBlock)); + Assert.ThrowsExactly(() => _ = NativeContract.NEO.Transfer(clonedCache, new byte[19], to, BigInteger.One, false, persistingBlock)); + Assert.ThrowsExactly(() => _ = NativeContract.NEO.Transfer(clonedCache, from, new byte[19], BigInteger.One, false, persistingBlock)); + + // More than balance + + Assert.IsFalse(NativeContract.NEO.Transfer(clonedCache, to, from, new BigInteger(2), true, persistingBlock)); + } + + [TestMethod] + public void Check_BalanceOf() + { + var clonedCache = _snapshotCache.CloneCache(); + byte[] account = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray(); + + Assert.AreEqual(100_000_000, NativeContract.NEO.BalanceOf(clonedCache, account)); + + account[5]++; // Without existing balance + + Assert.AreEqual(0, NativeContract.NEO.BalanceOf(clonedCache, account)); + } + + [TestMethod] + public void Check_CommitteeBonus() + { + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = new() + { + Index = 1, + Witness = Witness.Empty, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero + }, + Transactions = [], + }; + Assert.IsTrue(Check_PostPersist(clonedCache, persistingBlock)); + + var committee = TestProtocolSettings.Default.StandbyCommittee; + Assert.AreEqual(50000000, NativeContract.GAS.BalanceOf(clonedCache, Contract.CreateSignatureContract(committee[0]).ScriptHash)); + Assert.AreEqual(50000000, NativeContract.GAS.BalanceOf(clonedCache, Contract.CreateSignatureContract(committee[1]).ScriptHash)); + Assert.AreEqual(0, NativeContract.GAS.BalanceOf(clonedCache, Contract.CreateSignatureContract(committee[2]).ScriptHash)); + } + + [TestMethod] + public void Check_Initialize() + { + var clonedCache = _snapshotCache.CloneCache(); + + // StandbyValidators + + Check_GetCommittee(clonedCache, null); + } + + [TestMethod] + public void TestCalculateBonus() + { + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = (Block)RuntimeHelpers.GetUninitializedObject(typeof(Block)); + + StorageKey key = CreateStorageKey(20, UInt160.Zero.ToArray()); + + // Fault: balance < 0 + + clonedCache.Add(key, new StorageItem(new NeoAccountState + { + Balance = -100 + })); + try + { + NativeContract.NEO.UnclaimedGas(clonedCache, UInt160.Zero, 10); + Assert.Fail("Should have thrown ArgumentOutOfRangeException"); + } + catch (ArgumentOutOfRangeException) { } + clonedCache.Delete(key); + + // Fault range: start >= end + + clonedCache.GetAndChange(key, () => new StorageItem(new NeoAccountState + { + Balance = 100, + BalanceHeight = 100 + })); + try + { + NativeContract.NEO.UnclaimedGas(clonedCache, UInt160.Zero, 10); + Assert.Fail("Should have thrown ArgumentOutOfRangeException"); + } + catch (ArgumentOutOfRangeException) { } + clonedCache.Delete(key); + + // Fault range: start >= end + + clonedCache.GetAndChange(key, () => new StorageItem(new NeoAccountState + { + Balance = 100, + BalanceHeight = 100 + })); + try + { + NativeContract.NEO.UnclaimedGas(clonedCache, UInt160.Zero, 10); + Assert.Fail("Should have thrown ArgumentOutOfRangeException"); + } + catch (ArgumentOutOfRangeException) { } + clonedCache.Delete(key); + + // Normal 1) votee is non exist + + clonedCache.GetAndChange(key, () => new StorageItem(new NeoAccountState + { + Balance = 100 + })); + + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + var item = clonedCache.GetAndChange(storageKey)!.GetInteroperable(); + item.Index = 99; + + Assert.AreEqual(new BigInteger(0.5 * 100 * 100), NativeContract.NEO.UnclaimedGas(clonedCache, UInt160.Zero, 100)); + clonedCache.Delete(key); + + // Normal 2) votee is not committee + + clonedCache.GetAndChange(key, () => new StorageItem(new NeoAccountState + { + Balance = 100, + VoteTo = ECCurve.Secp256r1.G + })); + Assert.AreEqual(new BigInteger(0.5 * 100 * 100), NativeContract.NEO.UnclaimedGas(clonedCache, UInt160.Zero, 100)); + clonedCache.Delete(key); + + // Normal 3) votee is committee + + clonedCache.GetAndChange(key, () => new StorageItem(new NeoAccountState + { + Balance = 100, + VoteTo = TestProtocolSettings.Default.StandbyCommittee[0] + })); + clonedCache.Add(new KeyBuilder(NativeContract.NEO.Id, 23).Add(TestProtocolSettings.Default.StandbyCommittee[0]).Add(uint.MaxValue - 50), new StorageItem() { Value = new BigInteger(50 * 10000L).ToByteArray() }); + Assert.AreEqual(new BigInteger(50 * 100), NativeContract.NEO.UnclaimedGas(clonedCache, UInt160.Zero, 100)); + clonedCache.Delete(key); + } + + [TestMethod] + public void TestGetNextBlockValidators1() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var result = (Neo.VM.Types.Array)NativeContract.NEO.Call(snapshotCache, "getNextBlockValidators")!; + Assert.HasCount(7, result); + Assert.AreEqual("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", result[0].GetSpan().ToHexString()); + Assert.AreEqual("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", result[1].GetSpan().ToHexString()); + Assert.AreEqual("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", result[2].GetSpan().ToHexString()); + Assert.AreEqual("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", result[3].GetSpan().ToHexString()); + Assert.AreEqual("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", result[4].GetSpan().ToHexString()); + Assert.AreEqual("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", result[5].GetSpan().ToHexString()); + Assert.AreEqual("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", result[6].GetSpan().ToHexString()); + } + + [TestMethod] + public void TestGetNextBlockValidators2() + { + var clonedCache = _snapshotCache.CloneCache(); + var result = NativeContract.NEO.GetNextBlockValidators(clonedCache, 7); + Assert.HasCount(7, result); + Assert.AreEqual("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", result[0].ToArray().ToHexString()); + Assert.AreEqual("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", result[1].ToArray().ToHexString()); + Assert.AreEqual("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", result[2].ToArray().ToHexString()); + Assert.AreEqual("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", result[3].ToArray().ToHexString()); + Assert.AreEqual("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", result[4].ToArray().ToHexString()); + Assert.AreEqual("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", result[5].ToArray().ToHexString()); + Assert.AreEqual("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", result[6].ToArray().ToHexString()); + } + + [TestMethod] + public void TestGetCandidates1() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var array = (Neo.VM.Types.Array)NativeContract.NEO.Call(snapshotCache, "getCandidates")!; + Assert.IsEmpty(array); + } + + [TestMethod] + public void TestGetCandidates2() + { + var clonedCache = _snapshotCache.CloneCache(); + var result = NativeContract.NEO.GetCandidatesInternal(clonedCache); + Assert.AreEqual(0, result.Count()); + + StorageKey key = NativeContract.NEO.CreateStorageKey(33, ECCurve.Secp256r1.G); + clonedCache.Add(key, new StorageItem(new CandidateState() { Registered = true })); + Assert.AreEqual(1, NativeContract.NEO.GetCandidatesInternal(clonedCache).Count()); + } + + [TestMethod] + public void TestCheckCandidate() + { + var cloneCache = _snapshotCache.CloneCache(); + var committee = NativeContract.NEO.GetCommittee(cloneCache); + var point = committee[0].EncodePoint(true); + + // Prepare Prefix_VoterRewardPerCommittee + var storageKey = new KeyBuilder(NativeContract.NEO.Id, 23).Add(committee[0]); + cloneCache.Add(storageKey, new StorageItem(new BigInteger(1000))); + + // Prepare Candidate + storageKey = new KeyBuilder(NativeContract.NEO.Id, 33).Add(committee[0]); + cloneCache.Add(storageKey, new StorageItem(new CandidateState { Registered = true, Votes = BigInteger.One })); + + storageKey = new KeyBuilder(NativeContract.NEO.Id, 23).Add(committee[0]); + Assert.HasCount(1, cloneCache.Find(storageKey).ToArray()); + + // Pre-persist + var persistingBlock = new Block + { + Header = new() + { + Index = 21, + Witness = Witness.Empty, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero + }, + Transactions = [], + }; + Assert.IsTrue(Check_OnPersist(cloneCache, persistingBlock)); + + // Clear votes + storageKey = new KeyBuilder(NativeContract.NEO.Id, 33).Add(committee[0]); + cloneCache.GetAndChange(storageKey)!.GetInteroperable().Votes = BigInteger.Zero; + + // Unregister candidate, remove + var (state, result) = Check_UnregisterCandidate(cloneCache, point, persistingBlock); + Assert.IsTrue(state); + Assert.IsTrue(result); + + storageKey = new KeyBuilder(NativeContract.NEO.Id, 23).Add(committee[0]); + Assert.IsEmpty(cloneCache.Find(storageKey).ToArray()); + + // Post-persist + Assert.IsTrue(Check_PostPersist(cloneCache, persistingBlock)); + + storageKey = new KeyBuilder(NativeContract.NEO.Id, 23).Add(committee[0]); + Assert.HasCount(1, cloneCache.Find(storageKey).ToArray()); + } + + [TestMethod] + public void TestGetCommittee() + { + var clonedCache = TestBlockchain.GetTestSnapshotCache(); + var result = (Neo.VM.Types.Array)NativeContract.NEO.Call(clonedCache, "getCommittee")!; + Assert.HasCount(21, result); + Assert.AreEqual("020f2887f41474cfeb11fd262e982051c1541418137c02a0f4961af911045de639", result[0].GetSpan().ToHexString()); + Assert.AreEqual("03204223f8c86b8cd5c89ef12e4f0dbb314172e9241e30c9ef2293790793537cf0", result[1].GetSpan().ToHexString()); + Assert.AreEqual("0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30", result[2].GetSpan().ToHexString()); + Assert.AreEqual("0226933336f1b75baa42d42b71d9091508b638046d19abd67f4e119bf64a7cfb4d", result[3].GetSpan().ToHexString()); + Assert.AreEqual("023a36c72844610b4d34d1968662424011bf783ca9d984efa19a20babf5582f3fe", result[4].GetSpan().ToHexString()); + Assert.AreEqual("03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0", result[5].GetSpan().ToHexString()); + Assert.AreEqual("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", result[6].GetSpan().ToHexString()); + Assert.AreEqual("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", result[7].GetSpan().ToHexString()); + Assert.AreEqual("02504acbc1f4b3bdad1d86d6e1a08603771db135a73e61c9d565ae06a1938cd2ad", result[8].GetSpan().ToHexString()); + Assert.AreEqual("03708b860c1de5d87f5b151a12c2a99feebd2e8b315ee8e7cf8aa19692a9e18379", result[9].GetSpan().ToHexString()); + Assert.AreEqual("0288342b141c30dc8ffcde0204929bb46aed5756b41ef4a56778d15ada8f0c6654", result[10].GetSpan().ToHexString()); + Assert.AreEqual("02a62c915cf19c7f19a50ec217e79fac2439bbaad658493de0c7d8ffa92ab0aa62", result[11].GetSpan().ToHexString()); + Assert.AreEqual("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", result[12].GetSpan().ToHexString()); + Assert.AreEqual("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", result[13].GetSpan().ToHexString()); + Assert.AreEqual("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", result[14].GetSpan().ToHexString()); + Assert.AreEqual("03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050", result[15].GetSpan().ToHexString()); + Assert.AreEqual("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", result[16].GetSpan().ToHexString()); + Assert.AreEqual("02cd5a5547119e24feaa7c2a0f37b8c9366216bab7054de0065c9be42084003c8a", result[17].GetSpan().ToHexString()); + Assert.AreEqual("03cdcea66032b82f5c30450e381e5295cae85c5e6943af716cc6b646352a6067dc", result[18].GetSpan().ToHexString()); + Assert.AreEqual("03d281b42002647f0113f36c7b8efb30db66078dfaaa9ab3ff76d043a98d512fde", result[19].GetSpan().ToHexString()); + Assert.AreEqual("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", result[20].GetSpan().ToHexString()); + } + + [TestMethod] + public void TestGetValidators() + { + var clonedCache = _snapshotCache.CloneCache(); + var result = NativeContract.NEO.ComputeNextBlockValidators(clonedCache, TestProtocolSettings.Default); + Assert.AreEqual("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", result[0].ToArray().ToHexString()); + Assert.AreEqual("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", result[1].ToArray().ToHexString()); + Assert.AreEqual("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", result[2].ToArray().ToHexString()); + Assert.AreEqual("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", result[3].ToArray().ToHexString()); + Assert.AreEqual("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", result[4].ToArray().ToHexString()); + Assert.AreEqual("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", result[5].ToArray().ToHexString()); + Assert.AreEqual("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", result[6].ToArray().ToHexString()); + } + + [TestMethod] + public void TestOnBalanceChanging() + { + var ret = Transfer4TesingOnBalanceChanging(new BigInteger(0), false); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + + ret = Transfer4TesingOnBalanceChanging(new BigInteger(1), false); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + + ret = Transfer4TesingOnBalanceChanging(new BigInteger(1), true); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + } + + [TestMethod] + public void TestTotalSupply() + { + var clonedCache = _snapshotCache.CloneCache(); + Assert.AreEqual(new BigInteger(100000000), NativeContract.NEO.TotalSupply(clonedCache)); + } + + [TestMethod] + public void TestEconomicParameter() + { + const byte Prefix_CurrentBlock = 12; + var clonedCache = _snapshotCache.CloneCache(); + var persistingBlock = new Block + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = [] + }; + + (BigInteger, bool) result = Check_GetGasPerBlock(clonedCache, persistingBlock); + Assert.IsTrue(result.Item2); + Assert.AreEqual(5 * NativeContract.GAS.Factor, result.Item1); + + persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 10, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + (Boolean, bool) result1 = Check_SetGasPerBlock(clonedCache, 10 * NativeContract.GAS.Factor, persistingBlock); + Assert.IsTrue(result1.Item2); + Assert.IsTrue(result1.Item1.GetBoolean()); + + var height = clonedCache[NativeContract.Ledger.CreateStorageKey(Prefix_CurrentBlock)].GetInteroperable(); + height.Index = persistingBlock.Index + 1; + result = Check_GetGasPerBlock(clonedCache, persistingBlock); + Assert.IsTrue(result.Item2); + Assert.AreEqual(10 * NativeContract.GAS.Factor, result.Item1); + + // Check calculate bonus + StorageItem storage = clonedCache.GetOrAdd(CreateStorageKey(20, UInt160.Zero.ToArray()), () => new StorageItem(new NeoAccountState())); + NeoAccountState state = storage.GetInteroperable(); + state.Balance = 1000; + state.BalanceHeight = 0; + height.Index = persistingBlock.Index + 1; + Assert.AreEqual(6500, NativeContract.NEO.UnclaimedGas(clonedCache, UInt160.Zero, persistingBlock.Index + 2)); + } + + [TestMethod] + public void TestClaimGas() + { + var clonedCache = _snapshotCache.CloneCache(); + + // Initialize block + clonedCache.Add(CreateStorageKey(1), new StorageItem(new BigInteger(30000000))); + + ECPoint[] standbyCommittee = TestProtocolSettings.Default.StandbyCommittee.OrderBy(p => p).ToArray(); + CachedCommittee cachedCommittee = new(); + for (var i = 0; i < TestProtocolSettings.Default.CommitteeMembersCount; i++) + { + ECPoint member = standbyCommittee[i]; + clonedCache.Add(new KeyBuilder(NativeContract.NEO.Id, 33).Add(member), new StorageItem(new CandidateState() + { + Registered = true, + Votes = 200 * 10000 + })); + cachedCommittee.Add((member, 200 * 10000)); + } + clonedCache.GetOrAdd(new KeyBuilder(NativeContract.NEO.Id, 14), () => new StorageItem()).Value = BinarySerializer.Serialize(cachedCommittee.ToStackItem(null), ExecutionEngineLimits.Default); + + var item = clonedCache.GetAndChange(new KeyBuilder(NativeContract.NEO.Id, 1), () => new StorageItem()); + item.Value = ((BigInteger)2100 * 10000L).ToByteArray(); + + var persistingBlock = new Block + { + Header = new Header + { + Index = 0, + Witness = Witness.Empty, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero + }, + Transactions = [], + }; + Assert.IsTrue(Check_PostPersist(clonedCache, persistingBlock)); + + var committee = TestProtocolSettings.Default.StandbyCommittee.OrderBy(p => p).ToArray(); + var accountA = committee[0]; + var accountB = committee[TestProtocolSettings.Default.CommitteeMembersCount - 1]; + Assert.AreEqual(0, NativeContract.NEO.BalanceOf(clonedCache, Contract.CreateSignatureContract(accountA).ScriptHash)); + + StorageItem storageItem = clonedCache.TryGet(new KeyBuilder(NativeContract.NEO.Id, 23).Add(accountA))!; + Assert.AreEqual(30000000000, (BigInteger)storageItem); + + Assert.IsNull(clonedCache.TryGet(new KeyBuilder(NativeContract.NEO.Id, 23).Add(accountB).Add(uint.MaxValue - 1))); + + // Next block + + persistingBlock = new Block + { + Header = new Header + { + Index = 1, + Witness = Witness.Empty, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero + }, + Transactions = [], + }; + Assert.IsTrue(Check_PostPersist(clonedCache, persistingBlock)); + + Assert.AreEqual(0, NativeContract.NEO.BalanceOf(clonedCache, Contract.CreateSignatureContract(committee[1]).ScriptHash)); + + storageItem = clonedCache.TryGet(new KeyBuilder(NativeContract.NEO.Id, 23).Add(committee[1]))!; + Assert.AreEqual(30000000000, (BigInteger)storageItem); + + // Next block + + persistingBlock = new Block + { + Header = new Header + { + Index = 21, + Witness = Witness.Empty, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero + }, + Transactions = [], + }; + Assert.IsTrue(Check_PostPersist(clonedCache, persistingBlock)); + + accountA = TestProtocolSettings.Default.StandbyCommittee.OrderBy(p => p).ToArray()[2]; + Assert.AreEqual(0, NativeContract.NEO.BalanceOf(clonedCache, Contract.CreateSignatureContract(committee[2]).ScriptHash)); + + storageItem = clonedCache.TryGet(new KeyBuilder(NativeContract.NEO.Id, 23).Add(committee[2]))!; + Assert.AreEqual(30000000000 * 2, (BigInteger)storageItem); + + // Claim GAS + + var account = Contract.CreateSignatureContract(committee[2]).ScriptHash; + clonedCache.Add(new KeyBuilder(NativeContract.NEO.Id, 20).Add(account), new StorageItem(new NeoAccountState + { + BalanceHeight = 3, + Balance = 200 * 10000 - 2 * 100, + VoteTo = committee[2], + LastGasPerVote = 30000000000, + })); + Assert.AreEqual(1999800, NativeContract.NEO.BalanceOf(clonedCache, account)); + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + clonedCache.GetAndChange(storageKey)!.GetInteroperable().Index = 29 + 2; + BigInteger value = NativeContract.NEO.UnclaimedGas(clonedCache, account, 29 + 3); + Assert.AreEqual(1999800 * 30000000000 / 100000000L + (1999800L * 10 * 5 * 29 / 100), value); + } + + [TestMethod] + public void TestUnclaimedGas() + { + var clonedCache = _snapshotCache.CloneCache(); + Assert.AreEqual(BigInteger.Zero, NativeContract.NEO.UnclaimedGas(clonedCache, UInt160.Zero, 10)); + clonedCache.Add(CreateStorageKey(20, UInt160.Zero.ToArray()), new StorageItem(new NeoAccountState())); + Assert.AreEqual(BigInteger.Zero, NativeContract.NEO.UnclaimedGas(clonedCache, UInt160.Zero, 10)); + } + + [TestMethod] + public void TestVote() + { + var clonedCache = _snapshotCache.CloneCache(); + UInt160 account = UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4"); + StorageKey keyAccount = CreateStorageKey(20, account.ToArray()); + StorageKey keyValidator = CreateStorageKey(33, ECCurve.Secp256r1.G.ToArray()); + _persistingBlock.Header.Index = 1; + var ret = Check_Vote(clonedCache, account.ToArray(), ECCurve.Secp256r1.G.ToArray(), false, _persistingBlock); + Assert.IsFalse(ret.Result); + Assert.IsTrue(ret.State); + + ret = Check_Vote(clonedCache, account.ToArray(), ECCurve.Secp256r1.G.ToArray(), true, _persistingBlock); + Assert.IsFalse(ret.Result); + Assert.IsTrue(ret.State); + + clonedCache.Add(keyAccount, new StorageItem(new NeoAccountState())); + ret = Check_Vote(clonedCache, account.ToArray(), ECCurve.Secp256r1.G.ToArray(), true, _persistingBlock); + Assert.IsFalse(ret.Result); + Assert.IsTrue(ret.State); + + var (_, _, vote_to_null) = GetAccountState(clonedCache, account); + Assert.IsNull(vote_to_null); + + clonedCache.Delete(keyAccount); + clonedCache.GetAndChange(keyAccount, () => new StorageItem(new NeoAccountState + { + Balance = 1, + VoteTo = ECCurve.Secp256r1.G + })); + clonedCache.Add(keyValidator, new StorageItem(new CandidateState() { Registered = true })); + ret = Check_Vote(clonedCache, account.ToArray(), ECCurve.Secp256r1.G.ToArray(), true, _persistingBlock); + Assert.IsTrue(ret.Result); + Assert.IsTrue(ret.State); + var (_, _, voteto) = GetAccountState(clonedCache, account); + Assert.AreEqual(ECCurve.Secp256r1.G.ToArray().ToHexString(), voteto.ToHexString()); + } + + internal (bool State, bool Result) Transfer4TesingOnBalanceChanging(BigInteger amount, bool addVotes) + { + var clonedCache = _snapshotCache.CloneCache(); + _persistingBlock.Header.Index = 1; + var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(UInt160.Zero), clonedCache, _persistingBlock, settings: TestProtocolSettings.Default); + ScriptBuilder sb = new(); + var tmp = engine.ScriptContainer!.GetScriptHashesForVerifying(engine.SnapshotCache); + UInt160 from = engine.ScriptContainer.GetScriptHashesForVerifying(engine.SnapshotCache)[0]; + if (addVotes) + { + clonedCache.Add(CreateStorageKey(20, from.ToArray()), new StorageItem(new NeoAccountState + { + VoteTo = ECCurve.Secp256r1.G, + Balance = new BigInteger(1000) + })); + clonedCache.Add(NativeContract.NEO.CreateStorageKey(33, ECCurve.Secp256r1.G), new StorageItem(new CandidateState())); + } + else + { + clonedCache.Add(CreateStorageKey(20, from.ToArray()), new StorageItem(new NeoAccountState + { + Balance = new BigInteger(1000) + })); + } + + sb.EmitDynamicCall(NativeContract.NEO.Hash, "transfer", from, UInt160.Zero, amount, null); + engine.LoadScript(sb.ToArray()); + var state = engine.Execute(); + Console.WriteLine($"{state} {engine.FaultException}"); + var result = engine.ResultStack.Peek(); + Assert.AreEqual(typeof(Boolean), result.GetType()); + return (true, result.GetBoolean()); + } + + internal static bool Check_OnPersist(DataCache clonedCache, Block persistingBlock) + { + var script = new ScriptBuilder(); + script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist); + var engine = ApplicationEngine.Create(TriggerType.OnPersist, null, clonedCache, persistingBlock, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + return engine.Execute() == VMState.HALT; + } + + internal static bool Check_PostPersist(DataCache clonedCache, Block persistingBlock) + { + using var script = new ScriptBuilder(); + script.EmitSysCall(ApplicationEngine.System_Contract_NativePostPersist); + using var engine = ApplicationEngine.Create(TriggerType.PostPersist, null, clonedCache, persistingBlock, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + return engine.Execute() == VMState.HALT; + } + + internal static (BigInteger Value, bool State) Check_GetGasPerBlock(DataCache clonedCache, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, clonedCache, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.NEO.Hash, "getGasPerBlock"); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() == VMState.FAULT) + { + return (BigInteger.Zero, false); + } + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return (((Integer)result).GetInteger(), true); + } + + internal static (Boolean Value, bool State) Check_SetGasPerBlock(DataCache clonedCache, BigInteger gasPerBlock, Block persistingBlock) + { + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(clonedCache); + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), clonedCache, persistingBlock, settings: TestProtocolSettings.Default); + + var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.NEO.Hash, "setGasPerBlock", gasPerBlock); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() == VMState.FAULT) + return (false, false); + + return (true, true); + } + + internal static (bool State, bool Result) Check_Vote(DataCache clonedCache, byte[] account, byte[] pubkey, bool signAccount, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(signAccount ? new UInt160(account) : UInt160.Zero), clonedCache, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.NEO.Hash, "vote", account, pubkey); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() == VMState.FAULT) + { + Console.WriteLine(engine.FaultException); + return (false, false); + } + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return (true, result.GetBoolean()); + } + + internal static (bool State, bool Result) Check_RegisterValidator(DataCache clonedCache, byte[] pubkey, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(Contract.CreateSignatureRedeemScript(ECPoint.DecodePoint(pubkey, ECCurve.Secp256r1)).ToScriptHash()), clonedCache, persistingBlock, settings: TestProtocolSettings.Default, gas: 1100_00000000); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.NEO.Hash, "registerCandidate", pubkey); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() == VMState.FAULT) + { + return (false, false); + } + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return (true, result.GetBoolean()); + } + + internal static (bool State, bool Result) Check_RegisterValidatorViaNEP27(DataCache clonedCache, ECPoint pubkey, Block persistingBlock, bool passNEO, byte[] data, BigInteger amount) + { + var keyScriptHash = Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash(); + var contractID = passNEO ? NativeContract.NEO.Id : NativeContract.GAS.Id; + var storageKey = new KeyBuilder(contractID, 20).Add(keyScriptHash); // 20 is Prefix_Account + + if (passNEO) + clonedCache.Add(storageKey, new StorageItem(new NeoAccountState { Balance = amount })); + else + clonedCache.Add(storageKey, new StorageItem(new AccountState { Balance = amount })); + + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(keyScriptHash), clonedCache, persistingBlock, settings: TestProtocolSettings.Default, gas: 1_0000_0000); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(passNEO ? NativeContract.NEO.Hash : NativeContract.GAS.Hash, "transfer", keyScriptHash, NativeContract.NEO.Hash, amount, data); + engine.LoadScript(script.ToArray()); + + var execRes = engine.Execute(); + clonedCache.Delete(storageKey); // Clean up for subsequent invocations. + + if (execRes == VMState.FAULT) + return (false, false); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return (true, result.GetBoolean()); + } + + internal static ECPoint[] Check_GetCommittee(DataCache clonedCache, Block? persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, clonedCache, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.NEO.Hash, "getCommittee"); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result, out var array); + + return array.Select(u => ECPoint.DecodePoint(u.GetSpan(), ECCurve.Secp256r1)).ToArray(); + } + + internal static (BigInteger Value, bool State) Check_UnclaimedGas(DataCache clonedCache, byte[] address, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, clonedCache, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.NEO.Hash, "unclaimedGas", address, persistingBlock.Index); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() == VMState.FAULT) + { + Console.WriteLine(engine.FaultException); + return (BigInteger.Zero, false); + } + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return (result.GetInteger(), true); + } + + internal static void CheckValidator(ECPoint eCPoint, KeyValuePair trackable) + { + BigInteger st = trackable.Value.Item; + Assert.AreEqual(0, st); + + CollectionAssert.AreEqual(new byte[] { 33 }.Concat(eCPoint.EncodePoint(true)).ToArray(), trackable.Key.Key.ToArray()); + } + + internal static void CheckBalance(byte[] account, KeyValuePair trackable, BigInteger balance, BigInteger height, ECPoint voteTo) + { + var st = (Struct)BinarySerializer.Deserialize(trackable.Value.Item.Value, ExecutionEngineLimits.Default); + + Assert.HasCount(3, st); + CollectionAssert.AreEqual(new Type[] { typeof(Integer), typeof(Integer), typeof(ByteString) }, st.Select(u => u.GetType()).ToArray()); // Balance + + Assert.AreEqual(balance, st[0].GetInteger()); // Balance + Assert.AreEqual(height, st[1].GetInteger()); // BalanceHeight + Assert.AreEqual(voteTo, ECPoint.DecodePoint(st[2].GetSpan(), ECCurve.Secp256r1)); // Votes + + CollectionAssert.AreEqual(new byte[] { 20 }.Concat(account).ToArray(), trackable.Key.ToArray()); + } + + internal static StorageKey CreateStorageKey(byte prefix, byte[]? key = null) + { + byte[] buffer = GC.AllocateUninitializedArray(sizeof(byte) + (key?.Length ?? 0)); + buffer[0] = prefix; + key?.CopyTo(buffer.AsSpan(1)); + return new() + { + Id = NativeContract.NEO.Id, + Key = buffer + }; + } + + internal static (bool State, bool Result) Check_UnregisterCandidate(DataCache clonedCache, byte[] pubkey, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(Contract.CreateSignatureRedeemScript(ECPoint.DecodePoint(pubkey, ECCurve.Secp256r1)).ToScriptHash()), clonedCache, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.NEO.Hash, "unregisterCandidate", pubkey); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() == VMState.FAULT) + { + return (false, false); + } + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return (true, result.GetBoolean()); + } + + internal static (BigInteger balance, BigInteger height, byte[]? voteto) GetAccountState(DataCache clonedCache, UInt160 account) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, clonedCache, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.NEO.Hash, "getAccountState", account); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result, out Struct state); + var balance = state[0].GetInteger(); + var height = state[1].GetInteger(); + var voteto = state[2].IsNull ? null : state[2].GetSpan().ToArray(); + return (balance, height, voteto); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs new file mode 100644 index 0000000000..855c863be6 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs @@ -0,0 +1,815 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Notary.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.Extensions.VM; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.UnitTests.Extensions; +using Neo.VM; +using Neo.VM.Types; +using Neo.Wallets; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_Notary +{ + private DataCache _snapshot = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshot = TestBlockchain.GetTestSnapshotCache(); + } + + [TestMethod] + public void Check_Name() + { + Assert.AreEqual(nameof(Notary), NativeContract.Notary.Name); + } + + [TestMethod] + public void Check_OnNEP17Payment() + { + var snapshot = _snapshot.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + var from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray(); + var to = NativeContract.Notary.Hash.ToArray(); + + // Set proper current index for deposit's Till parameter check. + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + snapshot.Add(storageKey, new(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + + // Non-GAS transfer should fail. + Assert.ThrowsExactly( + () => NativeContract.NEO.Transfer(snapshot, from, to, BigInteger.Zero, true, persistingBlock)); + + // GAS transfer with invalid data format should fail. + Assert.ThrowsExactly( + () => NativeContract.GAS.Transfer(snapshot, from, to, BigInteger.Zero, true, persistingBlock, 5)); + + // GAS transfer with wrong number of data elements should fail. + var data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { new() { Type = ContractParameterType.Boolean, Value = true } } + }; + Assert.ThrowsExactly( + () => NativeContract.GAS.Transfer(snapshot, from, to, BigInteger.Zero, true, persistingBlock, data)); + + // Gas transfer with invalid Till parameter should fail. + data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Any }, + new() { Type = ContractParameterType.Integer, Value = persistingBlock.Index } , + } + }; + Assert.ThrowsExactly( + () => NativeContract.GAS.TransferWithTransaction(snapshot, from, to, BigInteger.Zero, true, persistingBlock, data)); + + // Insufficient first deposit. + data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Any }, + new() { Type = ContractParameterType.Integer, Value = persistingBlock.Index + 100 }, + } + }; + Assert.ThrowsExactly( + () => NativeContract.GAS.TransferWithTransaction(snapshot, from, to, 2 * 1000_0000 - 1, true, persistingBlock, data)); + + // Good deposit. + data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Any }, + new() { Type = ContractParameterType.Integer, Value = persistingBlock.Index + 100 }, + } + }; + Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, to, 2 * 1000_0000 + 1, true, persistingBlock, data)); + } + + [TestMethod] + public void Check_ExpirationOf() + { + var snapshot = _snapshot.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + var from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray(); + var ntr = NativeContract.Notary.Hash.ToArray(); + + // Set proper current index for deposit's Till parameter check. + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + snapshot.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + + // Check that 'till' of an empty deposit is 0 by default. + Assert.AreEqual(0, Call_ExpirationOf(snapshot, from, persistingBlock)); + + // Make initial deposit. + var till = persistingBlock.Index + 123; + var data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Any }, + new() { Type = ContractParameterType.Integer, Value = till }, + } + }; + Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, ntr, 2 * 1000_0000 + 1, true, persistingBlock, data)); + + // Ensure deposit's 'till' value is properly set. + Assert.AreEqual(till, Call_ExpirationOf(snapshot, from, persistingBlock)); + + // Make one more deposit with updated 'till' parameter. + till += 5; + data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Any }, + new() { Type = ContractParameterType.Integer, Value = till }, + } + }; + Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, ntr, 5, true, persistingBlock, data)); + + // Ensure deposit's 'till' value is properly updated. + Assert.AreEqual(till, Call_ExpirationOf(snapshot, from, persistingBlock)); + + // Make deposit to some side account with custom 'till' value. + var to = UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4"); + data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Hash160, Value = to }, + new() { Type = ContractParameterType.Integer, Value = till }, + } + }; + Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, ntr, 2 * 1000_0000 + 1, true, persistingBlock, data)); + + // Default 'till' value should be set for to's deposit. + var defaultDeltaTill = 5760; + var expectedTill = persistingBlock.Index - 1 + defaultDeltaTill; + Assert.AreEqual(expectedTill, Call_ExpirationOf(snapshot, to.ToArray(), persistingBlock)); + + // Withdraw own deposit. + persistingBlock.Header.Index = till + 1; + var currentBlock = snapshot.GetAndChange(storageKey, () => new StorageItem(new HashIndexState())); + currentBlock.GetInteroperable().Index = till + 1; + Call_Withdraw(snapshot, from, from, persistingBlock); + + // Check that 'till' value is properly updated. + Assert.AreEqual(0, Call_ExpirationOf(snapshot, from, persistingBlock)); + } + + [TestMethod] + public void Check_LockDepositUntil() + { + var snapshot = _snapshot.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + var from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray(); + + // Set proper current index for deposit's Till parameter check. + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + snapshot.Add(storageKey, new(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + + // Check that 'till' of an empty deposit is 0 by default. + Assert.AreEqual(0, Call_ExpirationOf(snapshot, from, persistingBlock)); + + // Update `till` value of an empty deposit should fail. + Assert.IsFalse(Call_LockDepositUntil(snapshot, from, 123, persistingBlock)); + Assert.AreEqual(0, Call_ExpirationOf(snapshot, from, persistingBlock)); + + // Make initial deposit. + var till = persistingBlock.Index + 123; + var data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Any }, + new() { Type = ContractParameterType.Integer, Value = till }, + } + }; + + var hash = NativeContract.Notary.Hash.ToArray(); + Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, hash, 2 * 1000_0000 + 1, true, persistingBlock, data)); + + // Ensure deposit's 'till' value is properly set. + Assert.AreEqual(till, Call_ExpirationOf(snapshot, from, persistingBlock)); + + // Update deposit's `till` value for side account should fail. + UInt160 other = UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4"); + Assert.IsFalse(Call_LockDepositUntil(snapshot, other.ToArray(), till + 10, persistingBlock)); + Assert.AreEqual(till, Call_ExpirationOf(snapshot, from, persistingBlock)); + + // Decrease deposit's `till` value should fail. + Assert.IsFalse(Call_LockDepositUntil(snapshot, from, till - 1, persistingBlock)); + Assert.AreEqual(till, Call_ExpirationOf(snapshot, from, persistingBlock)); + + // Good. + till += 10; + Assert.IsTrue(Call_LockDepositUntil(snapshot, from, till, persistingBlock)); + Assert.AreEqual(till, Call_ExpirationOf(snapshot, from, persistingBlock)); + } + + [TestMethod] + public void Check_BalanceOf() + { + var snapshot = _snapshot.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + var fromAddr = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators); + var from = fromAddr.ToArray(); + var hash = NativeContract.Notary.Hash.ToArray(); + + // Set proper current index for deposit expiration. + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + snapshot.Add(storageKey, new(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + + // Ensure that default deposit is 0. + Assert.AreEqual(0, Call_BalanceOf(snapshot, from, persistingBlock)); + + // Make initial deposit. + var till = persistingBlock.Index + 123; + var deposit1 = 2 * 1_0000_0000; + var data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Any }, + new() { Type = ContractParameterType.Integer, Value = till }, + } + }; + Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, hash, deposit1, true, persistingBlock, data)); + + // Ensure value is deposited. + Assert.AreEqual(deposit1, Call_BalanceOf(snapshot, from, persistingBlock)); + + // Make one more deposit with updated 'till' parameter. + var deposit2 = 5; + data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Any }, + new() { Type = ContractParameterType.Integer, Value = till }, + } + }; + Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, hash, deposit2, true, persistingBlock, data)); + + // Ensure deposit's 'till' value is properly updated. + Assert.AreEqual(deposit1 + deposit2, Call_BalanceOf(snapshot, from, persistingBlock)); + + // Make deposit to some side account. + UInt160 to = UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4"); + data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Hash160, Value = to }, + new() { Type = ContractParameterType.Integer, Value = till }, + } + }; + Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, hash, deposit1, true, persistingBlock, data)); + + Assert.AreEqual(deposit1, Call_BalanceOf(snapshot, to.ToArray(), persistingBlock)); + + // Process some Notary transaction and check that some deposited funds have been withdrawn. + var tx1 = TestUtils.GetTransaction(NativeContract.Notary.Hash, fromAddr); + tx1.Attributes = [new NotaryAssisted() { NKeys = 4 }]; + tx1.NetworkFee = 1_0000_0000; + + // Build block to check transaction fee distribution during Gas OnPersist. + persistingBlock = new Block + { + Header = new Header + { + Index = (uint)TestProtocolSettings.Default.CommitteeMembersCount, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero, + Witness = Witness.Empty, + }, + Transactions = [tx1] + }; + + // Designate Notary node. + var privateKey1 = new byte[32]; + var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); + rng.GetBytes(privateKey1); + var key1 = new KeyPair(privateKey1); + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + var ret = NativeContract.RoleManagement.Call( + snapshot, + new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), + new Block + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = [] + }, + "designateAsRole", + new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger((int)Role.P2PNotary) }, + new ContractParameter(ContractParameterType.Array) + { + Value = new List(){ + new(ContractParameterType.ByteArray){Value = key1.PublicKey.ToArray()}, + }, + } + ); + snapshot.Commit(); + + // Execute OnPersist script. + var script = new ScriptBuilder(); + script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist); + var engine = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + snapshot.Commit(); + + // Check that transaction's fees were paid by from's deposit. + var expectedBalance = deposit1 + deposit2 - tx1.NetworkFee - tx1.SystemFee; + Assert.AreEqual(expectedBalance, Call_BalanceOf(snapshot, from, persistingBlock)); + + // Withdraw own deposit. + persistingBlock.Header.Index = till + 1; + var currentBlock = snapshot.GetAndChange(storageKey, () => new StorageItem(new HashIndexState())); + currentBlock.GetInteroperable().Index = till + 1; + Call_Withdraw(snapshot, from, from, persistingBlock); + + // Check that no deposit is left. + Assert.AreEqual(0, Call_BalanceOf(snapshot, from, persistingBlock)); + } + + [TestMethod] + public void Check_Withdraw() + { + var snapshot = _snapshot.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + var fromAddr = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators); + var from = fromAddr.ToArray(); + + // Set proper current index to get proper deposit expiration height. + var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); + snapshot.Add(storageKey, new(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 })); + + // Ensure that default deposit is 0. + Assert.AreEqual(0, Call_BalanceOf(snapshot, from, persistingBlock)); + + // Make initial deposit. + var till = persistingBlock.Index + 123; + var deposit1 = 2 * 1_0000_0000; + var data = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new List() { + new() { Type = ContractParameterType.Any }, + new() { Type = ContractParameterType.Integer, Value = till }, + } + }; + + var hash = NativeContract.Notary.Hash.ToArray(); + Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, hash, deposit1, true, persistingBlock, data)); + + // Ensure value is deposited. + Assert.AreEqual(deposit1, Call_BalanceOf(snapshot, from, persistingBlock)); + + // Unwitnessed withdraw should fail. + var sideAccount = UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4"); + Assert.IsFalse(Call_Withdraw(snapshot, from, sideAccount.ToArray(), persistingBlock, false)); + + // Withdraw missing (zero) deposit should fail. + Assert.IsFalse(Call_Withdraw(snapshot, sideAccount.ToArray(), sideAccount.ToArray(), persistingBlock)); + + // Withdraw before deposit expiration should fail. + Assert.IsFalse(Call_Withdraw(snapshot, from, from, persistingBlock)); + + // Good. + persistingBlock.Header.Index = till + 1; + var currentBlock = snapshot.GetAndChange(storageKey, () => new StorageItem(new HashIndexState())); + currentBlock.GetInteroperable().Index = till + 1; + Assert.IsTrue(Call_Withdraw(snapshot, from, from, persistingBlock)); + + // Check that no deposit is left. + Assert.AreEqual(0, Call_BalanceOf(snapshot, from, persistingBlock)); + } + + internal static BigInteger Call_BalanceOf(DataCache snapshot, byte[] address, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.Notary.Hash, "balanceOf", address); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetInteger(); + } + + internal static BigInteger Call_ExpirationOf(DataCache snapshot, byte[] address, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.Notary.Hash, "expirationOf", address); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetInteger(); + } + + internal static bool Call_LockDepositUntil(DataCache snapshot, byte[] address, uint till, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Transaction() + { + Signers = [new() { Account = new UInt160(address), Scopes = WitnessScope.Global }], + Attributes = [], + Witnesses = null! + }, + snapshot, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.Notary.Hash, "lockDepositUntil", address, till); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetBoolean(); + } + + internal static bool Call_Withdraw(DataCache snapshot, byte[] from, byte[] to, Block persistingBlock, bool witnessedByFrom = true) + { + var accFrom = UInt160.Zero; + if (witnessedByFrom) + { + accFrom = new UInt160(from); + } + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Transaction() + { + Signers = [new() { Account = accFrom, Scopes = WitnessScope.Global }], + Attributes = [], + Witnesses = null! + }, + snapshot, persistingBlock, settings: TestProtocolSettings.Default); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.Notary.Hash, "withdraw", from, to); + engine.LoadScript(script.ToArray()); + + if (engine.Execute() != VMState.HALT) + { + throw engine.FaultException!; + } + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + return result.GetBoolean(); + } + + [TestMethod] + public void Check_GetMaxNotValidBeforeDelta() + { + const uint defaultMaxNotValidBeforeDelta = 140; + Assert.AreEqual(defaultMaxNotValidBeforeDelta, NativeContract.Notary.GetMaxNotValidBeforeDelta(_snapshot)); + } + + [TestMethod] + public void Check_SetMaxNotValidBeforeDelta() + { + var snapshot = _snapshot.CloneCache(); + var persistingBlock = new Block + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + var committeeAddress = NativeContract.NEO.GetCommitteeAddress(snapshot); + + using var engine = ApplicationEngine.Create(TriggerType.Application, + new Nep17NativeContractExtensions.ManualWitness(committeeAddress), + snapshot, persistingBlock, settings: TestProtocolSettings.Default); + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.Notary.Hash, "setMaxNotValidBeforeDelta", 100); + engine.LoadScript(script.ToArray()); + + var vMState = engine.Execute(); + Assert.AreEqual(VMState.HALT, vMState); + Assert.AreEqual(100u, NativeContract.Notary.GetMaxNotValidBeforeDelta(snapshot)); + } + + [TestMethod] + public void Check_OnPersist_FeePerKeyUpdate() + { + // Hardcode test values. + const uint defaultNotaryAssistedFeePerKey = 1000_0000; + const uint newNotaryAssistedFeePerKey = 5000_0000; + const byte NKeys = 4; + + // Generate one transaction with NotaryAssisted attribute with hardcoded NKeys values. + var from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators); + var tx2 = TestUtils.GetTransaction(from); + tx2.Attributes = [new NotaryAssisted() { NKeys = NKeys }]; + + var netFee = 1_0000_0000; // enough to cover defaultNotaryAssistedFeePerKey, but not enough to cover newNotaryAssistedFeePerKey. + tx2.NetworkFee = netFee; + tx2.SystemFee = 1000_0000; + + // Calculate overall expected Notary nodes reward. + var expectedNotaryReward = (NKeys + 1) * defaultNotaryAssistedFeePerKey; + + // Build block to check transaction fee distribution during Gas OnPersist. + var persistingBlock = new Block + { + Header = new Header + { + Index = 10, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero, + Witness = Witness.Empty, + }, + Transactions = [tx2] + }; + var snapshot = _snapshot.CloneCache(); + + // Designate Notary node. + var privateKey1 = new byte[32]; + var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); + rng.GetBytes(privateKey1); + + var key1 = new KeyPair(privateKey1); + var committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + var ret = NativeContract.RoleManagement.Call( + snapshot, + new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), + new Block + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = [] + }, + "designateAsRole", + new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger((int)Role.P2PNotary) }, + new ContractParameter(ContractParameterType.Array) + { + Value = new List(){ + new(ContractParameterType.ByteArray) { Value = key1.PublicKey.ToArray() }, + }, + } + ); + snapshot.Commit(); + + // Create engine with custom settings (HF_Echidna should be enabled to properly interact with NotaryAssisted attribute). + var settings = ProtocolSettings.Default with + { + Network = 0x334F454Eu, + StandbyCommittee = [ + ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1), + ECPoint.Parse("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", ECCurve.Secp256r1), + ECPoint.Parse("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", ECCurve.Secp256r1), + ECPoint.Parse("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", ECCurve.Secp256r1), + ECPoint.Parse("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", ECCurve.Secp256r1), + ECPoint.Parse("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", ECCurve.Secp256r1), + ECPoint.Parse("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", ECCurve.Secp256r1) + ], + ValidatorsCount = 7, + Hardforks = [] + }; + + // Imitate Blockchain's Persist behaviour: OnPersist + transactions processing. + // Execute OnPersist firstly: + var script = new ScriptBuilder(); + script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist); + + var engine = ApplicationEngine.Create(TriggerType.OnPersist, + new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), + snapshot, persistingBlock, settings: settings); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute(), engine.FaultException?.ToString()); + snapshot.Commit(); + + // Process transaction that changes NotaryServiceFeePerKey after OnPersist. + ret = NativeContract.Policy.Call(engine, "setAttributeFee", + new(ContractParameterType.Integer) { Value = (BigInteger)(byte)TransactionAttributeType.NotaryAssisted }, + new(ContractParameterType.Integer) { Value = newNotaryAssistedFeePerKey }); + Assert.IsNull(ret); + snapshot.Commit(); + + // Process tx2 with NotaryAssisted attribute. + engine = ApplicationEngine.Create(TriggerType.Application, tx2, snapshot, persistingBlock, settings: TestProtocolSettings.Default, tx2.SystemFee); + engine.LoadScript(tx2.Script); + Assert.AreEqual(VMState.HALT, engine.Execute()); + snapshot.Commit(); + + // Ensure that Notary reward is distributed based on the old value of NotaryAssisted price + // and no underflow happens during GAS distribution. + var validators = NativeContract.NEO.GetNextBlockValidators(engine.SnapshotCache, engine.ProtocolSettings.ValidatorsCount); + var primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock!.PrimaryIndex]).ToScriptHash(); + Assert.AreEqual(netFee - expectedNotaryReward, NativeContract.GAS.BalanceOf(snapshot, primary)); + + var scriptHash = Contract.CreateSignatureRedeemScript(key1.PublicKey).ToScriptHash(); + Assert.AreEqual(expectedNotaryReward, NativeContract.GAS.BalanceOf(engine.SnapshotCache, scriptHash)); + } + + [TestMethod] + public void Check_OnPersist_NotaryRewards() + { + // Hardcode test values. + const uint defaultNotaryssestedFeePerKey = 1000_0000; + const byte NKeys1 = 4; + const byte NKeys2 = 6; + + // Generate two transactions with NotaryAssisted attributes with hardcoded NKeys values. + var from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators); + var tx1 = TestUtils.GetTransaction(from); + tx1.Attributes = [new NotaryAssisted() { NKeys = NKeys1 }]; + + var netFee1 = 1_0000_0000; + tx1.NetworkFee = netFee1; + + var tx2 = TestUtils.GetTransaction(from); + tx2.Attributes = [new NotaryAssisted() { NKeys = NKeys2 }]; + var netFee2 = 2_0000_0000; + tx2.NetworkFee = netFee2; + + // Calculate overall expected Notary nodes reward. + var expectedNotaryReward = (NKeys1 + 1) * defaultNotaryssestedFeePerKey + (NKeys2 + 1) * defaultNotaryssestedFeePerKey; + + // Build block to check transaction fee distribution during Gas OnPersist. + var persistingBlock = new Block + { + Header = new Header + { + Index = (uint)TestProtocolSettings.Default.CommitteeMembersCount, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + PrevHash = UInt256.Zero, + Witness = Witness.Empty, + }, + Transactions = [tx1, tx2] + }; + var snapshot = _snapshot.CloneCache(); + + // Designate several Notary nodes. + var privateKey1 = new byte[32]; + var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); + rng.GetBytes(privateKey1); + + var key1 = new KeyPair(privateKey1); + var privateKey2 = new byte[32]; + rng.GetBytes(privateKey2); + + var key2 = new KeyPair(privateKey2); + var committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + var ret = NativeContract.RoleManagement.Call( + snapshot, + new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), + new Block + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = [] + }, + "designateAsRole", + new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger((int)Role.P2PNotary) }, + new ContractParameter(ContractParameterType.Array) + { + Value = new List(){ + new(ContractParameterType.ByteArray) { Value = key1.PublicKey.ToArray() }, + new(ContractParameterType.ByteArray) { Value = key2.PublicKey.ToArray() }, + }, + } + ); + snapshot.Commit(); + + var script = new ScriptBuilder(); + script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist); + var engine = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default); + + // Check that block's Primary balance is 0. + var validators = NativeContract.NEO.GetNextBlockValidators(engine.SnapshotCache, engine.ProtocolSettings.ValidatorsCount); + var primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock!.PrimaryIndex]).ToScriptHash(); + Assert.AreEqual(0, NativeContract.GAS.BalanceOf(engine.SnapshotCache, primary)); + + // Execute OnPersist script. + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + + // Check that proper amount of GAS was minted to block's Primary and the rest + // is evenly devided between designated Notary nodes as a reward. + // burn tx1 and tx2 network fee + mint primary reward + transfer reward to Notary1 and Notary2 + Assert.HasCount(2 + 1 + 2, engine.Notifications); + Assert.AreEqual(netFee1 + netFee2 - expectedNotaryReward, engine.Notifications[2].State[2]); + Assert.AreEqual(netFee1 + netFee2 - expectedNotaryReward, NativeContract.GAS.BalanceOf(engine.SnapshotCache, primary)); + Assert.AreEqual(expectedNotaryReward / 2, engine.Notifications[3].State[2]); + + var scriptHash1 = Contract.CreateSignatureRedeemScript(key1.PublicKey).ToScriptHash(); + Assert.AreEqual(expectedNotaryReward / 2, NativeContract.GAS.BalanceOf(engine.SnapshotCache, scriptHash1)); + Assert.AreEqual(expectedNotaryReward / 2, engine.Notifications[4].State[2]); + + var scriptHash2 = Contract.CreateSignatureRedeemScript(key2.PublicKey).ToScriptHash(); + Assert.AreEqual(expectedNotaryReward / 2, NativeContract.GAS.BalanceOf(engine.SnapshotCache, scriptHash2)); + } + + internal static StorageKey CreateStorageKey(byte prefix, uint key) + { + return CreateStorageKey(prefix, BitConverter.GetBytes(key)); + } + + internal static StorageKey CreateStorageKey(byte prefix, byte[]? key = null) + { + var buffer = GC.AllocateUninitializedArray(sizeof(byte) + (key?.Length ?? 0)); + buffer[0] = prefix; + key?.CopyTo(buffer.AsSpan(1)); + return new() { Id = NativeContract.GAS.Id, Key = buffer }; + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs new file mode 100644 index 0000000000..c6b611e0ec --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs @@ -0,0 +1,600 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_PolicyContract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.Extensions.VM; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Iterators; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native; +using Neo.UnitTests.Extensions; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; +using Boolean = Neo.VM.Types.Boolean; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_PolicyContract +{ + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + } + + [TestMethod] + public void Check_Default() + { + var snapshot = _snapshotCache.CloneCache(); + + var ret = NativeContract.Policy.Call(snapshot, "getFeePerByte"); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(1000, ret.GetInteger()); + + ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", new ContractParameter(ContractParameterType.Integer) { Value = (BigInteger)(byte)TransactionAttributeType.Conflicts }); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(PolicyContract.DefaultAttributeFee, ret.GetInteger()); + + Assert.ThrowsExactly(() => _ = NativeContract.Policy.Call(snapshot, "getAttributeFee", new ContractParameter(ContractParameterType.Integer) { Value = (BigInteger)byte.MaxValue })); + } + + [TestMethod] + public void Check_SetAttributeFee() + { + var snapshot = _snapshotCache.CloneCache(); + + // Fake blockchain + Block block = new() + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + + var attr = new ContractParameter(ContractParameterType.Integer) { Value = (BigInteger)(byte)TransactionAttributeType.Conflicts }; + + // Without signature + Assert.ThrowsExactly(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(), block, + "setAttributeFee", attr, new ContractParameter(ContractParameterType.Integer) { Value = 100500 }); + }); + + var ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", attr); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(0, ret.GetInteger()); + + // With signature, wrong value + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + Assert.ThrowsExactly(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setAttributeFee", attr, new ContractParameter(ContractParameterType.Integer) { Value = 11_0000_0000 }); + }); + + ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", attr); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(0, ret.GetInteger()); + + // Proper set + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setAttributeFee", attr, new ContractParameter(ContractParameterType.Integer) { Value = 300300 }); + Assert.IsInstanceOfType(ret); + + ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", attr); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(300300, ret.GetInteger()); + + // Set to zero + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setAttributeFee", attr, new ContractParameter(ContractParameterType.Integer) { Value = 0 }); + Assert.IsInstanceOfType(ret); + + ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", attr); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(0, ret.GetInteger()); + } + + [TestMethod] + public void Check_SetFeePerByte() + { + var snapshot = _snapshotCache.CloneCache(); + + // Fake blockchain + + Block block = new() + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + + // Without signature + + Assert.ThrowsExactly(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(), block, + "setFeePerByte", new ContractParameter(ContractParameterType.Integer) { Value = 1 }); + }); + + var ret = NativeContract.Policy.Call(snapshot, "getFeePerByte"); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(1000, ret.GetInteger()); + + // With signature + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setFeePerByte", new ContractParameter(ContractParameterType.Integer) { Value = 1 }); + Assert.IsInstanceOfType(ret); + + ret = NativeContract.Policy.Call(snapshot, "getFeePerByte"); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(1, ret.GetInteger()); + } + + [TestMethod] + public void Check_SetBaseExecFee() + { + var snapshot = _snapshotCache.CloneCache(); + + // Fake blockchain + + Block block = new() + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + + // Without signature + + Assert.ThrowsExactly(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(), block, + "setExecFeeFactor", new ContractParameter(ContractParameterType.Integer) { Value = 50 }); + }); + + var ret = NativeContract.Policy.Call(snapshot, "getExecFeeFactor"); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(30, ret.GetInteger()); + + // With signature, wrong value + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + Assert.ThrowsExactly(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setExecFeeFactor", new ContractParameter(ContractParameterType.Integer) { Value = 100500 }); + }); + + ret = NativeContract.Policy.Call(snapshot, "getExecFeeFactor"); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(30, ret.GetInteger()); + + // Proper set + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setExecFeeFactor", new ContractParameter(ContractParameterType.Integer) { Value = 50 }); + Assert.IsInstanceOfType(ret); + + ret = NativeContract.Policy.Call(snapshot, "getExecFeeFactor"); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(50, ret.GetInteger()); + } + + [TestMethod] + public void Check_SetStoragePrice() + { + var snapshot = _snapshotCache.CloneCache(); + + // Fake blockchain + + Block block = new() + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + + // Without signature + + Assert.ThrowsExactly(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(), block, + "setStoragePrice", new ContractParameter(ContractParameterType.Integer) { Value = 100500 }); + }); + + var ret = NativeContract.Policy.Call(snapshot, "getStoragePrice"); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(100000, ret.GetInteger()); + + // With signature, wrong value + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + Assert.ThrowsExactly(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setStoragePrice", new ContractParameter(ContractParameterType.Integer) { Value = 100000000 }); + }); + + ret = NativeContract.Policy.Call(snapshot, "getStoragePrice"); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(100000, ret.GetInteger()); + + // Proper set + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setStoragePrice", new ContractParameter(ContractParameterType.Integer) { Value = 300300 }); + Assert.IsInstanceOfType(ret); + + ret = NativeContract.Policy.Call(snapshot, "getStoragePrice"); + Assert.IsInstanceOfType(ret); + Assert.AreEqual(300300, ret.GetInteger()); + } + + [TestMethod] + public void Check_BlockAccount() + { + var snapshot = _snapshotCache.CloneCache(); + + // Fake blockchain + + Block block = new() + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + + // Without signature + + Assert.ThrowsExactly(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(UInt160.Zero), block, + "blockAccount", + new ContractParameter(ContractParameterType.ByteArray) { Value = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01").ToArray() }); + }); + + // With signature + + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + var ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "blockAccount", + new ContractParameter(ContractParameterType.ByteArray) { Value = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01").ToArray() }); + Assert.IsInstanceOfType(ret); + Assert.IsTrue(ret.GetBoolean()); + + // Same account + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "blockAccount", + new ContractParameter(ContractParameterType.ByteArray) { Value = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01").ToArray() }); + Assert.IsInstanceOfType(ret); + Assert.IsFalse(ret.GetBoolean()); + + // Account B + + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "blockAccount", + new ContractParameter(ContractParameterType.ByteArray) { Value = UInt160.Parse("0xb400ff00ff00ff00ff00ff00ff00ff00ff00ff01").ToArray() }); + Assert.IsInstanceOfType(ret); + Assert.IsTrue(ret.GetBoolean()); + + // Check + + Assert.IsFalse(NativeContract.Policy.IsBlocked(snapshot, UInt160.Zero)); + Assert.IsTrue(NativeContract.Policy.IsBlocked(snapshot, UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01"))); + Assert.IsTrue(NativeContract.Policy.IsBlocked(snapshot, UInt160.Parse("0xb400ff00ff00ff00ff00ff00ff00ff00ff00ff01"))); + } + + [TestMethod] + public void Check_Block_UnblockAccount() + { + var snapshot = _snapshotCache.CloneCache(); + + // Fake blockchain + + Block block = new() + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + + // Block without signature + + Assert.ThrowsExactly(() => + { + var ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(), block, + "blockAccount", new ContractParameter(ContractParameterType.Hash160) { Value = UInt160.Zero }); + }); + + Assert.IsFalse(NativeContract.Policy.IsBlocked(snapshot, UInt160.Zero)); + + // Block with signature + + var ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "blockAccount", new ContractParameter(ContractParameterType.Hash160) { Value = UInt160.Zero }); + Assert.IsInstanceOfType(ret); + Assert.IsTrue(ret.GetBoolean()); + + Assert.IsTrue(NativeContract.Policy.IsBlocked(snapshot, UInt160.Zero)); + + // Unblock without signature + + Assert.ThrowsExactly(() => + { + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(), block, + "unblockAccount", new ContractParameter(ContractParameterType.Hash160) { Value = UInt160.Zero }); + }); + + Assert.IsTrue(NativeContract.Policy.IsBlocked(snapshot, UInt160.Zero)); + + // Unblock with signature + + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "unblockAccount", new ContractParameter(ContractParameterType.Hash160) { Value = UInt160.Zero }); + Assert.IsInstanceOfType(ret); + Assert.IsTrue(ret.GetBoolean()); + + Assert.IsFalse(NativeContract.Policy.IsBlocked(snapshot, UInt160.Zero)); + } + + [TestMethod] + public void TestListBlockedAccounts() + { + var snapshot = _snapshotCache.CloneCache(); + + // Fake blockchain + + Block block = new() + { + Header = new Header + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Index = 1000, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + + var ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "blockAccount", new ContractParameter(ContractParameterType.Hash160) { Value = UInt160.Zero }); + Assert.IsInstanceOfType(ret); + Assert.IsTrue(ret.GetBoolean()); + + Assert.IsTrue(NativeContract.Policy.IsBlocked(snapshot, UInt160.Zero)); + + var sb = new ScriptBuilder() + .EmitDynamicCall(NativeContract.Policy.Hash, "getBlockedAccounts"); + + var engine = ApplicationEngine.Run(sb.ToArray(), snapshot, null, block, TestBlockchain.GetSystem().Settings); + + Assert.IsInstanceOfType(engine.ResultStack[0]); + + var iter = engine.ResultStack[0].GetInterface()!; + Assert.IsTrue(iter.Next()); + Assert.AreEqual(new UInt160(iter.Value(new ReferenceCounter()).GetSpan()), UInt160.Zero); + } + + [TestMethod] + public void TestWhiteListFee() + { + // Create script + + var snapshotCache = _snapshotCache.CloneCache(); + + byte[] script; + using (var sb = new ScriptBuilder()) + { + sb.EmitDynamicCall(NativeContract.NEO.Hash, "balanceOf", NativeContract.NEO.GetCommitteeAddress(_snapshotCache.CloneCache())); + script = sb.ToArray(); + } + + var engine = CreateEngineWithCommitteeSigner(snapshotCache, script); + + // Not whitelisted + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.AreEqual(0, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(2028330, engine.FeeConsumed); + Assert.AreEqual(0, NativeContract.Policy.CleanWhitelist(engine, NativeContract.NEO.GetContractState(ProtocolSettings.Default, 0))); + Assert.IsEmpty(engine.Notifications); + + // Whitelist + + engine = CreateEngineWithCommitteeSigner(snapshotCache, script); + + NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "balanceOf", 1, 0); + engine.SnapshotCache.Commit(); + + // Whitelisted + + Assert.HasCount(1, engine.Notifications); // Whitelist changed + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.AreEqual(0, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(1045290, engine.FeeConsumed); + + // Clean white list + + engine.SnapshotCache.Commit(); + engine = CreateEngineWithCommitteeSigner(snapshotCache, script); + + Assert.AreEqual(1, NativeContract.Policy.CleanWhitelist(engine, NativeContract.NEO.GetContractState(ProtocolSettings.Default, 0))); + Assert.HasCount(1, engine.Notifications); // Whitelist deleted + } + + [TestMethod] + public void TestSetWhiteListFeeContractNegativeFixedFee() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + + // Register a dummy contract + UInt160 contractHash; + using (var sb = new ScriptBuilder()) + { + sb.Emit(OpCode.RET); + var script = sb.ToArray(); + contractHash = script.ToScriptHash(); + snapshotCache.DeleteContract(contractHash); + var manifest = TestUtils.CreateManifest("dummy", ContractParameterType.Any); + manifest.Abi.Methods = [ + new ContractMethodDescriptor + { + Name = "foo", + Parameters = [], + ReturnType = ContractParameterType.Any, + Offset = 0, + Safe = false + } + ]; + + var contract = TestUtils.GetContract(script, manifest); + snapshotCache.AddContract(contractHash, contract); + } + + // Invoke SetWhiteListFeeContract with fixedFee negative + + Assert.Throws(() => NativeContract.Policy.SetWhitelistFeeContract(engine, contractHash, "foo", 1, -1L)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractWhenContractNotFound() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + var randomHash = new UInt160(Crypto.Hash160([1, 2, 3]).ToArray()); + Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, randomHash, "transfer", 3, 10)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractWhenContractNotInAbi() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "noexists", 0, 10)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractWhenArgCountMismatch() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + // transfer exists with 4 args + Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "transfer", 0, 10)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractWhenNotCommittee() + { + var snapshotCache = _snapshotCache.CloneCache(); + var tx = new Transaction + { + Version = 0, + Nonce = 1, + Signers = [new() { Account = UInt160.Zero, Scopes = WitnessScope.Global }], + Attributes = [], + Witnesses = [new Witness { }], + Script = new byte[1], + NetworkFee = 0, + SystemFee = 0, + ValidUntilBlock = 0 + }; + + using var engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshotCache, settings: TestProtocolSettings.Default); + Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "transfer", 4, 10)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractSetContract() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "transfer", 4, 123_456); + + var method = NativeContract.NEO.GetContractState(ProtocolSettings.Default, 0) + .Manifest.Abi.Methods.Where(u => u.Name == "balanceOf").Single(); + + NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, method.Name, method.Parameters.Length, 123_456); + Assert.IsTrue(NativeContract.Policy.IsWhitelistFeeContract(engine.SnapshotCache, NativeContract.NEO.Hash, method, out var fixedFee)); + Assert.AreEqual(123_456, fixedFee); + } + + private static ApplicationEngine CreateEngineWithCommitteeSigner(DataCache snapshotCache, byte[]? script = null) + { + // Get committe public keys and calculate m + var committee = NativeContract.NEO.GetCommittee(snapshotCache); + var m = (committee.Length / 2) + 1; + var committeeContract = Contract.CreateMultiSigContract(m, committee); + + // Create Tx needed for CheckWitness / CheckCommittee + var tx = new Transaction + { + Version = 0, + Nonce = 1, + Signers = [new() { Account = committeeContract.ScriptHash, Scopes = WitnessScope.Global }], + Attributes = [], + Witnesses = [new Witness { InvocationScript = new byte[1], VerificationScript = committeeContract.Script }], + Script = script ?? [(byte)OpCode.NOP], + NetworkFee = 0, + SystemFee = 0, + ValidUntilBlock = 0 + }; + + var engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(tx.Script); + + return engine; + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_RoleManagement.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_RoleManagement.cs new file mode 100644 index 0000000000..d0c00a977e --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_RoleManagement.cs @@ -0,0 +1,106 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_RoleManagement.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.IO; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.UnitTests.Extensions; +using Neo.Wallets; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using Array = Neo.VM.Types.Array; +using ECPoint = Neo.Cryptography.ECC.ECPoint; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_RoleManagement +{ + [TestInitialize] + public void TestSetup() + { + var system = TestBlockchain.GetSystem(); + system.ResetStore(); + } + + [TestMethod] + public void TestSetAndGet() + { + var privateKey1 = new byte[32]; + var rng1 = RandomNumberGenerator.Create(); + rng1.GetBytes(privateKey1); + var key1 = new KeyPair(privateKey1); + var privateKey2 = new byte[32]; + var rng2 = RandomNumberGenerator.Create(); + rng2.GetBytes(privateKey2); + var key2 = new KeyPair(privateKey2); + var publicKeys = new ECPoint[2]; + publicKeys[0] = key1.PublicKey; + publicKeys[1] = key2.PublicKey; + publicKeys = [.. publicKeys.OrderBy(p => p)]; + + List roles = [Role.StateValidator, Role.Oracle, Role.NeoFSAlphabetNode, Role.P2PNotary]; + foreach (var role in roles) + { + var system = new TestBlockchain.TestNeoSystem(TestProtocolSettings.Default); + + var snapshot1 = system.GetTestSnapshotCache(false); + var committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot1); + List notifications = []; + void Ev(ApplicationEngine o, NotifyEventArgs e) => notifications.Add(e); + + var ret = NativeContract.RoleManagement.Call( + snapshot1, + new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), + new Block + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = [] + }, + "designateAsRole", Ev, + new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger((int)role) }, + new ContractParameter(ContractParameterType.Array) { Value = publicKeys.Select(p => new ContractParameter(ContractParameterType.ByteArray) { Value = p.ToArray() }).ToList() } + ); + snapshot1.Commit(); + Assert.HasCount(1, notifications); + Assert.AreEqual("Designation", notifications[0].EventName); + + var snapshot2 = system.GetTestSnapshotCache(false); + + ret = NativeContract.RoleManagement.Call( + snapshot2, + "getDesignatedByRole", + new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger((int)role) }, + new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger(1u) } + ); + Assert.IsInstanceOfType(ret, out var array); + Assert.HasCount(2, array); + Assert.AreEqual(publicKeys[0].ToArray().ToHexString(), array[0].GetSpan().ToHexString()); + Assert.AreEqual(publicKeys[1].ToArray().ToHexString(), array[1].GetSpan().ToHexString()); + + ret = NativeContract.RoleManagement.Call( + snapshot2, + "getDesignatedByRole", + new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger((int)role) }, + new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger(0) } + ); + Assert.IsInstanceOfType(ret, out array); + Assert.IsEmpty(array); + } + } + + private void ApplicationEngine_Notify(object sender, NotifyEventArgs e) + { + throw new NotImplementedException(); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_StdLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_StdLib.cs new file mode 100644 index 0000000000..fd7a1c5923 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_StdLib.cs @@ -0,0 +1,470 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_StdLib.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.VM; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; +using Array = System.Array; + +namespace Neo.UnitTests.SmartContract.Native; + +[TestClass] +public class UT_StdLib +{ + [TestMethod] + public void TestBinary() + { + var data = Array.Empty(); + + CollectionAssert.AreEqual(data, StdLib.Base64Decode(StdLib.Base64Encode(data))); + CollectionAssert.AreEqual(data, StdLib.Base58Decode(StdLib.Base58Encode(data))); + + data = new byte[] { 1, 2, 3 }; + + CollectionAssert.AreEqual(data, StdLib.Base64Decode(StdLib.Base64Encode(data))); + CollectionAssert.AreEqual(data, StdLib.Base64Decode("A \r Q \t I \n D")); + CollectionAssert.AreEqual(data, StdLib.Base58Decode(StdLib.Base58Encode(data))); + Assert.AreEqual("AQIDBA==", StdLib.Base64Encode(new byte[] { 1, 2, 3, 4 })); + Assert.AreEqual("2VfUX", StdLib.Base58Encode(new byte[] { 1, 2, 3, 4 })); + } + + [TestMethod] + public void TestItoaAtoi() + { + Assert.AreEqual("1", StdLib.Itoa(BigInteger.One, 10)); + Assert.AreEqual("1", StdLib.Itoa(BigInteger.One, 16)); + Assert.AreEqual("-1", StdLib.Itoa(BigInteger.MinusOne, 10)); + Assert.AreEqual("f", StdLib.Itoa(BigInteger.MinusOne, 16)); + Assert.AreEqual("3b9aca00", StdLib.Itoa(1_000_000_000, 16)); + Assert.AreEqual(-1, StdLib.Atoi("-1", 10)); + Assert.AreEqual(1, StdLib.Atoi("+1", 10)); + Assert.AreEqual(-1, StdLib.Atoi("ff", 16)); + Assert.AreEqual(-1, StdLib.Atoi("FF", 16)); + Assert.ThrowsExactly(() => _ = StdLib.Atoi("a", 10)); + Assert.ThrowsExactly(() => _ = StdLib.Atoi("g", 16)); + Assert.ThrowsExactly(() => _ = StdLib.Atoi("a", 11)); + + Assert.AreEqual(BigInteger.One, StdLib.Atoi(StdLib.Itoa(BigInteger.One, 10))); + Assert.AreEqual(BigInteger.MinusOne, StdLib.Atoi(StdLib.Itoa(BigInteger.MinusOne, 10))); + } + + [TestMethod] + public void MemoryCompare() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memoryCompare", "abc", "c"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memoryCompare", "abc", "d"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memoryCompare", "abc", "abc"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memoryCompare", "abc", "abcd"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(4, engine.ResultStack); + + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(0, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + } + + [TestMethod] + public void CheckDecodeEncode() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + using (ScriptBuilder script = new()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "base58CheckEncode", new byte[] { 1, 2, 3 }); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + + Assert.AreEqual("3DUz7ncyT", engine.ResultStack.Pop().GetString()); + } + + using (ScriptBuilder script = new()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "base58CheckDecode", "3DUz7ncyT"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + + CollectionAssert.AreEqual(new byte[] { 1, 2, 3 }, engine.ResultStack.Pop().GetSpan().ToArray()); + } + + // Error + + using (ScriptBuilder script = new()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "base58CheckDecode", "AA"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.FAULT, engine.Execute()); + } + + using (ScriptBuilder script = new()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "base58CheckDecode", [null]); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.FAULT, engine.Execute()); + } + } + + [TestMethod] + public void MemorySearch() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + using (var script = new ScriptBuilder()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 0); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 1); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 2); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 3); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "d", 0); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(5, engine.ResultStack); + + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(2, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(2, engine.ResultStack.Pop().GetInteger()); + } + + using (var script = new ScriptBuilder()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 0, false); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 1, false); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 2, false); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 3, false); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "d", 0, false); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(5, engine.ResultStack); + + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(2, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(2, engine.ResultStack.Pop().GetInteger()); + } + + using (var script = new ScriptBuilder()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 0, true); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 1, true); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 2, true); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "c", 3, true); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", "abc", "d", 0, true); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(5, engine.ResultStack); + + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(2, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(-1, engine.ResultStack.Pop().GetInteger()); + } + } + + [TestMethod] + public void StringSplit() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "stringSplit", "a,b", ","); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + + var arr = engine.ResultStack.Pop(); + Assert.HasCount(2, arr); + Assert.AreEqual("a", arr[0].GetString()); + Assert.AreEqual("b", arr[1].GetString()); + } + + [TestMethod] + public void StringElementLength() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", "🦆"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", "ã"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", "a"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(3, engine.ResultStack); + Assert.AreEqual(1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(1, engine.ResultStack.Pop().GetInteger()); + } + + [TestMethod] + public void TestInvalidUtf8Sequence() + { + // Simulating invalid UTF-8 byte (0xff) decoded as a UTF-16 char + const char badChar = (char)0xff; + var badStr = badChar.ToString(); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", badStr); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", badStr + "ab"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(2, engine.ResultStack); + Assert.AreEqual(3, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(1, engine.ResultStack.Pop().GetInteger()); + } + + [TestMethod] + public void Json_Deserialize() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + // Good + + using (var script = new ScriptBuilder()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonDeserialize", "123"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonDeserialize", "null"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(2, engine.ResultStack); + + engine.ResultStack.Pop(); + Assert.IsTrue(engine.ResultStack.Pop().GetInteger() == 123); + } + + // Error 1 - Wrong Json + + using (ScriptBuilder script = new()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonDeserialize", "***"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.FAULT, engine.Execute()); + Assert.IsEmpty(engine.ResultStack); + } + + // Error 2 - No decimals + + using (var script = new ScriptBuilder()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonDeserialize", "123.45"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.FAULT, engine.Execute()); + Assert.IsEmpty(engine.ResultStack); + } + } + + [TestMethod] + public void Json_Serialize() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + // Good + + using (var script = new ScriptBuilder()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonSerialize", 5); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonSerialize", true); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonSerialize", "test"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonSerialize", [null]); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonSerialize", new ContractParameter(ContractParameterType.Map) + { + Value = new List>() { + { new KeyValuePair( + new ContractParameter(ContractParameterType.String){ Value="key" }, + new ContractParameter(ContractParameterType.String){ Value= "value" }) + } + } + }); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(5, engine.ResultStack); + + Assert.AreEqual("{\"key\":\"value\"}", engine.ResultStack.Pop().GetString()); + Assert.AreEqual("null", engine.ResultStack.Pop().GetString()); + Assert.AreEqual("\"test\"", engine.ResultStack.Pop().GetString()); + Assert.AreEqual("true", engine.ResultStack.Pop().GetString()); + Assert.AreEqual("5", engine.ResultStack.Pop().GetString()); + } + + // Error + + using (var script = new ScriptBuilder()) + { + script.EmitDynamicCall(NativeContract.StdLib.Hash, "jsonSerialize"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.FAULT, engine.Execute()); + Assert.IsEmpty(engine.ResultStack); + } + } + + [TestMethod] + public void TestRuntime_Serialize() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + // Good + + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "serialize", 100); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "serialize", "test"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(2, engine.ResultStack); + + Assert.AreEqual("280474657374", engine.ResultStack.Pop().GetSpan().ToHexString()); + Assert.AreEqual("210164", engine.ResultStack.Pop().GetSpan().ToHexString()); + } + + [TestMethod] + public void TestRuntime_Deserialize() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + // Good + + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "deserialize", "280474657374".HexToBytes()); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "deserialize", "210164".HexToBytes()); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(2, engine.ResultStack); + + Assert.AreEqual(100, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual("test", engine.ResultStack.Pop().GetString()); + } + + [TestMethod] + public void TestBase64Url() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using var script = new ScriptBuilder(); + // Test encoding + script.EmitDynamicCall(NativeContract.StdLib.Hash, "base64UrlEncode", "Subject=test@example.com&Issuer=https://example.com"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "base64UrlDecode", "U3ViamVjdD10ZXN0QGV4YW1wbGUuY29tJklzc3Vlcj1odHRwczovL2V4YW1wbGUuY29t"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "base64UrlDecode", "U 3 \t V \n \riamVjdD10ZXN0QGV4YW1wbGUuY29tJklzc3Vlcj1odHRwczovL2V4YW1wbGUuY29t"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(3, engine.ResultStack); + Assert.AreEqual("Subject=test@example.com&Issuer=https://example.com", engine.ResultStack.Pop()); + Assert.AreEqual("Subject=test@example.com&Issuer=https://example.com", engine.ResultStack.Pop()); + Assert.AreEqual("U3ViamVjdD10ZXN0QGV4YW1wbGUuY29tJklzc3Vlcj1odHRwczovL2V4YW1wbGUuY29t", engine.ResultStack.Pop().GetString()); + } + + [TestMethod] + public void TestHexEncodeDecode() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var expectedBytes = new byte[] { 0x00, 0x01, 0x02, 0x03 }; + var expectedString = "00010203"; + + using var script = new ScriptBuilder(); + // Test encoding + script.EmitDynamicCall(NativeContract.StdLib.Hash, "hexEncode", expectedBytes); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "hexDecode", expectedString); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(2, engine.ResultStack); + Assert.AreEqual(expectedBytes, engine.ResultStack.Pop()); + Assert.AreEqual(expectedString, engine.ResultStack.Pop()); + } + + [TestMethod] + public void TestMemorySearch() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var expectedBytes = new byte[] { 0x00, 0x01, 0x02, 0x03 }; + var expectedValue = new byte[] { 0x03 }; + + using var sb = new ScriptBuilder() + .EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", expectedBytes, expectedValue, 0, false) + .EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", expectedBytes, expectedValue, expectedBytes.Length - 1, false) + .EmitDynamicCall(NativeContract.StdLib.Hash, "memorySearch", expectedBytes, expectedValue, expectedBytes.Length, true); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(sb.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(3, engine.ResultStack); + + Assert.AreEqual(3, engine.ResultStack.Pop()); + Assert.AreEqual(3, engine.ResultStack.Pop()); + Assert.AreEqual(3, engine.ResultStack.Pop()); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.Contract.cs b/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.Contract.cs new file mode 100644 index 0000000000..13d0a90d42 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.Contract.cs @@ -0,0 +1,71 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ApplicationEngine.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. + +using Neo.Extensions.VM; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.VM; + +namespace Neo.UnitTests.SmartContract; + +public partial class UT_ApplicationEngine +{ + private NeoSystem _system = null!; + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _system = TestBlockchain.GetSystem(); + _snapshotCache = _system.GetSnapshotCache(); + } + + [TestMethod] + public void TestCreateStandardAccount() + { + var snapshot = _snapshotCache.CloneCache(); + var settings = TestProtocolSettings.Default; + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default, gas: 1100_00000000); + + using var script = new ScriptBuilder(); + script.EmitSysCall(ApplicationEngine.System_Contract_CreateStandardAccount, settings.StandbyCommittee[0].EncodePoint(true)); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.AreEqual(Contract.CreateSignatureRedeemScript(settings.StandbyCommittee[0]).ToScriptHash(), new UInt160(result.GetSpan())); + } + + [TestMethod] + public void TestCreateStandardMultisigAccount() + { + var snapshot = _snapshotCache.CloneCache(); + var settings = TestProtocolSettings.Default; + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default, gas: 1100_00000000); + + using var script = new ScriptBuilder(); + script.EmitSysCall(ApplicationEngine.System_Contract_CreateMultisigAccount, new object[] + { + 2, + 3, + settings.StandbyCommittee[0].EncodePoint(true), + settings.StandbyCommittee[1].EncodePoint(true), + settings.StandbyCommittee[2].EncodePoint(true) + }); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var result = engine.ResultStack.Pop(); + Assert.AreEqual(Contract.CreateMultiSigRedeemScript(2, settings.StandbyCommittee.Take(3).ToArray()).ToScriptHash(), new UInt160(result.GetSpan())); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.Runtime.cs b/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.Runtime.cs new file mode 100644 index 0000000000..0ac2a50373 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.Runtime.cs @@ -0,0 +1,199 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ApplicationEngine.Runtime.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract.Manifest; +using Neo.VM.Types; +using System.Numerics; +using System.Text; +using Array = System.Array; +using Buffer = Neo.VM.Types.Buffer; + +namespace Neo.UnitTests.SmartContract; + +public partial class UT_ApplicationEngine +{ + [TestMethod] + public void TestGetNetworkAndAddressVersion() + { + var tx = TestUtils.GetTransaction(UInt160.Zero); + using var engine = ApplicationEngine.Create(TriggerType.Application, tx, null!, _system.GenesisBlock, settings: TestProtocolSettings.Default, gas: 1100_00000000); + + Assert.AreEqual(TestProtocolSettings.Default.Network, engine.GetNetwork()); + Assert.AreEqual(TestProtocolSettings.Default.AddressVersion, engine.GetAddressVersion()); + } + + [TestMethod] + public void TestNotSupportedNotification() + { + using var engine = ApplicationEngine.Create(TriggerType.Application, null, null!, _system.GenesisBlock, settings: TestProtocolSettings.Default, gas: 1100_00000000); + engine.LoadScript(Array.Empty()); + engine.CurrentContext!.GetState().Contract = new() + { + Hash = UInt160.Zero, + Nef = null!, + Manifest = new() + { + Name = "", + Groups = [], + SupportedStandards = [], + Abi = new() + { + Methods = [], + Events = new[] + { + new ContractEventDescriptor + { + Name = "e1", + Parameters = new[] + { + new ContractParameterDefinition + { + Name = "p1", + Type = ContractParameterType.Array + } + } + } + } + }, + Permissions = [], + Trusts = WildcardContainer.CreateWildcard() + } + }; + + // circular + + Neo.VM.Types.Array array = new(); + array.Add(array); + + Assert.ThrowsExactly(() => engine.RuntimeNotify(Encoding.ASCII.GetBytes("e1"), array)); + + // Buffer + + array.Clear(); + array.Add(new Buffer(1)); + engine.CurrentContext.GetState().Contract!.Manifest.Abi.Events[0].Parameters[0].Type = ContractParameterType.ByteArray; + + engine.RuntimeNotify(Encoding.ASCII.GetBytes("e1"), array); + Assert.AreEqual(StackItemType.ByteString, engine.Notifications[0].State[0].Type); + + // Pointer + + array.Clear(); + array.Add(new Pointer(Array.Empty(), 1)); + + Assert.ThrowsExactly(() => engine.RuntimeNotify(Encoding.ASCII.GetBytes("e1"), array)); + + // InteropInterface + + array.Clear(); + array.Add(new InteropInterface(new object())); + engine.CurrentContext.GetState().Contract!.Manifest.Abi.Events[0].Parameters[0].Type = ContractParameterType.InteropInterface; + + Assert.ThrowsExactly(() => engine.RuntimeNotify(Encoding.ASCII.GetBytes("e1"), array)); + } + + [TestMethod] + public void TestGetRandomSameBlock() + { + var tx = TestUtils.GetTransaction(UInt160.Zero); + // Even if persisting the same block, in different ApplicationEngine instance, the random number should be different + using var engine_1 = ApplicationEngine.Create(TriggerType.Application, tx, null!, _system.GenesisBlock, settings: TestProtocolSettings.Default, gas: 1100_00000000); + using var engine_2 = ApplicationEngine.Create(TriggerType.Application, tx, null!, _system.GenesisBlock, settings: TestProtocolSettings.Default, gas: 1100_00000000); + + engine_1.LoadScript(new byte[] { 0x01 }); + engine_2.LoadScript(new byte[] { 0x01 }); + + var rand_1 = engine_1.GetRandom(); + var rand_2 = engine_1.GetRandom(); + var rand_3 = engine_1.GetRandom(); + var rand_4 = engine_1.GetRandom(); + var rand_5 = engine_1.GetRandom(); + + var rand_6 = engine_2.GetRandom(); + var rand_7 = engine_2.GetRandom(); + var rand_8 = engine_2.GetRandom(); + var rand_9 = engine_2.GetRandom(); + var rand_10 = engine_2.GetRandom(); + + Assert.AreEqual(BigInteger.Parse("271339657438512451304577787170704246350"), rand_1); + Assert.AreEqual(BigInteger.Parse("98548189559099075644778613728143131367"), rand_2); + Assert.AreEqual(BigInteger.Parse("247654688993873392544380234598471205121"), rand_3); + Assert.AreEqual(BigInteger.Parse("291082758879475329976578097236212073607"), rand_4); + Assert.AreEqual(BigInteger.Parse("247152297361212656635216876565962360375"), rand_5); + + Assert.AreEqual(rand_6, rand_1); + Assert.AreEqual(rand_7, rand_2); + Assert.AreEqual(rand_8, rand_3); + Assert.AreEqual(rand_9, rand_4); + Assert.AreEqual(rand_10, rand_5); + } + + [TestMethod] + public void TestGetRandomDifferentBlock() + { + var tx_1 = TestUtils.GetTransaction(UInt160.Zero); + + var tx_2 = new Transaction + { + Version = 0, + Nonce = 2083236893, + ValidUntilBlock = 0, + Signers = Array.Empty(), + Attributes = Array.Empty(), + Script = Array.Empty(), + SystemFee = 0, + NetworkFee = 0, + Witnesses = Array.Empty() + }; + + using var engine_1 = ApplicationEngine.Create(TriggerType.Application, tx_1, null!, _system.GenesisBlock, settings: TestProtocolSettings.Default, gas: 1100_00000000); + // The next_nonce shuld be reinitialized when a new block is persisting + using var engine_2 = ApplicationEngine.Create(TriggerType.Application, tx_2, null!, _system.GenesisBlock, settings: TestProtocolSettings.Default, gas: 1100_00000000); + + var rand_1 = engine_1.GetRandom(); + var rand_2 = engine_1.GetRandom(); + var rand_3 = engine_1.GetRandom(); + var rand_4 = engine_1.GetRandom(); + var rand_5 = engine_1.GetRandom(); + + var rand_6 = engine_2.GetRandom(); + var rand_7 = engine_2.GetRandom(); + var rand_8 = engine_2.GetRandom(); + var rand_9 = engine_2.GetRandom(); + var rand_10 = engine_2.GetRandom(); + + Assert.AreEqual(BigInteger.Parse("271339657438512451304577787170704246350"), rand_1); + Assert.AreEqual(BigInteger.Parse("98548189559099075644778613728143131367"), rand_2); + Assert.AreEqual(BigInteger.Parse("247654688993873392544380234598471205121"), rand_3); + Assert.AreEqual(BigInteger.Parse("291082758879475329976578097236212073607"), rand_4); + Assert.AreEqual(BigInteger.Parse("247152297361212656635216876565962360375"), rand_5); + + Assert.AreNotEqual(rand_6, rand_1); + Assert.AreNotEqual(rand_7, rand_2); + Assert.AreNotEqual(rand_8, rand_3); + Assert.AreNotEqual(rand_9, rand_4); + Assert.AreNotEqual(rand_10, rand_5); + } + + [TestMethod] + public void TestInvalidUtf8LogMessage() + { + var tx_1 = TestUtils.GetTransaction(UInt160.Zero); + using var engine = ApplicationEngine.Create(TriggerType.Application, tx_1, null!, _system.GenesisBlock, settings: TestProtocolSettings.Default, gas: 1100_00000000); + var msg = new byte[] + { + 68, 216, 160, 6, 89, 102, 86, 72, 37, 15, 132, 45, 76, 221, 170, 21, 128, 51, 34, 168, 205, 56, 10, 228, 51, 114, 4, 218, 245, 155, 172, 132 + }; + Assert.ThrowsExactly(() => engine.RuntimeLog(msg)); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.cs b/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.cs new file mode 100644 index 0000000000..328343f4b6 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngine.cs @@ -0,0 +1,188 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ApplicationEngine.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions.VM; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.UnitTests.Extensions; +using Neo.VM; +using Array = Neo.VM.Types.Array; +using Boolean = Neo.VM.Types.Boolean; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public partial class UT_ApplicationEngine +{ + private string? eventName = null; + + [TestMethod] + public void TestNotify() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(System.Array.Empty()); + engine.Notify += Test_Notify1; + const string notifyEvent = "TestEvent"; + + engine.SendNotification(UInt160.Zero, notifyEvent, new Array()); + Assert.AreEqual(notifyEvent, eventName); + + engine.Notify += Test_Notify2; + engine.SendNotification(UInt160.Zero, notifyEvent, new Array()); + Assert.IsNull(eventName); + + eventName = notifyEvent; + engine.Notify -= Test_Notify1; + engine.SendNotification(UInt160.Zero, notifyEvent, new Array()); + Assert.IsNull(eventName); + + engine.Notify -= Test_Notify2; + engine.SendNotification(UInt160.Zero, notifyEvent, new Array()); + Assert.IsNull(eventName); + } + + private void Test_Notify1(object sender, NotifyEventArgs e) + { + eventName = e.EventName; + } + + private void Test_Notify2(object sender, NotifyEventArgs e) + { + eventName = null; + } + + [TestMethod] + public void TestCreateDummyBlock() + { + var system = TestBlockchain.GetSystem(); + var snapshotCache = system.GetTestSnapshotCache(); + byte[] SyscallSystemRuntimeCheckWitnessHash = [0x68, 0xf8, 0x27, 0xec, 0x8c]; + ApplicationEngine engine = ApplicationEngine.Run(SyscallSystemRuntimeCheckWitnessHash, snapshotCache, settings: TestProtocolSettings.Default); + Assert.AreEqual(0u, engine.PersistingBlock!.Version); + Assert.AreEqual(system.GenesisBlock.Hash, engine.PersistingBlock.PrevHash); + Assert.AreEqual(new UInt256(), engine.PersistingBlock.MerkleRoot); + } + + [TestMethod] + public void TestSystem_Contract_Call_Permissions() + { + UInt160 scriptHash; + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + // Setup: put a simple contract to the storage. + using (var script = new ScriptBuilder()) + { + // Push True on stack and return. + script.EmitPush(true); + script.Emit(OpCode.RET); + + // Mock contract and put it to the Managemant's storage. + scriptHash = script.ToArray().ToScriptHash(); + + snapshotCache.DeleteContract(scriptHash); + var contract = TestUtils.GetContract(script.ToArray(), TestUtils.CreateManifest("test", ContractParameterType.Any)); + contract.Manifest.Abi.Methods = [ + new ContractMethodDescriptor { Name = "disallowed", Parameters = [] }, + new ContractMethodDescriptor { Name = "test", Parameters = [] } + ]; + snapshotCache.AddContract(scriptHash, contract); + } + + // Disallowed method call. + using (var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, null, ProtocolSettings.Default)) + using (var script = new ScriptBuilder()) + { + // Build call script calling disallowed method. + script.EmitDynamicCall(scriptHash, "disallowed"); + + // Mock executing state to be a contract-based. + engine.LoadScript(script.ToArray()); + engine.CurrentContext!.GetState().Contract = new() + { + Hash = UInt160.Zero, + Nef = null!, + Manifest = new() + { + Name = "", + Groups = [], + SupportedStandards = [], + Abi = new() + { + Methods = [], + Events = [] + }, + Permissions = [ + new ContractPermission + { + Contract = ContractPermissionDescriptor.Create(scriptHash), + Methods = WildcardContainer.Create(["test"]) // allowed to call only "test" method of the target contract. + } + ], + Trusts = WildcardContainer.CreateWildcard() + } + }; + var currentScriptHash = engine.EntryScriptHash; + + Assert.AreEqual("", engine.GetEngineStackInfoOnFault()); + Assert.AreEqual(VMState.FAULT, engine.Execute()); + Assert.Contains($"Cannot Call Method disallowed Of Contract {scriptHash}", engine.FaultException!.ToString()); + string traceback = engine.GetEngineStackInfoOnFault(); + Assert.Contains($"Cannot Call Method disallowed Of Contract {scriptHash}", traceback); + Assert.Contains("CurrentScriptHash", traceback); + Assert.Contains("EntryScriptHash", traceback); + Assert.Contains("InstructionPointer", traceback); + Assert.Contains("OpCode SYSCALL, Script Length=", traceback); + } + + // Allowed method call. + using (var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, null, ProtocolSettings.Default)) + using (var script = new ScriptBuilder()) + { + // Build call script. + script.EmitDynamicCall(scriptHash, "test"); + + // Mock executing state to be a contract-based. + engine.LoadScript(script.ToArray()); + engine.CurrentContext!.GetState().Contract = new() + { + Hash = UInt160.Zero, + Nef = null!, + Manifest = new() + { + Name = "", + Groups = [], + SupportedStandards = [], + Abi = new() + { + Methods = [], + Events = [] + }, + Permissions = [ + new ContractPermission + { + Contract = ContractPermissionDescriptor.Create(scriptHash), + Methods = WildcardContainer.Create(["test"]) // allowed to call only "test" method of the target contract. + } + ], + Trusts = WildcardContainer.CreateWildcard() + } + }; + var currentScriptHash = engine.EntryScriptHash; + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsInstanceOfType(engine.ResultStack.Peek()); + var res = (Boolean)engine.ResultStack.Pop(); + Assert.IsTrue(res.GetBoolean()); + } + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngineProvider.cs b/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngineProvider.cs new file mode 100644 index 0000000000..cd32e56ed4 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_ApplicationEngineProvider.cs @@ -0,0 +1,100 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ApplicationEngineProvider.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.VM; +using System.Reflection; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_ApplicationEngineProvider +{ + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + ApplicationEngine.Provider = null; + } + + [TestCleanup] + public void TestCleanup() + { + ApplicationEngine.Provider = null; + } + + [TestMethod] + public void TestSetAppEngineProvider() + { + ApplicationEngine.Provider = new TestProvider(); + var snapshot = _snapshotCache.CloneCache(); + + using var appEngine = ApplicationEngine.Create(TriggerType.Application, + null, snapshot, gas: 0, settings: TestProtocolSettings.Default); + Assert.IsTrue(appEngine is TestEngine); + } + + [TestMethod] + public void TestDefaultAppEngineProvider() + { + var snapshot = _snapshotCache.CloneCache(); + using var appEngine = ApplicationEngine.Create(TriggerType.Application, + null, snapshot, gas: 0, settings: TestProtocolSettings.Default); + Assert.IsNotNull(appEngine); + } + + [TestMethod] + public void TestInitNonce() + { + var block = new Block + { + Header = new() + { + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Nonce = 0x0102030405060708, + NextConsensus = UInt160.Zero, + Witness = null! + }, + Transactions = [] + }; + using var app = new TestEngine(TriggerType.Application, + null, null!, block, TestProtocolSettings.Default, 0, null, null); + + var nonceData = typeof(ApplicationEngine) + .GetField("nonceData", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(app) as byte[]; + Assert.IsNotNull(nonceData); + Assert.AreEqual("08070605040302010000000000000000", nonceData.ToHexString()); + } + + class TestProvider : IApplicationEngineProvider + { + public ApplicationEngine Create(TriggerType trigger, IVerifiable? container, DataCache snapshot, + Block? persistingBlock, ProtocolSettings settings, long gas, IDiagnostic? diagnostic, JumpTable? jumpTable) + { + return new TestEngine(trigger, container, snapshot, persistingBlock, settings, gas, diagnostic, jumpTable); + } + } + + class TestEngine : ApplicationEngine + { + public TestEngine(TriggerType trigger, IVerifiable? container, DataCache snapshotCache, + Block? persistingBlock, ProtocolSettings settings, long gas, IDiagnostic? diagnostic, JumpTable? jumpTable) + : base(trigger, container, snapshotCache, persistingBlock, settings, gas, diagnostic, jumpTable) + { + } + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_BinarySerializer.cs b/tests/Neo.UnitTests/SmartContract/UT_BinarySerializer.cs new file mode 100644 index 0000000000..31d89e2935 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_BinarySerializer.cs @@ -0,0 +1,122 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_BinarySerializer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; +using Neo.VM.Types; +using System.Text; +using Array = Neo.VM.Types.Array; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_BinarySerializer +{ + [TestMethod] + public void TestSerialize() + { + byte[] result1 = BinarySerializer.Serialize(new byte[5], ExecutionEngineLimits.Default); + byte[] expectedArray1 = new byte[] { + 0x28, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + Assert.AreEqual(Encoding.Default.GetString(expectedArray1), Encoding.Default.GetString(result1)); + + byte[] result2 = BinarySerializer.Serialize(true, ExecutionEngineLimits.Default); + byte[] expectedArray2 = new byte[] { + 0x20, 0x01 + }; + Assert.AreEqual(Encoding.Default.GetString(expectedArray2), Encoding.Default.GetString(result2)); + + byte[] result3 = BinarySerializer.Serialize(1, ExecutionEngineLimits.Default); + byte[] expectedArray3 = new byte[] { + 0x21, 0x01, 0x01 + }; + Assert.AreEqual(Encoding.Default.GetString(expectedArray3), Encoding.Default.GetString(result3)); + + StackItem stackItem4 = new InteropInterface(new object()); + Assert.ThrowsExactly(() => BinarySerializer.Serialize(stackItem4, ExecutionEngineLimits.Default)); + + List list6 = [1]; + StackItem stackItem62 = new Array(list6); + byte[] result6 = BinarySerializer.Serialize(stackItem62, ExecutionEngineLimits.Default); + byte[] expectedArray6 = new byte[] { + 0x40,0x01,0x21,0x01,0x01 + }; + Assert.AreEqual(Encoding.Default.GetString(expectedArray6), Encoding.Default.GetString(result6)); + + List list7 = [1]; + StackItem stackItem72 = new Struct(list7); + byte[] result7 = BinarySerializer.Serialize(stackItem72, ExecutionEngineLimits.Default); + byte[] expectedArray7 = new byte[] { + 0x41,0x01,0x21,0x01,0x01 + }; + Assert.AreEqual(Encoding.Default.GetString(expectedArray7), Encoding.Default.GetString(result7)); + + StackItem stackItem82 = new Map { [2] = 1 }; + byte[] result8 = BinarySerializer.Serialize(stackItem82, ExecutionEngineLimits.Default); + byte[] expectedArray8 = new byte[] { + 0x48,0x01,0x21,0x01,0x02,0x21,0x01,0x01 + }; + Assert.AreEqual(Encoding.Default.GetString(expectedArray8), Encoding.Default.GetString(result8)); + + Map stackItem91 = new(); + stackItem91[1] = stackItem91; + Assert.ThrowsExactly(() => BinarySerializer.Serialize(stackItem91, ExecutionEngineLimits.Default)); + + Array stackItem10 = new(); + stackItem10.Add(stackItem10); + Assert.ThrowsExactly(() => BinarySerializer.Serialize(stackItem10, ExecutionEngineLimits.Default)); + } + + [TestMethod] + public void TestDeserializeStackItem() + { + StackItem stackItem1 = new ByteString(new byte[5]); + byte[] byteArray1 = BinarySerializer.Serialize(stackItem1, ExecutionEngineLimits.Default); + StackItem result1 = BinarySerializer.Deserialize(byteArray1, ExecutionEngineLimits.Default); + Assert.AreEqual(stackItem1, result1); + + StackItem stackItem2 = StackItem.True; + byte[] byteArray2 = BinarySerializer.Serialize(stackItem2, ExecutionEngineLimits.Default); + StackItem result2 = BinarySerializer.Deserialize(byteArray2, ExecutionEngineLimits.Default); + Assert.AreEqual(stackItem2, result2); + + StackItem stackItem3 = new Integer(1); + byte[] byteArray3 = BinarySerializer.Serialize(stackItem3, ExecutionEngineLimits.Default); + StackItem result3 = BinarySerializer.Deserialize(byteArray3, ExecutionEngineLimits.Default); + Assert.AreEqual(stackItem3, result3); + + byte[] byteArray4 = BinarySerializer.Serialize(1, ExecutionEngineLimits.Default); + byteArray4[0] = 0x40; + Assert.ThrowsExactly(() => BinarySerializer.Deserialize(byteArray4, ExecutionEngineLimits.Default)); + + List list5 = [1]; + StackItem stackItem52 = new Array(list5); + byte[] byteArray5 = BinarySerializer.Serialize(stackItem52, ExecutionEngineLimits.Default); + StackItem result5 = BinarySerializer.Deserialize(byteArray5, ExecutionEngineLimits.Default); + Assert.AreEqual(((Array)stackItem52).Count, ((Array)result5).Count); + Assert.AreEqual(((Array)stackItem52).GetEnumerator().Current, ((Array)result5).GetEnumerator().Current); + + List list6 = [1]; + StackItem stackItem62 = new Struct(list6); + byte[] byteArray6 = BinarySerializer.Serialize(stackItem62, ExecutionEngineLimits.Default); + StackItem result6 = BinarySerializer.Deserialize(byteArray6, ExecutionEngineLimits.Default); + Assert.AreEqual(((Struct)stackItem62).Count, ((Struct)result6).Count); + Assert.AreEqual(((Struct)stackItem62).GetEnumerator().Current, ((Struct)result6).GetEnumerator().Current); + + StackItem stackItem72 = new Map { [2] = 1 }; + byte[] byteArray7 = BinarySerializer.Serialize(stackItem72, ExecutionEngineLimits.Default); + StackItem result7 = BinarySerializer.Deserialize(byteArray7, ExecutionEngineLimits.Default); + Assert.AreEqual(((Map)stackItem72).Count, ((Map)result7).Count); + CollectionAssert.AreEqual(((Map)stackItem72).Keys.ToArray(), ((Map)result7).Keys.ToArray()); + CollectionAssert.AreEqual(((Map)stackItem72).Values.ToArray(), ((Map)result7).Values.ToArray()); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_Contract.cs b/tests/Neo.UnitTests/SmartContract/UT_Contract.cs new file mode 100644 index 0000000000..69fa6e2b95 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_Contract.cs @@ -0,0 +1,210 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_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. + +using Neo.Extensions.VM; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using System.Security.Cryptography; +using ECPoint = Neo.Cryptography.ECC.ECPoint; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_Contract +{ + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + } + + [TestMethod] + public void TestGetScriptHash() + { + byte[] privateKey = new byte[32]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetBytes(privateKey); + var key = new KeyPair(privateKey); + Contract contract = Contract.CreateSignatureContract(key.PublicKey); + byte[] expectedArray = new byte[40]; + expectedArray[0] = (byte)OpCode.PUSHDATA1; + expectedArray[1] = 0x21; + Array.Copy(key.PublicKey.EncodePoint(true), 0, expectedArray, 2, 33); + expectedArray[35] = (byte)OpCode.SYSCALL; + Array.Copy(BitConverter.GetBytes(ApplicationEngine.System_Crypto_CheckSig), 0, expectedArray, 36, 4); + Assert.AreEqual(expectedArray.ToScriptHash(), contract.ScriptHash); + } + + [TestMethod] + public void TestCreate() + { + byte[] script = new byte[32]; + ContractParameterType[] parameterList = new ContractParameterType[] { ContractParameterType.Signature }; + Contract contract = Contract.Create(parameterList, script); + Assert.AreEqual(contract.Script, script); + Assert.HasCount(1, contract.ParameterList); + Assert.AreEqual(ContractParameterType.Signature, contract.ParameterList[0]); + } + + [TestMethod] + public void TestCreateMultiSigContract() + { + byte[] privateKey1 = new byte[32]; + RandomNumberGenerator rng1 = RandomNumberGenerator.Create(); + rng1.GetBytes(privateKey1); + var key1 = new KeyPair(privateKey1); + byte[] privateKey2 = new byte[32]; + RandomNumberGenerator rng2 = RandomNumberGenerator.Create(); + rng2.GetBytes(privateKey2); + var key2 = new KeyPair(privateKey2); + ECPoint[] publicKeys = new ECPoint[2]; + publicKeys[0] = key1.PublicKey; + publicKeys[1] = key2.PublicKey; + publicKeys = publicKeys.OrderBy(p => p).ToArray(); + Contract contract = Contract.CreateMultiSigContract(2, publicKeys); + byte[] expectedArray = new byte[77]; + expectedArray[0] = (byte)OpCode.PUSH2; + expectedArray[1] = (byte)OpCode.PUSHDATA1; + expectedArray[2] = 0x21; + Array.Copy(publicKeys[0].EncodePoint(true), 0, expectedArray, 3, 33); + expectedArray[36] = (byte)OpCode.PUSHDATA1; + expectedArray[37] = 0x21; + Array.Copy(publicKeys[1].EncodePoint(true), 0, expectedArray, 38, 33); + expectedArray[71] = (byte)OpCode.PUSH2; + expectedArray[72] = (byte)OpCode.SYSCALL; + Array.Copy(BitConverter.GetBytes(ApplicationEngine.System_Crypto_CheckMultisig), 0, expectedArray, 73, 4); + CollectionAssert.AreEqual(expectedArray, contract.Script); + Assert.HasCount(2, contract.ParameterList); + Assert.AreEqual(ContractParameterType.Signature, contract.ParameterList[0]); + Assert.AreEqual(ContractParameterType.Signature, contract.ParameterList[1]); + } + + [TestMethod] + public void TestCreateMultiSigRedeemScript() + { + byte[] privateKey1 = new byte[32]; + RandomNumberGenerator rng1 = RandomNumberGenerator.Create(); + rng1.GetBytes(privateKey1); + var key1 = new KeyPair(privateKey1); + byte[] privateKey2 = new byte[32]; + RandomNumberGenerator rng2 = RandomNumberGenerator.Create(); + rng2.GetBytes(privateKey2); + var key2 = new KeyPair(privateKey2); + ECPoint[] publicKeys = new ECPoint[2]; + publicKeys[0] = key1.PublicKey; + publicKeys[1] = key2.PublicKey; + publicKeys = publicKeys.OrderBy(p => p).ToArray(); + Assert.ThrowsExactly(() => Contract.CreateMultiSigRedeemScript(0, publicKeys)); + byte[] script = Contract.CreateMultiSigRedeemScript(2, publicKeys); + byte[] expectedArray = new byte[77]; + expectedArray[0] = (byte)OpCode.PUSH2; + expectedArray[1] = (byte)OpCode.PUSHDATA1; + expectedArray[2] = 0x21; + Array.Copy(publicKeys[0].EncodePoint(true), 0, expectedArray, 3, 33); + expectedArray[36] = (byte)OpCode.PUSHDATA1; + expectedArray[37] = 0x21; + Array.Copy(publicKeys[1].EncodePoint(true), 0, expectedArray, 38, 33); + expectedArray[71] = (byte)OpCode.PUSH2; + expectedArray[72] = (byte)OpCode.SYSCALL; + Array.Copy(BitConverter.GetBytes(ApplicationEngine.System_Crypto_CheckMultisig), 0, expectedArray, 73, 4); + CollectionAssert.AreEqual(expectedArray, script); + } + + [TestMethod] + public void TestCreateSignatureContract() + { + byte[] privateKey = new byte[32]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetBytes(privateKey); + var key = new KeyPair(privateKey); + Contract contract = Contract.CreateSignatureContract(key.PublicKey); + byte[] expectedArray = new byte[40]; + expectedArray[0] = (byte)OpCode.PUSHDATA1; + expectedArray[1] = 0x21; + Array.Copy(key.PublicKey.EncodePoint(true), 0, expectedArray, 2, 33); + expectedArray[35] = (byte)OpCode.SYSCALL; + Array.Copy(BitConverter.GetBytes(ApplicationEngine.System_Crypto_CheckSig), 0, expectedArray, 36, 4); + CollectionAssert.AreEqual(expectedArray, contract.Script); + Assert.HasCount(1, contract.ParameterList); + Assert.AreEqual(ContractParameterType.Signature, contract.ParameterList[0]); + } + + [TestMethod] + public void TestCreateSignatureRedeemScript() + { + byte[] privateKey = new byte[32]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetBytes(privateKey); + var key = new KeyPair(privateKey); + byte[] script = Contract.CreateSignatureRedeemScript(key.PublicKey); + byte[] expectedArray = new byte[40]; + expectedArray[0] = (byte)OpCode.PUSHDATA1; + expectedArray[1] = 0x21; + Array.Copy(key.PublicKey.EncodePoint(true), 0, expectedArray, 2, 33); + expectedArray[35] = (byte)OpCode.SYSCALL; + Array.Copy(BitConverter.GetBytes(ApplicationEngine.System_Crypto_CheckSig), 0, expectedArray, 36, 4); + CollectionAssert.AreEqual(expectedArray, script); + } + + [TestMethod] + public void TestSignatureRedeemScriptFee() + { + var snapshot = _snapshotCache.CloneCache(); + byte[] privateKey = new byte[32]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetBytes(privateKey); + var key = new KeyPair(privateKey); + byte[] verification = Contract.CreateSignatureRedeemScript(key.PublicKey); + byte[] invocation = new ScriptBuilder().EmitPush(UInt160.Zero).ToArray(); + + var fee = PolicyContract.DefaultExecFeeFactor * (ApplicationEngine.OpCodePriceTable[(byte)OpCode.PUSHDATA1] * 2 + ApplicationEngine.OpCodePriceTable[(byte)OpCode.SYSCALL] + ApplicationEngine.CheckSigPrice); + + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, + new Transaction { Signers = Array.Empty(), Attributes = Array.Empty(), Witnesses = [] }, snapshot, settings: TestProtocolSettings.Default); + engine.LoadScript(invocation.Concat(verification).ToArray(), configureState: p => p.CallFlags = CallFlags.None); + engine.Execute(); + Assert.AreEqual(fee, engine.FeeConsumed); + } + + [TestMethod] + public void TestCreateMultiSigRedeemScriptFee() + { + var snapshot = _snapshotCache.CloneCache(); + byte[] privateKey1 = new byte[32]; + RandomNumberGenerator rng1 = RandomNumberGenerator.Create(); + rng1.GetBytes(privateKey1); + var key1 = new KeyPair(privateKey1); + byte[] privateKey2 = new byte[32]; + RandomNumberGenerator rng2 = RandomNumberGenerator.Create(); + rng2.GetBytes(privateKey2); + var key2 = new KeyPair(privateKey2); + ECPoint[] publicKeys = new ECPoint[2]; + publicKeys[0] = key1.PublicKey; + publicKeys[1] = key2.PublicKey; + publicKeys = publicKeys.OrderBy(p => p).ToArray(); + byte[] verification = Contract.CreateMultiSigRedeemScript(2, publicKeys); + byte[] invocation = new ScriptBuilder().EmitPush(UInt160.Zero).EmitPush(UInt160.Zero).ToArray(); + + long fee = PolicyContract.DefaultExecFeeFactor * (ApplicationEngine.OpCodePriceTable[(byte)OpCode.PUSHDATA1] * (2 + 2) + ApplicationEngine.OpCodePriceTable[(byte)OpCode.PUSHINT8] * 2 + ApplicationEngine.OpCodePriceTable[(byte)OpCode.SYSCALL] + ApplicationEngine.CheckSigPrice * 2); + + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, + new Transaction { Signers = Array.Empty(), Attributes = Array.Empty(), Witnesses = [] }, snapshot, settings: TestProtocolSettings.Default); + engine.LoadScript(invocation.Concat(verification).ToArray(), configureState: p => p.CallFlags = CallFlags.None); + engine.Execute(); + Assert.AreEqual(fee, engine.FeeConsumed); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_ContractParameter.cs b/tests/Neo.UnitTests/SmartContract/UT_ContractParameter.cs new file mode 100644 index 0000000000..ebd505de1b --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_ContractParameter.cs @@ -0,0 +1,232 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractParameter.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using Neo.Json; +using Neo.SmartContract; +using System.Numerics; +using System.Text; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_ContractParameter +{ + [TestMethod] + public void TestGenerator1() + { + ContractParameter contractParameter = new(); + Assert.IsNotNull(contractParameter); + } + + [TestMethod] + public void TestGenerator2() + { + ContractParameter contractParameter1 = new(ContractParameterType.Signature); + byte[] expectedArray1 = new byte[64]; + Assert.IsNotNull(contractParameter1); + Assert.AreEqual(Encoding.Default.GetString(expectedArray1), Encoding.Default.GetString((byte[])contractParameter1.Value!)); + + ContractParameter contractParameter2 = new(ContractParameterType.Boolean); + Assert.IsNotNull(contractParameter2); + Assert.IsFalse((bool?)contractParameter2.Value); + + ContractParameter contractParameter3 = new(ContractParameterType.Integer); + Assert.IsNotNull(contractParameter3); + Assert.AreEqual(0, contractParameter3.Value); + + ContractParameter contractParameter4 = new(ContractParameterType.Hash160); + Assert.IsNotNull(contractParameter4); + Assert.AreEqual(new UInt160(), contractParameter4.Value); + + ContractParameter contractParameter5 = new(ContractParameterType.Hash256); + Assert.IsNotNull(contractParameter5); + Assert.AreEqual(new UInt256(), contractParameter5.Value); + + ContractParameter contractParameter6 = new(ContractParameterType.ByteArray); + byte[] expectedArray6 = Array.Empty(); + Assert.IsNotNull(contractParameter6); + Assert.AreEqual(Encoding.Default.GetString(expectedArray6), Encoding.Default.GetString((byte[])contractParameter6.Value!)); + + ContractParameter contractParameter7 = new(ContractParameterType.PublicKey); + Assert.IsNotNull(contractParameter7); + Assert.AreEqual(ECCurve.Secp256r1.G, contractParameter7.Value); + + ContractParameter contractParameter8 = new(ContractParameterType.String); + Assert.IsNotNull(contractParameter8); + Assert.AreEqual("", contractParameter8.Value); + + ContractParameter contractParameter9 = new(ContractParameterType.Array); + Assert.IsNotNull(contractParameter9); + Assert.IsEmpty((List)contractParameter9.Value!); + + ContractParameter contractParameter10 = new(ContractParameterType.Map); + Assert.IsNotNull(contractParameter10); + Assert.IsEmpty((List>)contractParameter10.Value!); + + Assert.ThrowsExactly(() => _ = new ContractParameter(ContractParameterType.Void)); + } + + [TestMethod] + public void TestFromAndToJson() + { + ContractParameter contractParameter1 = new(ContractParameterType.Signature); + JObject jobject1 = contractParameter1.ToJson(); + Assert.AreEqual(jobject1.ToString(), ContractParameter.FromJson(jobject1).ToJson().ToString()); + + ContractParameter contractParameter2 = new(ContractParameterType.Boolean); + JObject jobject2 = contractParameter2.ToJson(); + Assert.AreEqual(jobject2.ToString(), ContractParameter.FromJson(jobject2).ToJson().ToString()); + + ContractParameter contractParameter3 = new(ContractParameterType.Integer); + JObject jobject3 = contractParameter3.ToJson(); + Assert.AreEqual(jobject3.ToString(), ContractParameter.FromJson(jobject3).ToJson().ToString()); + + ContractParameter contractParameter4 = new(ContractParameterType.Hash160); + JObject jobject4 = contractParameter4.ToJson(); + Assert.AreEqual(jobject4.ToString(), ContractParameter.FromJson(jobject4).ToJson().ToString()); + + ContractParameter contractParameter5 = new(ContractParameterType.Hash256); + JObject jobject5 = contractParameter5.ToJson(); + Assert.AreEqual(jobject5.ToString(), ContractParameter.FromJson(jobject5).ToJson().ToString()); + + ContractParameter contractParameter6 = new(ContractParameterType.ByteArray); + JObject jobject6 = contractParameter6.ToJson(); + Assert.AreEqual(jobject6.ToString(), ContractParameter.FromJson(jobject6).ToJson().ToString()); + + ContractParameter contractParameter7 = new(ContractParameterType.PublicKey); + JObject jobject7 = contractParameter7.ToJson(); + Assert.AreEqual(jobject7.ToString(), ContractParameter.FromJson(jobject7).ToJson().ToString()); + + ContractParameter contractParameter8 = new(ContractParameterType.String); + JObject jobject8 = contractParameter8.ToJson(); + Assert.AreEqual(jobject8.ToString(), ContractParameter.FromJson(jobject8).ToJson().ToString()); + + ContractParameter contractParameter9 = new(ContractParameterType.Array); + JObject jobject9 = contractParameter9.ToJson(); + Assert.AreEqual(jobject9.ToString(), ContractParameter.FromJson(jobject9).ToJson().ToString()); + + ContractParameter contractParameter10 = new(ContractParameterType.Map); + JObject jobject10 = contractParameter10.ToJson(); + Assert.AreEqual(jobject10.ToString(), ContractParameter.FromJson(jobject10).ToJson().ToString()); + + ContractParameter contractParameter11 = new(ContractParameterType.String); + JObject jobject11 = contractParameter11.ToJson(); + jobject11["type"] = "Void"; + Assert.ThrowsExactly(() => _ = ContractParameter.FromJson(jobject11)); + } + + [TestMethod] + public void TestContractParameterCyclicReference() + { + var map = new ContractParameter + { + Type = ContractParameterType.Map, + Value = new List> + { + new( + new ContractParameter { Type = ContractParameterType.Integer, Value = 1 }, + new ContractParameter { Type = ContractParameterType.Integer, Value = 2 } + ) + } + }; + + var value = new List { map, map }; + var item = new ContractParameter { Type = ContractParameterType.Array, Value = value }; + + // just check there is no exception + var json = item.ToJson(); + Assert.AreEqual(json.ToString(), ContractParameter.FromJson(json).ToJson().ToString()); + + // check cyclic reference + value.Add(item); + Assert.ThrowsExactly(() => _ = item.ToJson()); + } + + [TestMethod] + public void TestSetValue() + { + ContractParameter contractParameter1 = new(ContractParameterType.Signature); + byte[] expectedArray1 = new byte[64]; + contractParameter1.SetValue(new byte[64].ToHexString()); + Assert.AreEqual(Encoding.Default.GetString(expectedArray1), Encoding.Default.GetString((byte[])contractParameter1.Value!)); + Assert.ThrowsExactly(() => contractParameter1.SetValue(new byte[50].ToHexString())); + + ContractParameter contractParameter2 = new(ContractParameterType.Boolean); + contractParameter2.SetValue("true"); + Assert.IsTrue((bool?)contractParameter2.Value); + + ContractParameter contractParameter3 = new(ContractParameterType.Integer); + contractParameter3.SetValue("11"); + Assert.AreEqual(new BigInteger(11), contractParameter3.Value); + + ContractParameter contractParameter4 = new(ContractParameterType.Hash160); + contractParameter4.SetValue("0x0000000000000000000000000000000000000001"); + Assert.AreEqual(UInt160.Parse("0x0000000000000000000000000000000000000001"), contractParameter4.Value); + + ContractParameter contractParameter5 = new(ContractParameterType.Hash256); + contractParameter5.SetValue("0x0000000000000000000000000000000000000000000000000000000000000000"); + Assert.AreEqual(UInt256.Parse("0x0000000000000000000000000000000000000000000000000000000000000000"), contractParameter5.Value); + + ContractParameter contractParameter6 = new(ContractParameterType.ByteArray); + contractParameter6.SetValue("2222"); + byte[] expectedArray6 = new byte[2]; + expectedArray6[0] = 0x22; + expectedArray6[1] = 0x22; + Assert.AreEqual(Encoding.Default.GetString(expectedArray6), Encoding.Default.GetString((byte[])contractParameter6.Value!)); + + ContractParameter contractParameter7 = new(ContractParameterType.PublicKey); + byte[] privateKey7 = new byte[32]; + for (int j = 0; j < privateKey7.Length; j++) + privateKey7[j] = RandomNumberFactory.NextByte(); + ECPoint publicKey7 = ECCurve.Secp256r1.G * privateKey7; + contractParameter7.SetValue(publicKey7.ToString()); + Assert.IsTrue(publicKey7.Equals(contractParameter7.Value)); + + ContractParameter contractParameter8 = new(ContractParameterType.String); + contractParameter8.SetValue("AAA"); + Assert.AreEqual("AAA", contractParameter8.Value); + + ContractParameter contractParameter9 = new(ContractParameterType.Array); + Assert.ThrowsExactly(() => contractParameter9.SetValue("AAA")); + } + + [TestMethod] + public void TestToString() + { + ContractParameter contractParameter1 = new(); + Assert.AreEqual("(null)", contractParameter1.ToString()); + + ContractParameter contractParameter2 = new(ContractParameterType.ByteArray) + { + Value = new byte[1] + }; + Assert.AreEqual("00", contractParameter2.ToString()); + + ContractParameter contractParameter3 = new(ContractParameterType.Array); + Assert.AreEqual("[]", contractParameter3.ToString()); + ContractParameter internalContractParameter3 = new(ContractParameterType.Boolean); + ((IList)contractParameter3.Value!).Add(internalContractParameter3); + Assert.AreEqual("[False]", contractParameter3.ToString()); + + ContractParameter contractParameter4 = new(ContractParameterType.Map); + Assert.AreEqual("[]", contractParameter4.ToString()); + ContractParameter internalContractParameter4 = new(ContractParameterType.Boolean); + ((IList>)contractParameter4.Value!).Add(new KeyValuePair( + internalContractParameter4, internalContractParameter4 + )); + Assert.AreEqual("[{False,False}]", contractParameter4.ToString()); + + ContractParameter contractParameter5 = new(ContractParameterType.String); + Assert.AreEqual("", contractParameter5.ToString()); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_ContractParameterContext.cs b/tests/Neo.UnitTests/SmartContract/UT_ContractParameterContext.cs new file mode 100644 index 0000000000..882f3f4577 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_ContractParameterContext.cs @@ -0,0 +1,238 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractParameterContext.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract.Manifest; +using Neo.UnitTests.Extensions; +using Neo.VM; +using Neo.Wallets; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_ContractParameterContext +{ + private static Contract contract = null!; + private static KeyPair key = null!; + + [ClassInitialize] + public static void ClassSetUp(TestContext ctx) + { + if (contract == null) + { + byte[] privateKey = Enumerable.Repeat((byte)0x01, 32).ToArray(); + key = new KeyPair(privateKey); + contract = Contract.CreateSignatureContract(key.PublicKey); + } + } + + [TestMethod] + public void TestGetComplete() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var tx = TestUtils.GetTransaction(UInt160.Parse("0x1bd5c777ec35768892bd3daab60fb7a1cb905066")); + var context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsFalse(context.Completed); + } + + [TestMethod] + public void TestToString() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var tx = TestUtils.GetTransaction(UInt160.Parse("0x1bd5c777ec35768892bd3daab60fb7a1cb905066")); + var context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + context.Add(contract, 0, new byte[] { 0x01 }); + var expected = """ + { + "type":"Neo.Network.P2P.Payloads.Transaction", + "hash":"0x602c1fa1c08b041e4e6b87aa9a9f9c643166cd34bdd5215a3dd85778c59cce88", + "data":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFmUJDLobcPtqo9vZKIdjXsd8fVGwEAARI=", + "items":{}, + "network": + """ + TestProtocolSettings.Default.Network.ToString() + "}"; + expected = Regex.Replace(expected, @"\s+", ""); + Assert.AreEqual(expected, context.ToString()); + } + + [TestMethod] + public void TestParse() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var json = """ + { + "type":"Neo.Network.P2P.Payloads.Transaction", + "data":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFmUJDLobcPtqo9vZKIdjXsd8fVGwEAARI=", + "items":{ + "0xbecaad15c0ea585211faf99738a4354014f177f2":{ + "script":"IQJv8DuUkkHOHa3UNRnmlg4KhbQaaaBcMoEDqivOFZTKFmh0dHaq", + "parameters":[{"type":"Signature","value":"AQ=="}], + "signatures":{"03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c":"AQ=="} + } + }, + "network": + """ + TestProtocolSettings.Default.Network + "}"; + var ret = ContractParametersContext.Parse(json, snapshotCache); + Assert.AreEqual("0x1bd5c777ec35768892bd3daab60fb7a1cb905066", ret.ScriptHashes[0].ToString()); + Assert.AreEqual(new byte[] { 18 }.ToHexString(), ((Transaction)ret.Verifiable).Script.Span.ToHexString()); + } + + [TestMethod] + public void TestFromJson() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var json = """ + { + "type":"wrongType", + "data":"00000000007c97764845172d827d3c863743293931a691271a0000000000000000000000000000000000000000000100", + "items":{ + "0x1bd5c777ec35768892bd3daab60fb7a1cb905066":{ + "script":"21026ff03b949241ce1dadd43519e6960e0a85b41a69a05c328103aa2bce1594ca1650680a906ad4", + "parameters":[{"type":"Signature","value":"01"}] + } + } + } + """; + Assert.ThrowsExactly(() => ContractParametersContext.Parse(json, snapshotCache)); + } + + [TestMethod] + public void TestAdd() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + Transaction tx = TestUtils.GetTransaction(UInt160.Zero); + var context1 = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsFalse(context1.Add(contract, 0, new byte[] { 0x01 })); + + tx = TestUtils.GetTransaction(UInt160.Parse("0x902e0d38da5e513b6d07c1c55b85e77d3dce8063")); + var context2 = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsTrue(context2.Add(contract, 0, new byte[] { 0x01 })); + //test repeatlly createItem + Assert.IsTrue(context2.Add(contract, 0, new byte[] { 0x01 })); + } + + [TestMethod] + public void TestGetParameter() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + Transaction tx = TestUtils.GetTransaction(UInt160.Parse("0x902e0d38da5e513b6d07c1c55b85e77d3dce8063")); + var context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsNull(context.GetParameter(tx.Sender, 0)); + + context.Add(contract, 0, new byte[] { 0x01 }); + var ret = context.GetParameter(tx.Sender, 0)!; + Assert.AreEqual(new byte[] { 0x01 }.ToHexString(), ((byte[])ret.Value!).ToHexString()); + } + + [TestMethod] + public void TestGetWitnesses() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + Transaction tx = TestUtils.GetTransaction(UInt160.Parse("0x902e0d38da5e513b6d07c1c55b85e77d3dce8063")); + var context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + context.Add(contract, 0, new byte[] { 0x01 }); + + var witnesses = context.GetWitnesses(); + Assert.HasCount(1, witnesses); + Assert.AreEqual(new byte[] { (byte)OpCode.PUSHDATA1, 0x01, 0x01 }.ToHexString(), witnesses[0].InvocationScript.Span.ToHexString()); + Assert.AreEqual(contract.Script.ToHexString(), witnesses[0].VerificationScript.Span.ToHexString()); + } + + [TestMethod] + public void TestAddSignature() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var singleSender = UInt160.Parse("0x902e0d38da5e513b6d07c1c55b85e77d3dce8063"); + Transaction tx = TestUtils.GetTransaction(singleSender); + + //singleSign + + var context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsTrue(context.AddSignature(contract, key.PublicKey, [0x01])); + + var contract1 = new Contract + { + Script = Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = Array.Empty() + }; + context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsFalse(context.AddSignature(contract1, key.PublicKey, [0x01])); + + contract1 = new Contract + { + Script = Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = [ContractParameterType.Signature, ContractParameterType.Signature] + }; + Assert.ThrowsExactly(() => context.AddSignature(contract1, key.PublicKey, [0x01])); + + //multiSign + byte[] privateKey2 = Enumerable.Repeat((byte)0x01, 31).Append((byte)0x02).ToArray(); + var key2 = new KeyPair(privateKey2); + var multiSignContract = Contract.CreateMultiSigContract(2, [key.PublicKey, key2.PublicKey]); + var multiSender = UInt160.Parse("0xf76b51bc6605ac3cfcd188173af0930507f51210"); + + tx = TestUtils.GetTransaction(multiSender); + context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsTrue(context.AddSignature(multiSignContract, key.PublicKey, [0x01])); + Assert.IsTrue(context.AddSignature(multiSignContract, key2.PublicKey, [0x01])); + + tx = TestUtils.GetTransaction(singleSender); + context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsFalse(context.AddSignature(multiSignContract, key.PublicKey, [0x01])); + + tx = TestUtils.GetTransaction(multiSender); + context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + byte[] privateKey3 = Enumerable.Repeat((byte)0x01, 31).Append((byte)0x03).ToArray(); + var key3 = new KeyPair(privateKey3); + Assert.IsFalse(context.AddSignature(multiSignContract, key3.PublicKey, [0x01])); + } + + [TestMethod] + public void TestAddWithScriptHash() + { + var h160 = UInt160.Parse("0x902e0d38da5e513b6d07c1c55b85e77d3dce8063"); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var tx = TestUtils.GetTransaction(h160); + var context = new ContractParametersContext(snapshotCache, tx, TestProtocolSettings.Default.Network); + Assert.IsFalse(context.AddWithScriptHash(h160)); + + var contract = new ContractState() + { + Hash = h160, + Nef = (NefFile)RuntimeHelpers.GetUninitializedObject(typeof(NefFile)), + Manifest = new() + { + Name = "TestContract", + Groups = [], + SupportedStandards = [], + Abi = new() { Methods = [new() { Name = ContractBasicMethod.Verify, Parameters = [] }], Events = [] }, + Permissions = [], + Trusts = WildcardContainer.CreateWildcard() + } + }; + snapshotCache.AddContract(h160, contract); + Assert.IsTrue(context.AddWithScriptHash(h160)); + + snapshotCache.DeleteContract(h160); + contract.Manifest.Abi = new() + { + Methods = [new() { + Name = ContractBasicMethod.Verify, + Parameters = [new() { Name = "signature", Type = ContractParameterType.Signature }], + }], + Events = [] + }; + snapshotCache.AddContract(h160, contract); + Assert.IsFalse(context.AddWithScriptHash(h160)); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_ContractState.cs b/tests/Neo.UnitTests/SmartContract/UT_ContractState.cs new file mode 100644 index 0000000000..5c9127f6c4 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_ContractState.cs @@ -0,0 +1,97 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ContractState.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; +using Neo.VM; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_ContractState +{ + ContractState contract = null!; + readonly byte[] script = { 0x01 }; + ContractManifest manifest = null!; + + [TestInitialize] + public void TestSetup() + { + manifest = TestUtils.CreateDefaultManifest(); + contract = new ContractState + { + Nef = new NefFile + { + Compiler = nameof(ScriptBuilder), + Source = string.Empty, + Tokens = Array.Empty(), + Script = script + }, + Hash = script.ToScriptHash(), + Manifest = manifest + }; + contract.Nef.CheckSum = NefFile.ComputeChecksum(contract.Nef); + } + + [TestMethod] + public void TestGetScriptHash() + { + // _scriptHash == null + Assert.AreEqual(script.ToScriptHash(), contract.Hash); + // _scriptHash != null + Assert.AreEqual(script.ToScriptHash(), contract.Hash); + } + + [TestMethod] + public void TestClone() + { + var clone = (ContractState)((IInteroperable)contract).Clone(); + CollectionAssert.AreEqual( + BinarySerializer.Serialize(((IInteroperable)clone).ToStackItem(null), ExecutionEngineLimits.Default), + BinarySerializer.Serialize(((IInteroperable)contract).ToStackItem(null), ExecutionEngineLimits.Default)); + + clone.Nef.CheckSum++; + Assert.AreNotEqual(clone.Nef.CheckSum, contract.Nef.CheckSum); + clone.Manifest.Name += "X"; + Assert.AreNotEqual(clone.Manifest.Name, contract.Manifest.Name); + CollectionAssert.AreNotEqual( + BinarySerializer.Serialize((clone as IInteroperable).ToStackItem(null), ExecutionEngineLimits.Default), + BinarySerializer.Serialize((contract as IInteroperable).ToStackItem(null), ExecutionEngineLimits.Default) + ); + } + + [TestMethod] + public void TestIInteroperable() + { + IInteroperable newContract = (ContractState)RuntimeHelpers.GetUninitializedObject(typeof(ContractState)); + newContract.FromStackItem(contract.ToStackItem(null)); + Assert.AreEqual(contract.Manifest.ToJson().ToString(), ((ContractState)newContract).Manifest.ToJson().ToString()); + Assert.IsTrue(((ContractState)newContract).Script.Span.SequenceEqual(contract.Script.Span)); + } + + [TestMethod] + public void TestCanCall() + { + var temp = new ContractState() { Hash = UInt160.Zero, Nef = null!, Manifest = TestUtils.CreateDefaultManifest() }; + Assert.IsTrue(temp.CanCall(new() { Hash = UInt160.Zero, Nef = null!, Manifest = TestUtils.CreateDefaultManifest() }, "AAA")); + } + + [TestMethod] + public void TestToJson() + { + JObject json = contract.ToJson(); + Assert.AreEqual("0x820944cfdc70976602d71b0091445eedbc661bc5", json["hash"]!.AsString()); + Assert.AreEqual("AQ==", json["nef"]!["script"]!.AsString()); + Assert.AreEqual(manifest.ToJson().AsString(), json["manifest"]!.AsString()); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_DeployedContract.cs b/tests/Neo.UnitTests/SmartContract/UT_DeployedContract.cs new file mode 100644 index 0000000000..5843c31f9c --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_DeployedContract.cs @@ -0,0 +1,93 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_DeployedContract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract.Manifest; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_DeployedContract +{ + [TestMethod] + public void TestGetScriptHash() + { + var contract = new DeployedContract(new ContractState() + { + Manifest = new ContractManifest() + { + Name = "", + Groups = [], + SupportedStandards = [], + Abi = new ContractAbi() + { + Methods = new[] + { + new ContractMethodDescriptor() + { + Name = "verify", + Parameters = Array.Empty() + } + }, + Events = [] + }, + Permissions = [], + Trusts = WildcardContainer.CreateWildcard() + }, + Nef = new NefFile + { + Compiler = "", + Source = "", + Tokens = [], + Script = new byte[] { 1, 2, 3 } + }, + Hash = new byte[] { 1, 2, 3 }.ToScriptHash() + }); + + Assert.AreEqual("0xb2e3fe334830b4741fa5d762f2ab36b90b86c49b", contract.ScriptHash.ToString()); + } + + [TestMethod] + public void TestErrors() + { + Assert.ThrowsExactly(() => _ = new DeployedContract(new ContractState() + { + Hash = UInt160.Zero, + Manifest = new ContractManifest() + { + Name = "", + Groups = [], + SupportedStandards = [], + Abi = new ContractAbi() + { + Methods = new[] + { + new ContractMethodDescriptor() + { + Name = "noverify", + Parameters = Array.Empty() + } + }, + Events = [] + }, + Permissions = [], + Trusts = WildcardContainer.CreateWildcard() + }, + Nef = new NefFile + { + Compiler = "", + Source = "", + Tokens = [], + Script = new byte[] { 1, 2, 3 } + } + })); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_Helper.cs b/tests/Neo.UnitTests/SmartContract/UT_Helper.cs new file mode 100644 index 0000000000..9297a73b6b --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_Helper.cs @@ -0,0 +1,159 @@ +// Copyright (C) 2015-2025 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 Neo.Cryptography.ECC; +using Neo.Factories; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.Wallets; +using static Neo.SmartContract.Helper; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_Helper +{ + private KeyPair _key = null!; + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var pk = RandomNumberFactory.NextBytes(32); + _key = new KeyPair(pk); + } + + [TestMethod] + public void TestGetContractHash() + { + var nef = new NefFile() + { + Compiler = "test", + Source = string.Empty, + Tokens = Array.Empty(), + Script = new byte[] { 1, 2, 3 } + }; + nef.CheckSum = NefFile.ComputeChecksum(nef); + + Assert.AreEqual("0x9b9628e4f1611af90e761eea8cc21372380c74b6", GetContractHash(UInt160.Zero, nef.CheckSum, "").ToString()); + Assert.AreEqual("0x66eec404d86b918d084e62a29ac9990e3b6f4286", GetContractHash(UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01"), nef.CheckSum, "").ToString()); + } + + [TestMethod] + public void TestIsMultiSigContract() + { + var case1 = new byte[] + { + 0, 2, 12, 33, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, + 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 12, 33, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 0, + }; + Assert.IsFalse(IsMultiSigContract(case1)); + + var case2 = new byte[] + { + 18, 12, 33, 2, 111, 240, 59, 148, 146, 65, 206, 29, 173, 212, 53, 25, 230, 150, 14, 10, 133, 180, 26, + 105, 160, 92, 50, 129, 3, 170, 43, 206, 21, 148, 202, 22, 12, 33, 2, 111, 240, 59, 148, 146, 65, 206, + 29, 173, 212, 53, 25, 230, 150, 14, 10, 133, 180, 26, 105, 160, 92, 50, 129, 3, 170, 43, 206, 21, 148, + 202, 22, 18 + }; + Assert.IsFalse(IsMultiSigContract(case2)); + } + + [TestMethod] + // TestIsMultiSigContract_WrongCurve checks that multisignature verification script based on points + // not from Secp256r1 curve fails IsMultiSigContract check without any exception. + public void TestIsMultiSigContract_WrongCurve() + { + // A set of points on Koblitz curve in uncompressed representation. One of the points + // (the first one) is specially selected, this point can't be restored on Secp256r1 + // from compressed form, whereas three other points can be restored on both Secp256r1 + // and Koblitz curves. + var pubs = new List() + { + ECPoint.Parse("047b4e72ae854b6a0955b3e02d92651ab7fa641a936066776ad438f95bb674a269a63ff98544691663d91a6cfcd215831f01bfb7a226363a6c5c67ef14541dba07", ECCurve.Secp256k1), + ECPoint.Parse("040486468683c112125978ffe876245b2006bfe739aca8539b67335079262cb27ad0dedc9e5583f99b61c6f46bf80b97eaec3654b87add0e5bd7106c69922a229d", ECCurve.Secp256k1), + ECPoint.Parse("040d26fc2ad3b1aae20f040b5f83380670f8ef5c2b2ac921ba3bdd79fd0af0525177715fd4370b1012ddd10579698d186ab342c223da3e884ece9cab9b6638c7bb", ECCurve.Secp256k1), + ECPoint.Parse("04a114d72fe2997cdac67427b6f39ea08ed46213c8bb6a461bbac2a6212cf43fb510f8adf59b0b087a7859f96d0288e5e94800eab8388f30f03f92b2e4d807dfce", ECCurve.Secp256k1) + }; + const int m = 3; + + var badScript = Contract.CreateMultiSigRedeemScript(m, pubs); + Assert.IsFalse(IsMultiSigContract(badScript, out _, out ECPoint[]? _)); // enforce runtime point decoding by specifying ECPoint[] out variable. + Assert.IsTrue(IsMultiSigContract(badScript)); // this overload is unlucky since it doesn't perform ECPoint decoding. + + // Exclude the first special point and check one more time, both methods should return true. + var goodScript = Contract.CreateMultiSigRedeemScript(m, pubs.Skip(1).ToArray()); + Assert.IsTrue(IsMultiSigContract(goodScript, out _, out ECPoint[]? _)); // enforce runtime point decoding by specifying ECPoint[] out variable. + Assert.IsTrue(IsMultiSigContract(goodScript)); // this overload is unlucky since it doesn't perform ECPoint decoding. + } + + [TestMethod] + // TestIsSignatureContract_WrongCurve checks that signature verification script based on point + // not from Secp256r1 curve passes IsSignatureContract check without any exception. + public void TestIsSignatureContract_WrongCurve() + { + // A special point on Koblitz curve that can't be restored at Secp256r1 from compressed form. + var pub = ECPoint.Parse("047b4e72ae854b6a0955b3e02d92651ab7fa641a936066776ad438f95bb674a269a63ff98544691663d91a6cfcd215831f01bfb7a226363a6c5c67ef14541dba07", ECCurve.Secp256k1); + var script = Contract.CreateSignatureRedeemScript(pub); + + // IsSignatureContract should pass since it doesn't perform ECPoint decoding. + Assert.IsTrue(IsSignatureContract(script)); + } + + [TestMethod] + public void TestSignatureContractCost() + { + var snapshot = _snapshotCache.CloneCache(); + var contract = Contract.CreateSignatureContract(_key.PublicKey); + + var tx = TestUtils.CreateRandomHashTransaction(); + tx.Signers[0].Account = contract.ScriptHash; + + using ScriptBuilder invocationScript = new(); + invocationScript.EmitPush(Neo.Wallets.Helper.Sign(tx, _key, TestProtocolSettings.Default.Network)); + tx.Witnesses = new[] { new Witness() { InvocationScript = invocationScript.ToArray(), VerificationScript = contract.Script } }; + + using var engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot, null, TestProtocolSettings.Default); + engine.LoadScript(contract.Script); + engine.LoadScript(new Script(invocationScript.ToArray(), true), configureState: p => p.CallFlags = CallFlags.None); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.IsTrue(engine.ResultStack.Pop().GetBoolean()); + + Assert.AreEqual(SignatureContractCost() * PolicyContract.DefaultExecFeeFactor, engine.FeeConsumed); + } + + [TestMethod] + public void TestMultiSignatureContractCost() + { + var snapshot = _snapshotCache.CloneCache(); + var contract = Contract.CreateMultiSigContract(1, new ECPoint[] { _key.PublicKey }); + + var tx = TestUtils.CreateRandomHashTransaction(); + tx.Signers[0].Account = contract.ScriptHash; + + using ScriptBuilder invocationScript = new(); + invocationScript.EmitPush(Neo.Wallets.Helper.Sign(tx, _key, TestProtocolSettings.Default.Network)); + + using var engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot, null, TestProtocolSettings.Default); + engine.LoadScript(contract.Script); + engine.LoadScript(new Script(invocationScript.ToArray(), true), configureState: p => p.CallFlags = CallFlags.None); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.IsTrue(engine.ResultStack.Pop().GetBoolean()); + + Assert.AreEqual(MultiSignatureContractCost(1, 1) * PolicyContract.DefaultExecFeeFactor, engine.FeeConsumed); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_InteropPrices.cs b/tests/Neo.UnitTests/SmartContract/UT_InteropPrices.cs new file mode 100644 index 0000000000..b3ab0c18ce --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_InteropPrices.cs @@ -0,0 +1,216 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_InteropPrices.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.SmartContract; +using Neo.UnitTests.Extensions; +using Neo.VM; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_InteropPrices +{ + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + } + + [TestMethod] + public void ApplicationEngineFixedPrices() + { + var snapshot = _snapshotCache.CloneCache(); + // System.Runtime.CheckWitness: f827ec8c (price is 200) + byte[] SyscallSystemRuntimeCheckWitnessHash = new byte[] { 0x68, 0xf8, 0x27, 0xec, 0x8c }; + using (ApplicationEngine ae = ApplicationEngine.Create(TriggerType.Application, null, snapshot, gas: 0)) + { + ae.LoadScript(SyscallSystemRuntimeCheckWitnessHash); + Assert.AreEqual(0_00001024L, ApplicationEngine.System_Runtime_CheckWitness.FixedPrice); + } + + // System.Storage.GetContext: 9bf667ce (price is 1) + byte[] SyscallSystemStorageGetContextHash = new byte[] { 0x68, 0x9b, 0xf6, 0x67, 0xce }; + using (ApplicationEngine ae = ApplicationEngine.Create(TriggerType.Application, null, snapshot, gas: 0)) + { + ae.LoadScript(SyscallSystemStorageGetContextHash); + Assert.AreEqual(0_00000016L, ApplicationEngine.System_Storage_GetContext.FixedPrice); + } + + // System.Storage.Get: 925de831 (price is 100) + byte[] SyscallSystemStorageGetHash = new byte[] { 0x68, 0x92, 0x5d, 0xe8, 0x31 }; + using (ApplicationEngine ae = ApplicationEngine.Create(TriggerType.Application, null, snapshot, gas: 0)) + { + ae.LoadScript(SyscallSystemStorageGetHash); + Assert.AreEqual(32768L, ApplicationEngine.System_Storage_Get.FixedPrice); + } + } + + /// + /// Put without previous content (should charge per byte used) + /// + [TestMethod] + public void ApplicationEngineRegularPut() + { + var snapshot = _snapshotCache.CloneCache(); + var key = new byte[] { (byte)OpCode.PUSH1 }; + var value = new byte[] { (byte)OpCode.PUSH1 }; + + byte[] script = CreatePutScript(key, value); + + ContractState contractState = TestUtils.GetContract(script); + + StorageKey skey = TestUtils.GetStorageKey(contractState.Id, key); + StorageItem sItem = TestUtils.GetStorageItem(Array.Empty()); + + snapshot.Add(skey, sItem); + snapshot.AddContract(script.ToScriptHash(), contractState); + + using var ae = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + Debugger debugger = new(ae); + ae.LoadScript(script); + debugger.StepInto(); + debugger.StepInto(); + debugger.StepInto(); + var setupPrice = ae.FeeConsumed; + debugger.Execute(); + Assert.AreEqual(ae.StoragePrice * value.Length + (1 << 15) * 30, ae.FeeConsumed - setupPrice); + } + + /// + /// Reuses the same amount of storage. Should cost 0. + /// + [TestMethod] + public void ApplicationEngineReusedStorage_FullReuse() + { + var snapshot = _snapshotCache.CloneCache(); + var key = new byte[] { (byte)OpCode.PUSH1 }; + var value = new byte[] { (byte)OpCode.PUSH1 }; + + byte[] script = CreatePutScript(key, value); + + ContractState contractState = TestUtils.GetContract(script); + + StorageKey skey = TestUtils.GetStorageKey(contractState.Id, key); + StorageItem sItem = TestUtils.GetStorageItem(value); + + snapshot.Add(skey, sItem); + snapshot.AddContract(script.ToScriptHash(), contractState); + + using ApplicationEngine applicationEngine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + Debugger debugger = new(applicationEngine); + applicationEngine.LoadScript(script); + debugger.StepInto(); + debugger.StepInto(); + debugger.StepInto(); + var setupPrice = applicationEngine.FeeConsumed; + debugger.Execute(); + Assert.AreEqual(1 * applicationEngine.StoragePrice + (1 << 15) * 30, applicationEngine.FeeConsumed - setupPrice); + } + + /// + /// Reuses one byte and allocates a new one + /// It should only pay for the second byte. + /// + [TestMethod] + public void ApplicationEngineReusedStorage_PartialReuse() + { + var snapshot = _snapshotCache.CloneCache(); + var key = new byte[] { (byte)OpCode.PUSH1 }; + var oldValue = new byte[] { (byte)OpCode.PUSH1 }; + var value = new byte[] { (byte)OpCode.PUSH1, (byte)OpCode.PUSH1 }; + + byte[] script = CreatePutScript(key, value); + + ContractState contractState = TestUtils.GetContract(script); + + StorageKey skey = TestUtils.GetStorageKey(contractState.Id, key); + StorageItem sItem = TestUtils.GetStorageItem(oldValue); + + snapshot.Add(skey, sItem); + snapshot.AddContract(script.ToScriptHash(), contractState); + + using ApplicationEngine ae = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + Debugger debugger = new(ae); + ae.LoadScript(script); + debugger.StepInto(); + debugger.StepInto(); + debugger.StepInto(); + var setupPrice = ae.FeeConsumed; + debugger.StepInto(); + debugger.StepInto(); + Assert.AreEqual((1 + (oldValue.Length / 4) + value.Length - oldValue.Length) * ae.StoragePrice + (1 << 15) * 30, ae.FeeConsumed - setupPrice); + } + + /// + /// Use put for the same key twice. + /// Pays for 1 extra byte for the first Put and 1 byte for the second basic fee (as value2.length == value1.length). + /// + [TestMethod] + public void ApplicationEngineReusedStorage_PartialReuseTwice() + { + var snapshot = _snapshotCache.CloneCache(); + var key = new byte[] { (byte)OpCode.PUSH1 }; + var oldValue = new byte[] { (byte)OpCode.PUSH1 }; + var value = new byte[] { (byte)OpCode.PUSH1, (byte)OpCode.PUSH1 }; + + byte[] script = CreateMultiplePutScript(key, value); + + ContractState contractState = TestUtils.GetContract(script); + + StorageKey skey = TestUtils.GetStorageKey(contractState.Id, key); + StorageItem sItem = TestUtils.GetStorageItem(oldValue); + + snapshot.Add(skey, sItem); + snapshot.AddContract(script.ToScriptHash(), contractState); + + using ApplicationEngine ae = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + Debugger debugger = new(ae); + ae.LoadScript(script); + debugger.StepInto(); //push value + debugger.StepInto(); //push key + debugger.StepInto(); //syscall Storage.GetContext + debugger.StepInto(); //syscall Storage.Put + debugger.StepInto(); //push value + debugger.StepInto(); //push key + debugger.StepInto(); //syscall Storage.GetContext + var setupPrice = ae.FeeConsumed; + debugger.StepInto(); //syscall Storage.Put + Assert.AreEqual((sItem.Value.Length / 4 + 1) * ae.StoragePrice + (1 << 15) * 30, ae.FeeConsumed - setupPrice); // = PUT basic fee + } + + private static byte[] CreateMultiplePutScript(byte[] key, byte[] value, int times = 2) + { + var scriptBuilder = new ScriptBuilder(); + + for (int i = 0; i < times; i++) + { + scriptBuilder.EmitPush(value); + scriptBuilder.EmitPush(key); + scriptBuilder.EmitSysCall(ApplicationEngine.System_Storage_GetContext); + scriptBuilder.EmitSysCall(ApplicationEngine.System_Storage_Put); + } + + return scriptBuilder.ToArray(); + } + + private static byte[] CreatePutScript(byte[] key, byte[] value) + { + var scriptBuilder = new ScriptBuilder(); + scriptBuilder.EmitPush(value); + scriptBuilder.EmitPush(key); + scriptBuilder.EmitSysCall(ApplicationEngine.System_Storage_GetContext); + scriptBuilder.EmitSysCall(ApplicationEngine.System_Storage_Put); + return scriptBuilder.ToArray(); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_InteropService.NEO.cs b/tests/Neo.UnitTests/SmartContract/UT_InteropService.NEO.cs new file mode 100644 index 0000000000..f6b7c2a5fc --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_InteropService.NEO.cs @@ -0,0 +1,197 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_InteropService.NEO.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions; +using Neo.Extensions.IO; +using Neo.Network.P2P; +using Neo.SmartContract; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Native; +using Neo.UnitTests.Extensions; +using Neo.VM; +using Neo.Wallets; + +namespace Neo.UnitTests.SmartContract; + +public partial class UT_InteropService +{ + [TestMethod] + public void TestCheckSig() + { + var engine = GetEngine(true); + var iv = engine.ScriptContainer!; + var message = iv.GetSignData(TestProtocolSettings.Default.Network); + var privateKey = Enumerable.Repeat((byte)0x01, 32).ToArray(); + var keyPair = new KeyPair(privateKey); + var pubkey = keyPair.PublicKey; + var signature = Crypto.Sign(message, privateKey); + Assert.IsTrue(engine.CheckSig(pubkey.EncodePoint(false), signature)); + Assert.ThrowsExactly(() => engine.CheckSig(new byte[70], signature)); + } + + [TestMethod] + public void TestCrypto_CheckMultiSig() + { + var engine = GetEngine(true); + var iv = engine.ScriptContainer!; + var message = iv.GetSignData(TestProtocolSettings.Default.Network); + + var privkey1 = Enumerable.Repeat((byte)0x01, 32).ToArray(); + var key1 = new KeyPair(privkey1); + var pubkey1 = key1.PublicKey; + var signature1 = Crypto.Sign(message, privkey1); + + var privkey2 = Enumerable.Repeat((byte)0x01, 32).ToArray(); + var key2 = new KeyPair(privkey2); + var pubkey2 = key2.PublicKey; + var signature2 = Crypto.Sign(message, privkey2); + + var pubkeys = new[] { pubkey1.EncodePoint(false), pubkey2.EncodePoint(false) }; + var signatures = new[] { signature1, signature2 }; + Assert.IsTrue(engine.CheckMultisig(pubkeys, signatures)); + + pubkeys = []; + Assert.ThrowsExactly(() => _ = engine.CheckMultisig(pubkeys, signatures)); + + pubkeys = [pubkey1.EncodePoint(false), pubkey2.EncodePoint(false)]; + signatures = []; + Assert.ThrowsExactly(() => _ = engine.CheckMultisig(pubkeys, signatures)); + + pubkeys = [pubkey1.EncodePoint(false), pubkey2.EncodePoint(false)]; + signatures = [signature1, new byte[64]]; + Assert.IsFalse(engine.CheckMultisig(pubkeys, signatures)); + + pubkeys = [pubkey1.EncodePoint(false), new byte[70]]; + signatures = [signature1, signature2]; + Assert.ThrowsExactly(() => _ = engine.CheckMultisig(pubkeys, signatures)); + } + + [TestMethod] + public void TestContract_Create() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var nef = new NefFile() + { + Script = Enumerable.Repeat((byte)OpCode.RET, byte.MaxValue).ToArray(), + Source = string.Empty, + Compiler = "", + Tokens = [] + }; + nef.CheckSum = NefFile.ComputeChecksum(nef); + var nefFile = nef.ToArray(); + var manifest = TestUtils.CreateDefaultManifest(); + Assert.ThrowsExactly(() => _ = snapshotCache.DeployContract(UInt160.Zero, nefFile, new byte[ContractManifest.MaxLength + 1])); + Assert.ThrowsExactly(() => _ = snapshotCache.DeployContract(UInt160.Zero, nefFile, manifest.ToJson().ToByteArray(true), 10000000)); + + var scriptExceedMaxLength = new NefFile() + { + Script = new byte[ExecutionEngineLimits.Default.MaxItemSize - 50], + Source = string.Empty, + Compiler = "", + Tokens = [], + }; + scriptExceedMaxLength.CheckSum = NefFile.ComputeChecksum(scriptExceedMaxLength); + + Assert.ThrowsExactly(() => _ = scriptExceedMaxLength.ToArray().AsSerializable()); + Assert.ThrowsExactly(() => _ = snapshotCache.DeployContract(UInt160.Zero, scriptExceedMaxLength.ToArray(), manifest.ToJson().ToByteArray(true))); + + var scriptZeroLength = Array.Empty(); + Assert.ThrowsExactly(() => _ = snapshotCache.DeployContract(UInt160.Zero, scriptZeroLength, manifest.ToJson().ToByteArray(true))); + + var manifestZeroLength = Array.Empty(); + Assert.ThrowsExactly(() => _ = snapshotCache.DeployContract(UInt160.Zero, nefFile, manifestZeroLength)); + + manifest = TestUtils.CreateDefaultManifest(); + var ret = snapshotCache.DeployContract(UInt160.Zero, nefFile, manifest.ToJson().ToByteArray(false)); + Assert.AreEqual("0x7b37d4bd3d87f53825c3554bd1a617318235a685", ret.Hash.ToString()); + Assert.ThrowsExactly(() => _ = snapshotCache.DeployContract(UInt160.Zero, nefFile, manifest.ToJson().ToByteArray(false))); + + var state = TestUtils.GetContract(); + snapshotCache.AddContract(state.Hash, state); + + Assert.ThrowsExactly(() => _ = snapshotCache.DeployContract(UInt160.Zero, nefFile, manifest.ToJson().ToByteArray(false))); + } + + [TestMethod] + public void TestContract_Update() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var nef = new NefFile() + { + Script = new[] { (byte)OpCode.RET }, + Source = string.Empty, + Compiler = "", + Tokens = [], + }; + nef.CheckSum = NefFile.ComputeChecksum(nef); + + var manifest = TestUtils.CreateDefaultManifest(); + var privkey = Enumerable.Repeat((byte)0x01, 32).ToArray(); + var key = new KeyPair(privkey); + + var pubkey = key.PublicKey; + var state = TestUtils.GetContract(); + var signature = Crypto.Sign(state.Hash.ToArray(), privkey); + manifest.Groups = [new() { PubKey = pubkey, Signature = signature }]; + + var storageItem = new StorageItem + { + Value = new byte[] { 0x01 } + }; + + var storageKey = new StorageKey + { + Id = state.Id, + Key = new byte[] { 0x01 } + }; + snapshotCache.AddContract(state.Hash, state); + snapshotCache.Add(storageKey, storageItem); + Assert.AreEqual(0, state.UpdateCounter); + snapshotCache.UpdateContract(state.Hash, nef.ToArray(), manifest.ToJson().ToByteArray(false)); + var ret = NativeContract.ContractManagement.GetContract(snapshotCache, state.Hash)!; + Assert.HasCount(1, snapshotCache.Find(BitConverter.GetBytes(state.Id)).ToList()); + Assert.AreEqual(1, ret.UpdateCounter); + Assert.AreEqual(state.Id, ret.Id); + Assert.AreEqual(manifest.ToJson().ToString(), ret.Manifest.ToJson().ToString()); + Assert.AreEqual(nef.Script.Span.ToHexString().ToString(), ret.Script.Span.ToHexString()); + } + + [TestMethod] + public void TestStorage_Find() + { + var snapshot = _snapshotCache.CloneCache(); + var state = TestUtils.GetContract(); + + var storageItem = new StorageItem + { + Value = new byte[] { 0x01, 0x02, 0x03, 0x04 } + }; + var storageKey = new StorageKey + { + Id = state.Id, + Key = new byte[] { 0x01 } + }; + snapshot.AddContract(state.Hash, state); + snapshot.Add(storageKey, storageItem); + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + engine.LoadScript(new byte[] { 0x01 }); + + var iterator = engine.Find(new StorageContext + { + Id = state.Id, + IsReadOnly = false + }, [0x01], FindOptions.ValuesOnly); + iterator.Next(); + var ele = iterator.Value(null); + Assert.AreEqual(storageItem.Value.Span.ToHexString(), ele.GetSpan().ToHexString()); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_InteropService.cs b/tests/Neo.UnitTests/SmartContract/UT_InteropService.cs new file mode 100644 index 0000000000..3c7a25c117 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_InteropService.cs @@ -0,0 +1,897 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_InteropService.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.MsTest; +using Akka.Util.Internal; +using Neo.Cryptography; +using Neo.Extensions.IO; +using Neo.Extensions.VM; +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.UnitTests.Extensions; +using Neo.VM; +using Neo.VM.Types; +using Neo.Wallets; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using Array = System.Array; +using ECCurve = Neo.Cryptography.ECC.ECCurve; +using ECPoint = Neo.Cryptography.ECC.ECPoint; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public partial class UT_InteropService : TestKit +{ + private NeoSystem _system = null!; + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _system = TestBlockchain.GetSystem(); + _snapshotCache = _system.GetSnapshotCache(); + } + + [TestMethod] + public void Runtime_GetNotifications_Test() + { + UInt160 scriptHash2; + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + using (var script = new ScriptBuilder()) + { + // Notify method + + script.Emit(OpCode.SWAP, OpCode.NEWARRAY, OpCode.SWAP); + script.EmitSysCall(ApplicationEngine.System_Runtime_Notify); + + // Add return + + script.EmitPush(true); + script.Emit(OpCode.RET); + + // Mock contract + + scriptHash2 = script.ToArray().ToScriptHash(); + + snapshotCache.DeleteContract(scriptHash2); + var contract = TestUtils.GetContract(script.ToArray(), TestUtils.CreateManifest("test", ContractParameterType.Any, ContractParameterType.Integer, ContractParameterType.Integer)); + contract.Manifest.Abi.Events = + [ + new ContractEventDescriptor + { + Name = "testEvent2", + Parameters = + [ + new ContractParameterDefinition + { + Name = "testName", + Type = ContractParameterType.Any + } + ] + } + ]; + contract.Manifest.Permissions = + [ + new ContractPermission + { + Contract = ContractPermissionDescriptor.Create(scriptHash2), + Methods = WildcardContainer.Create(["test"]) + } + ]; + snapshotCache.AddContract(scriptHash2, contract); + } + + // Wrong length + + using (var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, null, ProtocolSettings.Default)) + using (var script = new ScriptBuilder()) + { + // Retrive + + script.EmitPush(1); + script.EmitSysCall(ApplicationEngine.System_Runtime_GetNotifications); + + // Execute + + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.FAULT, engine.Execute()); + } + + // All test + + using (var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, null, ProtocolSettings.Default)) + using (var script = new ScriptBuilder()) + { + // Notification + + script.EmitPush(0); + script.Emit(OpCode.NEWARRAY); + script.EmitPush("testEvent1"); + script.EmitSysCall(ApplicationEngine.System_Runtime_Notify); + + // Call script + + script.EmitDynamicCall(scriptHash2, "test", "testEvent2", 1); + + // Drop return + + script.Emit(OpCode.DROP); + + // Receive all notifications + + script.Emit(OpCode.PUSHNULL); + script.EmitSysCall(ApplicationEngine.System_Runtime_GetNotifications); + + // Execute + + engine.LoadScript(script.ToArray()); + engine.CurrentContext!.GetState().Contract = new() + { + Hash = UInt160.Zero, + Nef = null!, + Manifest = new() + { + Name = "", + Groups = [], + SupportedStandards = [], + Abi = new() + { + Methods = [], + Events = + [ + new ContractEventDescriptor + { + Name = "testEvent1", + Parameters = [] + } + ] + }, + Permissions = + [ + new ContractPermission + { + Contract = ContractPermissionDescriptor.Create(scriptHash2), + Methods = WildcardContainer.Create(["test"]) + } + ], + Trusts = WildcardContainer.CreateWildcard() + } + }; + var currentScriptHash = engine.EntryScriptHash!; + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.HasCount(2, engine.Notifications); + + Assert.IsInstanceOfType(engine.ResultStack.Peek()); + + var array = (Neo.VM.Types.Array)engine.ResultStack.Pop(); + + // Check syscall result + + AssertNotification(array[1], scriptHash2, "testEvent2"); + AssertNotification(array[0], currentScriptHash, "testEvent1"); + + // Check notifications + + Assert.AreEqual(scriptHash2, engine.Notifications[1].ScriptHash); + Assert.AreEqual("testEvent2", engine.Notifications[1].EventName); + + Assert.AreEqual(currentScriptHash, engine.Notifications[0].ScriptHash); + Assert.AreEqual("testEvent1", engine.Notifications[0].EventName); + } + + // Script notifications + + using (var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, null, ProtocolSettings.Default)) + using (var script = new ScriptBuilder()) + { + // Notification + + script.EmitPush(0); + script.Emit(OpCode.NEWARRAY); + script.EmitPush("testEvent1"); + script.EmitSysCall(ApplicationEngine.System_Runtime_Notify); + + // Call script + + script.EmitDynamicCall(scriptHash2, "test", "testEvent2", 1); + + // Drop return + + script.Emit(OpCode.DROP); + + // Receive all notifications + + script.EmitPush(scriptHash2.ToArray()); + script.EmitSysCall(ApplicationEngine.System_Runtime_GetNotifications); + + // Execute + + engine.LoadScript(script.ToArray()); + engine.CurrentContext!.GetState().Contract = new() + { + Hash = UInt160.Zero, + Nef = null!, + Manifest = new() + { + Name = "", + Groups = [], + SupportedStandards = [], + Abi = new() + { + Methods = [], + Events = + [ + new ContractEventDescriptor + { + Name = "testEvent1", + Parameters = [] + } + ] + }, + Permissions = + [ + new ContractPermission + { + Contract = ContractPermissionDescriptor.Create(scriptHash2), + Methods = WildcardContainer.Create(["test"]) + } + ], + Trusts = WildcardContainer.CreateWildcard() + } + }; + var currentScriptHash = engine.EntryScriptHash; + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.HasCount(2, engine.Notifications); + + Assert.IsInstanceOfType(engine.ResultStack.Peek()); + + var array = (Neo.VM.Types.Array)engine.ResultStack.Pop(); + + // Check syscall result + + AssertNotification(array[0], scriptHash2, "testEvent2"); + + // Check notifications + + Assert.AreEqual(scriptHash2, engine.Notifications[1].ScriptHash); + Assert.AreEqual("testEvent2", engine.Notifications[1].EventName); + + Assert.AreEqual(currentScriptHash, engine.Notifications[0].ScriptHash); + Assert.AreEqual("testEvent1", engine.Notifications[0].EventName); + } + + // Clean storage + + snapshotCache.DeleteContract(scriptHash2); + } + + private static void AssertNotification(StackItem stackItem, UInt160 scriptHash, string notification) + { + Assert.IsInstanceOfType(stackItem); + + var array = (Neo.VM.Types.Array)stackItem; + Assert.HasCount(3, array); + CollectionAssert.AreEqual(scriptHash.ToArray(), array[0].GetSpan().ToArray()); + Assert.AreEqual(notification, array[1].GetString()); + } + + [TestMethod] + public void TestExecutionEngine_GetScriptContainer() + { + Assert.IsInstanceOfType(GetEngine(true).GetScriptContainer()); + } + + [TestMethod] + public void TestExecutionEngine_GetCallingScriptHash() + { + // Test without + + var engine = GetEngine(true); + Assert.IsNull(engine.CallingScriptHash); + + // Test real + + using ScriptBuilder scriptA = new(); + scriptA.Emit(OpCode.DROP); // Drop arguments + scriptA.Emit(OpCode.DROP); // Drop method + scriptA.EmitSysCall(ApplicationEngine.System_Runtime_GetCallingScriptHash); + + var contract = TestUtils.GetContract(scriptA.ToArray(), TestUtils.CreateManifest("test", ContractParameterType.Any, ContractParameterType.String, ContractParameterType.Integer)); + engine = GetEngine(true, true, addScript: false); + engine.SnapshotCache.AddContract(contract.Hash, contract); + + using ScriptBuilder scriptB = new(); + scriptB.EmitDynamicCall(contract.Hash, "test", "0", 1); + engine.LoadScript(scriptB.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + + Assert.AreEqual(scriptB.ToArray().ToScriptHash().ToArray().ToHexString(), engine.ResultStack.Pop().GetSpan().ToHexString()); + } + + [TestMethod] + public void TestContract_GetCallFlags() + { + Assert.AreEqual(CallFlags.All, GetEngine().GetCallFlags()); + } + + [TestMethod] + public void TestRuntime_Platform() + { + Assert.AreEqual("NEO", ApplicationEngine.GetPlatform()); + } + + [TestMethod] + public void TestRuntime_CheckWitness() + { + byte[] privateKey = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + var keyPair = new KeyPair(privateKey); + var pubkey = keyPair.PublicKey; + + var engine = GetEngine(true); + ((Transaction)engine.ScriptContainer!).Signers[0].Account = Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash(); + ((Transaction)engine.ScriptContainer).Signers[0].Scopes = WitnessScope.CalledByEntry; + + Assert.IsTrue(engine.CheckWitness(pubkey.EncodePoint(true))); + Assert.IsTrue(engine.CheckWitness(((Transaction)engine.ScriptContainer).Sender.ToArray())); + + ((Transaction)engine.ScriptContainer).Signers = Array.Empty(); + Assert.IsFalse(engine.CheckWitness(pubkey.EncodePoint(true))); + + Assert.ThrowsExactly(() => engine.CheckWitness(Array.Empty())); + } + + [TestMethod] + public void TestRuntime_CheckWitness_Null_ScriptContainer() + { + byte[] privateKey = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + var keyPair = new KeyPair(privateKey); + var pubkey = keyPair.PublicKey; + + var engine = GetEngine(); + + Assert.IsFalse(engine.CheckWitness(pubkey.EncodePoint(true))); + } + + [TestMethod] + public void TestRuntime_Log() + { + var engine = GetEngine(true); + var message = "hello"; + engine.Log += LogEvent; + engine.RuntimeLog(Encoding.UTF8.GetBytes(message)); + Assert.AreEqual(new byte[] { 0x01, 0x02, 0x03 }.ToHexString(), ((Transaction)engine.ScriptContainer!).Script.Span.ToHexString()); + engine.Log -= LogEvent; + } + + [TestMethod] + public void TestRuntime_GetTime() + { + Block block = new() + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = [] + }; + var engine = GetEngine(true, hasBlock: true); + Assert.AreEqual(block.Timestamp, engine.GetTime()); + } + + [TestMethod] + public void TestRuntime_GetInvocationCounter() + { + var engine = GetEngine(); + Assert.AreEqual(1, engine.GetInvocationCounter()); + } + + [TestMethod] + public void TestRuntime_GetCurrentSigners() + { + using var engine = GetEngine(hasContainer: true); + Assert.AreEqual(UInt160.Zero, engine.GetCurrentSigners()![0].Account); + } + + [TestMethod] + public void TestRuntime_GetCurrentSigners_SysCall() + { + using ScriptBuilder script = new(); + script.EmitSysCall(ApplicationEngine.System_Runtime_CurrentSigners.Hash); + + // Null + + using var engineA = GetEngine(addScript: false, hasContainer: false); + + engineA.LoadScript(script.ToArray()); + engineA.Execute(); + Assert.AreEqual(VMState.HALT, engineA.State); + + var result = engineA.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + + // Not null + + using var engineB = GetEngine(addScript: false, hasContainer: true); + + engineB.LoadScript(script.ToArray()); + engineB.Execute(); + Assert.AreEqual(VMState.HALT, engineB.State); + + result = engineB.ResultStack.Pop(); + Assert.IsInstanceOfType(result, out var array); + Assert.HasCount(1, array); + result = array[0]; + Assert.IsInstanceOfType(result, out array); + Assert.HasCount(5, array); + result = array[0]; // Address + Assert.AreEqual(UInt160.Zero, new UInt160(result.GetSpan())); + } + + [TestMethod] + public void TestCrypto_Verify() + { + var engine = GetEngine(true); + var iv = engine.ScriptContainer!; + var message = iv.GetSignData(TestProtocolSettings.Default.Network); + byte[] privateKey = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + KeyPair keyPair = new(privateKey); + var pubkey = keyPair.PublicKey; + var signature = Crypto.Sign(message, privateKey); + Assert.IsTrue(engine.CheckSig(pubkey.EncodePoint(false), signature)); + + var wrongkey = pubkey.EncodePoint(false); + wrongkey[0] = 5; + Assert.ThrowsExactly(() => _ = engine.CheckSig(wrongkey, signature)); + } + + [TestMethod] + public void TestBlockchain_GetHeight() + { + var engine = GetEngine(true, true); + Assert.AreEqual((uint)0, NativeContract.Ledger.CurrentIndex(engine.SnapshotCache)); + } + + [TestMethod] + public void TestBlockchain_GetBlock() + { + var engine = GetEngine(true, true); + + Assert.IsNull(NativeContract.Ledger.GetBlock(engine.SnapshotCache, UInt256.Zero)); + + var data1 = new byte[] { 0x01, 0x01, 0x01 ,0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + Assert.IsNull(NativeContract.Ledger.GetBlock(engine.SnapshotCache, new UInt256(data1))); + Assert.IsNotNull(NativeContract.Ledger.GetBlock(engine.SnapshotCache, _system.GenesisBlock.Hash)); + } + + [TestMethod] + public void TestBlockchain_GetTransaction() + { + var engine = GetEngine(true, true); + var data1 = new byte[] { 0x01, 0x01, 0x01 ,0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + Assert.IsNull(NativeContract.Ledger.GetTransaction(engine.SnapshotCache, new UInt256(data1))); + } + + [TestMethod] + public void TestBlockchain_GetTransactionHeight() + { + var engine = GetEngine(addScript: false); + var state = new TransactionState() + { + BlockIndex = 0, + Transaction = TestUtils.CreateRandomHashTransaction() + }; + TestUtils.TransactionAdd(engine.SnapshotCache, state); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.Ledger.Hash, "getTransactionHeight", state.Transaction.Hash); + engine.LoadScript(script.ToArray()); + engine.Execute(); + Assert.AreEqual(VMState.HALT, engine.State); + + var result = engine.ResultStack.Pop(); + Assert.IsInstanceOfType(result); + Assert.AreEqual(0, result.GetInteger()); + } + + [TestMethod] + public void TestBlockchain_GetContract() + { + var engine = GetEngine(true, true); + var data1 = new byte[] { 0x01, 0x01, 0x01 ,0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01 }; + Assert.IsNull(NativeContract.ContractManagement.GetContract(engine.SnapshotCache, new UInt160(data1))); + Assert.IsFalse(NativeContract.ContractManagement.IsContract(engine.SnapshotCache, new UInt160(data1))); + + var state = TestUtils.GetContract(); + engine.SnapshotCache.AddContract(state.Hash, state); + engine = ApplicationEngine.Create(TriggerType.Application, null, engine.SnapshotCache); + engine.LoadScript(new byte[] { 0x01 }); + Assert.AreEqual(state.Hash, NativeContract.ContractManagement.GetContract(engine.SnapshotCache, state.Hash)!.Hash); + Assert.IsTrue(NativeContract.ContractManagement.IsContract(engine.SnapshotCache, state.Hash)); + } + + [TestMethod] + public void TestBlockchain_GetContractById() + { + var engine = GetEngine(true, true); + var contract = NativeContract.ContractManagement.GetContractById(engine.SnapshotCache, -1)!; + Assert.AreEqual(-1, contract.Id); + Assert.AreEqual(nameof(ContractManagement), contract.Manifest.Name); + } + + [TestMethod] + public void TestBlockchain_HasMethod() + { + var engine = GetEngine(true, true); + Assert.IsTrue(NativeContract.ContractManagement.HasMethod(engine.SnapshotCache, NativeContract.NEO.Hash, "symbol", 0)); + Assert.IsTrue(NativeContract.ContractManagement.HasMethod(engine.SnapshotCache, NativeContract.NEO.Hash, "transfer", 4)); + } + + [TestMethod] + public void TestBlockchain_ListContracts() + { + var engine = GetEngine(true, true); + var list = NativeContract.ContractManagement.ListContracts(engine.SnapshotCache); + list.ForEach(p => Assert.IsLessThan(0, p.Id)); + + var state = TestUtils.GetContract(); + engine.SnapshotCache.AddContract(state.Hash, state); + engine = ApplicationEngine.Create(TriggerType.Application, null, engine.SnapshotCache); + engine.LoadScript(new byte[] { 0x01 }); + Assert.AreEqual(state.Hash, NativeContract.ContractManagement.GetContract(engine.SnapshotCache, state.Hash)!.Hash); + + var list2 = NativeContract.ContractManagement.ListContracts(engine.SnapshotCache); + Assert.AreEqual(list.Count(), list2.Count()); + } + + [TestMethod] + public void TestStorage_GetContext() + { + var engine = GetEngine(false, true); + var state = TestUtils.GetContract(); + engine.SnapshotCache.AddContract(state.Hash, state); + engine.LoadScript(state.Script); + Assert.IsFalse(engine.GetStorageContext().IsReadOnly); + } + + [TestMethod] + public void TestStorage_GetReadOnlyContext() + { + var engine = GetEngine(false, true); + var state = TestUtils.GetContract(); + engine.SnapshotCache.AddContract(state.Hash, state); + engine.LoadScript(state.Script); + Assert.IsTrue(engine.GetReadOnlyContext().IsReadOnly); + } + + [TestMethod] + public void TestStorage_Get() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var state = TestUtils.GetContract(); + + var storageKey = new StorageKey + { + Id = state.Id, + Key = new byte[] { 0x01 } + }; + + var storageItem = new StorageItem + { + Value = new byte[] { 0x01, 0x02, 0x03, 0x04 } + }; + snapshotCache.AddContract(state.Hash, state); + snapshotCache.Add(storageKey, storageItem); + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache); + engine.LoadScript(new byte[] { 0x01 }); + + Assert.AreEqual(storageItem.Value.Span.ToHexString(), engine.Get(new StorageContext + { + Id = state.Id, + IsReadOnly = false + }, new byte[] { 0x01 })!.Value.Span.ToHexString()); + } + + [TestMethod] + public void TestStorage_Put() + { + var engine = GetEngine(false, true); + + //CheckStorageContext fail + var key = new byte[] { 0x01 }; + var value = new byte[] { 0x02 }; + var state = TestUtils.GetContract(); + var storageContext = new StorageContext + { + Id = state.Id, + IsReadOnly = false + }; + engine.Put(storageContext, key, value); + + //key.Length > MaxStorageKeySize + key = new byte[ApplicationEngine.MaxStorageKeySize + 1]; + value = [0x02]; + Assert.ThrowsExactly(() => engine.Put(storageContext, key, value)); + + //value.Length > MaxStorageValueSize + key = [0x01]; + value = new byte[ushort.MaxValue + 1]; + Assert.ThrowsExactly(() => engine.Put(storageContext, key, value)); + + //context.IsReadOnly + key = [0x01]; + value = [0x02]; + storageContext.IsReadOnly = true; + Assert.ThrowsExactly(() => engine.Put(storageContext, key, value)); + + //storage value is constant + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + + var storageKey = new StorageKey + { + Id = state.Id, + Key = new byte[] { 0x01 } + }; + var storageItem = new StorageItem + { + Value = new byte[] { 0x01, 0x02, 0x03, 0x04 } + }; + snapshotCache.AddContract(state.Hash, state); + snapshotCache.Add(storageKey, storageItem); + engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache); + engine.LoadScript(new byte[] { 0x01 }); + key = [0x01]; + value = [0x02]; + storageContext.IsReadOnly = false; + engine.Put(storageContext, key, value); + + //value length == 0 + key = [0x01]; + value = []; + engine.Put(storageContext, key, value); + } + + [TestMethod] + public void TestStorage_Delete() + { + var engine = GetEngine(false, true); + var state = TestUtils.GetContract(); + var storageKey = new StorageKey + { + Id = 0x42000000, + Key = new byte[] { 0x01 } + }; + var storageItem = new StorageItem + { + Value = new byte[] { 0x01, 0x02, 0x03, 0x04 } + }; + engine.SnapshotCache.AddContract(state.Hash, state); + engine.SnapshotCache.Add(storageKey, storageItem); + engine = ApplicationEngine.Create(TriggerType.Application, null, engine.SnapshotCache); + engine.LoadScript(new byte[] { 0x01 }); + var key = new byte[] { 0x01 }; + var storageContext = new StorageContext + { + Id = state.Id, + IsReadOnly = false + }; + engine.Delete(storageContext, key); + + //context is readonly + storageContext.IsReadOnly = true; + Assert.ThrowsExactly(() => engine.Delete(storageContext, key)); + } + + [TestMethod] + public void TestStorageContext_AsReadOnly() + { + var state = TestUtils.GetContract(); + var storageContext = new StorageContext + { + Id = state.Id, + IsReadOnly = false + }; + Assert.IsTrue(ApplicationEngine.AsReadOnly(storageContext).IsReadOnly); + } + + [TestMethod] + public void TestContract_Call() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var method = "method"; + var args = new Neo.VM.Types.Array { 0, 1 }; + var state = TestUtils.GetContract(method, args.Count); + + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshotCache, null, ProtocolSettings.Default); + engine.LoadScript(new byte[] { 0x01 }); + engine.SnapshotCache.AddContract(state.Hash, state); + + engine.CallContract(state.Hash, method, CallFlags.All, args); + Assert.AreEqual(args[0], engine.CurrentContext!.EvaluationStack.Pop()); + Assert.AreEqual(args[1], engine.CurrentContext.EvaluationStack.Pop()); + + state.Manifest.Permissions[0].Methods = WildcardContainer.Create("a"); + engine.SnapshotCache.DeleteContract(state.Hash); + engine.SnapshotCache.AddContract(state.Hash, state); + Assert.ThrowsExactly(() => engine.CallContract(state.Hash, method, CallFlags.All, args)); + + state.Manifest.Permissions[0].Methods = WildcardContainer.CreateWildcard(); + engine.SnapshotCache.DeleteContract(state.Hash); + engine.SnapshotCache.AddContract(state.Hash, state); + engine.CallContract(state.Hash, method, CallFlags.All, args); + + engine.SnapshotCache.DeleteContract(state.Hash); + engine.SnapshotCache.AddContract(state.Hash, state); + Assert.ThrowsExactly(() => engine.CallContract(UInt160.Zero, method, CallFlags.All, args)); + } + + [TestMethod] + public void TestContract_Destroy() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var state = TestUtils.GetContract(); + var scriptHash = UInt160.Parse("0xcb9f3b7c6fb1cf2c13a40637c189bdd066a272b4"); + var storageItem = new StorageItem + { + Value = new byte[] { 0x01, 0x02, 0x03, 0x04 } + }; + + var storageKey = new StorageKey + { + Id = 0x43000000, + Key = new byte[] { 0x01 } + }; + snapshotCache.AddContract(scriptHash, state); + snapshotCache.Add(storageKey, storageItem); + snapshotCache.DestroyContract(scriptHash); + Assert.IsFalse(snapshotCache.Find(BitConverter.GetBytes(0x43000000)).Any()); + + //storages are removed + state = TestUtils.GetContract(); + snapshotCache.AddContract(scriptHash, state); + snapshotCache.DestroyContract(scriptHash); + Assert.IsFalse(snapshotCache.Find(BitConverter.GetBytes(0x43000000)).Any()); + } + + [TestMethod] + public void TestContract_CreateStandardAccount() + { + var pubkey = ECPoint.Parse("024b817ef37f2fc3d4a33fe36687e592d9f30fe24b3e28187dc8f12b3b3b2b839e", ECCurve.Secp256r1); + Assert.AreEqual("c44ea575c5f79638f0e73f39d7bd4b3337c81691", GetEngine().CreateStandardAccount(pubkey).ToArray().ToHexString()); + } + + public static void LogEvent(object sender, LogEventArgs args) + { + var tx = (Transaction)args.ScriptContainer!; + tx.Script = new byte[] { 0x01, 0x02, 0x03 }; + } + + private ApplicationEngine GetEngine(bool hasContainer = false, bool hasBlock = false, bool addScript = true, long gas = 20_00000000) + { + var snapshot = _snapshotCache.CloneCache(); + var tx = hasContainer ? TestUtils.GetTransaction(UInt160.Zero) : null; + var block = hasBlock ? new Block + { + Header = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)), + Transactions = [] + } : null; + var engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshot, block, TestProtocolSettings.Default, gas: gas); + if (addScript) engine.LoadScript(new byte[] { 0x01 }); + return engine; + } + + [TestMethod] + public void TestVerifyWithECDsa() + { + var privateKey = new byte[32]; + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(privateKey); + var publicKeyR1 = new KeyPair(privateKey).PublicKey.ToArray(); + var publicKeyK1 = (ECCurve.Secp256k1.G * privateKey).ToArray(); + var hexMessage = "Hello, world!"u8.ToArray(); + var signatureR1 = Crypto.Sign(hexMessage, privateKey, ECCurve.Secp256r1); + var signatureK1 = Crypto.Sign(hexMessage, privateKey, ECCurve.Secp256k1); + + var result = CryptoLib.VerifyWithECDsa(hexMessage, publicKeyR1, signatureR1, NamedCurveHash.secp256r1SHA256); + Assert.IsTrue(result); + result = CryptoLib.VerifyWithECDsa(hexMessage, publicKeyK1, signatureK1, NamedCurveHash.secp256k1SHA256); + Assert.IsTrue(result); + Assert.ThrowsExactly(() => CryptoLib.VerifyWithECDsa(hexMessage, publicKeyK1, [], NamedCurveHash.secp256k1SHA256)); + } + + [TestMethod] + public void TestSha256() + { + var input = "Hello, world!"u8.ToArray(); + var actualHash = CryptoLib.Sha256(input); + var expectedHash = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"; + Assert.AreEqual(expectedHash, actualHash.ToHexString()); + } + + [TestMethod] + public void TestRIPEMD160() + { + var input = "Hello, world!"u8.ToArray(); + var actualHash = CryptoLib.RIPEMD160(input); + var expectedHash = "58262d1fbdbe4530d8865d3518c6d6e41002610f"; + Assert.AreEqual(expectedHash, actualHash.ToHexString()); + } + + [TestMethod] + public void TestMurmur32() + { + var input = "Hello, world!"u8.ToArray(); + var actualHash = CryptoLib.Murmur32(input, 0); + var expectedHash = "433e36c0"; + Assert.AreEqual(expectedHash, actualHash.ToHexString()); + } + + [TestMethod] + public void TestGetBlockHash() + { + var snapshotCache = GetEngine(true, true).SnapshotCache; + var hash = NativeContract.Ledger.GetBlockHash(snapshotCache, 0)!; + var hash2 = NativeContract.Ledger.GetBlock(snapshotCache, 0)!.Hash; + var hash3 = NativeContract.Ledger.GetHeader(snapshotCache, 0)!.Hash; + Assert.AreEqual(hash.ToString(), hash2.ToString()); + Assert.AreEqual(hash.ToString(), hash3.ToString()); + Assert.AreEqual("0x1f4d1defa46faa5e7b9b8d3f79a06bec777d7c26c4aa5f6f5899a291daa87c15", hash.ToString()); + Assert.IsTrue(NativeContract.Ledger.ContainsBlock(snapshotCache, hash)); + } + + [TestMethod] + public void TestGetCandidateVote() + { + var snapshotCache = GetEngine(true, true).SnapshotCache; + var vote = NativeContract.NEO.GetCandidateVote(snapshotCache, new ECPoint()); + Assert.AreEqual(-1, vote); + } + + [TestMethod] + public void TestContractPermissionDescriptorEquals() + { + var descriptor1 = ContractPermissionDescriptor.CreateWildcard(); + Assert.IsFalse(descriptor1.Equals(null)); + Assert.IsFalse(descriptor1.Equals(null as object)); + var descriptor2 = ContractPermissionDescriptor.Create(NativeContract.NEO.Hash); + var descriptor3 = ContractPermissionDescriptor.Create(hash: null!); + Assert.IsTrue(descriptor1.Equals(descriptor3)); + Assert.IsTrue(descriptor1.Equals(descriptor3 as object)); + var descriptor4 = ContractPermissionDescriptor.Create(group: null!); + var descriptor5 = ContractPermissionDescriptor.Create(group: new ECPoint()); + Assert.IsTrue(descriptor1.Equals(descriptor4)); + Assert.IsTrue(descriptor1.Equals(descriptor4 as object)); + Assert.IsFalse(descriptor2.Equals(descriptor3)); + Assert.IsFalse(descriptor5.Equals(descriptor3)); + Assert.IsTrue(descriptor5.Equals(descriptor5)); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_JsonSerializer.cs b/tests/Neo.UnitTests/SmartContract/UT_JsonSerializer.cs new file mode 100644 index 0000000000..6220fe0e1f --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_JsonSerializer.cs @@ -0,0 +1,289 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_JsonSerializer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Persistence; +using Neo.SmartContract; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; +using Array = Neo.VM.Types.Array; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_JsonSerializer +{ + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + } + + [TestMethod] + public void JsonTest_WrongJson() + { + var json = "[ ]XXXXXXX"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "{ }XXXXXXX"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[,,,,]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "false,X"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "false@@@"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + // repeat "9" 974 times + var longNumber = string.Concat(Enumerable.Repeat("9", 974)); + json = $"{{\"length\":{longNumber}}}"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + } + + [TestMethod] + public void JsonTest_Array() + { + var json = "[ ]"; + var parsed = JToken.Parse(json)!; + + Assert.AreEqual("[]", parsed.ToString()); + + json = "[1,\"a==\", -1.3 ,null] "; + parsed = JToken.Parse(json)!; + + Assert.AreEqual("[1,\"a==\",-1.3,null]", parsed.ToString()); + } + + [TestMethod] + public void JsonTest_Bool() + { + var json = "[ true ,false ]"; + var parsed = JToken.Parse(json)!; + + Assert.AreEqual("[true,false]", parsed.ToString()); + + json = "[True,FALSE] "; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + } + + [TestMethod] + public void JsonTest_Numbers() + { + var json = "[ 1, -2 , 3.5 ]"; + var parsed = JToken.Parse(json)!; + + Assert.AreEqual("[1,-2,3.5]", parsed.ToString()); + + json = "[200.500000E+005,200.500000e+5,-1.1234e-100,9.05E+28]"; + parsed = JToken.Parse(json)!; + + Assert.AreEqual("[20050000,20050000,-1.1234E-100,9.05E+28]", parsed.ToString()); + + json = "[-]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[1.]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[.123]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[--1.123]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[+1.123]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[1.12.3]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[e--1]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[e++1]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[E- 1]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[3e--1]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[2e++1]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = "[1E- 1]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + } + + [TestMethod] + public void JsonTest_String() + { + var json = @" ["""" , ""\b\f\t\n\r\/\\"" ]"; + var parsed = JToken.Parse(json)!; + + Assert.AreEqual(@"["""",""\b\f\t\n\r/\\""]", parsed.ToString()); + + json = @"[""\uD834\uDD1E""]"; + parsed = JToken.Parse(json)!; + + Assert.AreEqual(json, parsed.ToString()); + + json = @"[""\\x00""]"; + parsed = JToken.Parse(json)!; + + Assert.AreEqual(json, parsed.ToString()); + + json = @"[""]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = @"[""\uaaa""]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = @"[""\uaa""]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = @"[""\ua""]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = @"[""\u""]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + } + + [TestMethod] + public void JsonTest_Object() + { + var json = @" {""test"": true}"; + var parsed = JToken.Parse(json)!; + + Assert.AreEqual(@"{""test"":true}", parsed.ToString()); + + json = @" {""\uAAAA"": true}"; + parsed = JToken.Parse(json)!; + + Assert.AreEqual(@"{""\uAAAA"":true}", parsed.ToString()); + + json = @"{""a"":}"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = @"{NULL}"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + + json = @"[""a"":]"; + Assert.ThrowsExactly(() => _ = JToken.Parse(json)); + } + + [TestMethod] + public void Deserialize_WrongJson() + { + var snapshot = _snapshotCache.CloneCache(); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + Assert.ThrowsExactly(() => _ = JsonSerializer.Deserialize(engine, JToken.Parse("x")!, ExecutionEngineLimits.Default)); + } + + [TestMethod] + public void Deserialize_EmptyObject() + { + var snapshot = _snapshotCache.CloneCache(); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + var items = JsonSerializer.Deserialize(engine, JToken.Parse("{}")!, ExecutionEngineLimits.Default); + + Assert.IsInstanceOfType(items); + Assert.IsEmpty((Map)items); + } + + [TestMethod] + public void Deserialize_EmptyArray() + { + var snapshot = _snapshotCache.CloneCache(); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + var items = JsonSerializer.Deserialize(engine, JToken.Parse("[]")!, ExecutionEngineLimits.Default); + + Assert.IsInstanceOfType(items); + Assert.IsEmpty((Array)items); + } + + [TestMethod] + public void Deserialize_Map_Test() + { + var snapshot = _snapshotCache.CloneCache(); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, null, ProtocolSettings.Default); + var items = JsonSerializer.Deserialize(engine, JToken.Parse("{\"test1\":123,\"test2\":321}")!, ExecutionEngineLimits.Default); + + Assert.IsInstanceOfType(items); + Assert.HasCount(2, (Map)items); + + var map = (Map)items; + + Assert.IsTrue(map.TryGetValue("test1", out var value)); + Assert.AreEqual(123, value.GetInteger()); + + Assert.IsTrue(map.TryGetValue("test2", out value)); + Assert.AreEqual(321, value.GetInteger()); + + CollectionAssert.AreEqual(map.Values.Select(u => u.GetInteger()).ToArray(), new BigInteger[] { 123, 321 }); + } + + [TestMethod] + public void Deserialize_Array_Bool_Str_Num() + { + var snapshot = _snapshotCache.CloneCache(); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, null, ProtocolSettings.Default); + var items = JsonSerializer.Deserialize(engine, JToken.Parse("[true,\"test\",123,9.05E+28]")!, ExecutionEngineLimits.Default); + + Assert.IsInstanceOfType(items); + Assert.HasCount(4, (Array)items); + + var array = (Array)items; + + Assert.IsTrue(array[0].GetBoolean()); + Assert.AreEqual("test", array[1].GetString()); + Assert.AreEqual(123, array[2].GetInteger()); + Assert.AreEqual(array[3].GetInteger(), BigInteger.Parse("90500000000000000000000000000")); + } + + [TestMethod] + public void Deserialize_Array_OfArray() + { + var snapshot = _snapshotCache.CloneCache(); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, null, ProtocolSettings.Default); + var items = JsonSerializer.Deserialize(engine, JToken.Parse("[[true,\"test1\",123],[true,\"test2\",321]]")!, ExecutionEngineLimits.Default); + + Assert.IsInstanceOfType(items); + Assert.HasCount(2, (Array)items); + + var array = (Array)items; + + Assert.IsInstanceOfType(array[0]); + Assert.HasCount(3, (Array)array[0]); + + array = (Array)array[0]; + Assert.HasCount(3, array); + + Assert.IsTrue(array[0].GetBoolean()); + Assert.AreEqual("test1", array[1].GetString()); + Assert.AreEqual(123, array[2].GetInteger()); + + array = (Array)items; + array = (Array)array[1]; + Assert.HasCount(3, array); + + Assert.IsTrue(array[0].GetBoolean()); + Assert.AreEqual("test2", array[1].GetString()); + Assert.AreEqual(321, array[2].GetInteger()); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_KeyBuilder.cs b/tests/Neo.UnitTests/SmartContract/UT_KeyBuilder.cs new file mode 100644 index 0000000000..caccc45fb3 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_KeyBuilder.cs @@ -0,0 +1,110 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_KeyBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.UnitTests.SmartContract; + +[TestClass] +public class UT_KeyBuilder +{ + [TestMethod] + public void Test() + { + var key = new KeyBuilder(1, 2); + + Assert.AreEqual("0100000002", key.ToArray().ToHexString()); + + key = new KeyBuilder(1, 2); + key = key.Add([3, 4]); + Assert.AreEqual("01000000020304", key.ToArray().ToHexString()); + + key = new KeyBuilder(1, 2); + key = key.Add([3, 4]); + key = key.Add(UInt160.Zero); + Assert.AreEqual("010000000203040000000000000000000000000000000000000000", key.ToArray().ToHexString()); + + key = new KeyBuilder(1, 2); + key = key.Add(123); + Assert.AreEqual("01000000020000007b", key.ToArray().ToHexString()); + + key = new KeyBuilder(1, 0); + key = key.Add(1); + Assert.AreEqual("010000000000000001", key.ToArray().ToHexString()); + } + + [TestMethod] + public void TestAddInt() + { + var key = new KeyBuilder(1, 2); + Assert.AreEqual("0100000002", key.ToArray().ToHexString()); + + // add int + key = new KeyBuilder(1, 2); + key = key.Add(-1); + key = key.Add(2); + key = key.Add(3); + Assert.AreEqual("0100000002ffffffff0000000200000003", key.ToArray().ToHexString()); + + // add ulong + key = new KeyBuilder(1, 2); + key = key.Add(1ul); + key = key.Add(2ul); + key = key.Add(ulong.MaxValue); + Assert.AreEqual("010000000200000000000000010000000000000002ffffffffffffffff", key.ToArray().ToHexString()); + + // add uint + key = new KeyBuilder(1, 2); + key = key.Add(1u); + key = key.Add(2u); + key = key.Add(uint.MaxValue); + Assert.AreEqual("01000000020000000100000002ffffffff", key.ToArray().ToHexString()); + + // add byte + key = new KeyBuilder(1, 2); + key = key.Add((byte)1); + key = key.Add((byte)2); + key = key.Add((byte)3); + Assert.AreEqual("0100000002010203", key.ToArray().ToHexString()); + } + + [TestMethod] + public void TestAddUInt() + { + var key = new KeyBuilder(1, 2); + var value = new byte[UInt160.Length]; + for (int i = 0; i < value.Length; i++) + value[i] = (byte)i; + + key = key.Add(new UInt160(value)); + Assert.AreEqual("0100000002000102030405060708090a0b0c0d0e0f10111213", key.ToArray().ToHexString()); + + var key2 = new KeyBuilder(1, 2); + key2 = key2.Add((ISerializableSpan)new UInt160(value)); + + // It must be same before and after optimization. + Assert.AreEqual(key.ToArray().ToHexString(), key2.ToArray().ToHexString()); + + key = new KeyBuilder(1, 2); + value = new byte[UInt256.Length]; + for (int i = 0; i < value.Length; i++) + value[i] = (byte)i; + key = key.Add(new UInt256(value)); + Assert.AreEqual("0100000002000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", key.ToArray().ToHexString()); + + key2 = new KeyBuilder(1, 2); + key2 = key2.Add((ISerializableSpan)new UInt256(value)); + + // It must be same before and after optimization. + Assert.AreEqual(key.ToArray().ToHexString(), key2.ToArray().ToHexString()); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_LogEventArgs.cs b/tests/Neo.UnitTests/SmartContract/UT_LogEventArgs.cs new file mode 100644 index 0000000000..9e70283016 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_LogEventArgs.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_LogEventArgs.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 System.Runtime.CompilerServices; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_LogEventArgs +{ + [TestMethod] + public void TestGeneratorAndGet() + { + IVerifiable container = (Header)RuntimeHelpers.GetUninitializedObject(typeof(Header)); + UInt160 scripthash = UInt160.Zero; + string message = "lalala"; + LogEventArgs logEventArgs = new(container, scripthash, message); + Assert.IsNotNull(logEventArgs); + Assert.AreEqual(container, logEventArgs.ScriptContainer); + Assert.AreEqual(scripthash, logEventArgs.ScriptHash); + Assert.AreEqual(message, logEventArgs.Message); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_MethodToken.cs b/tests/Neo.UnitTests/SmartContract/UT_MethodToken.cs new file mode 100644 index 0000000000..c9cf371111 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_MethodToken.cs @@ -0,0 +1,60 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_MethodToken.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.SmartContract; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_MethodToken +{ + [TestMethod] + public void TestSerialize() + { + var result = new MethodToken() + { + CallFlags = CallFlags.AllowCall, + Hash = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01"), + Method = "myMethod", + ParametersCount = 123, + HasReturnValue = true + }; + + var copy = result.ToArray().AsSerializable(); + + Assert.AreEqual(CallFlags.AllowCall, copy.CallFlags); + Assert.AreEqual("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01", copy.Hash.ToString()); + Assert.AreEqual("myMethod", copy.Method); + Assert.AreEqual(123, copy.ParametersCount); + Assert.IsTrue(copy.HasReturnValue); + } + + [TestMethod] + public void TestSerializeErrors() + { + var result = new MethodToken() + { + CallFlags = (CallFlags)byte.MaxValue, + Hash = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01"), + Method = "myLongMethod", + ParametersCount = 123, + HasReturnValue = true + }; + + Assert.ThrowsExactly(() => _ = result.ToArray().AsSerializable()); + + result.CallFlags = CallFlags.All; + result.Method += "-123123123123123123123123"; + Assert.ThrowsExactly(() => _ = result.ToArray().AsSerializable()); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_NefFile.cs b/tests/Neo.UnitTests/SmartContract/UT_NefFile.cs new file mode 100644 index 0000000000..c05179c339 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_NefFile.cs @@ -0,0 +1,150 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NefFile.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.IO; +using Neo.SmartContract; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_NefFile +{ + public NefFile file = new() + { + Compiler = "".PadLeft(32, ' '), + Source = string.Empty, + Tokens = Array.Empty(), + Script = new byte[] { 0x01, 0x02, 0x03 } + }; + + [TestInitialize] + public void TestSetup() + { + file.CheckSum = NefFile.ComputeChecksum(file); + } + + [TestMethod] + public void TestDeserialize() + { + byte[] wrongMagic = { 0x00, 0x00, 0x00, 0x00 }; + using (MemoryStream ms = new(1024)) + using (BinaryWriter writer = new(ms)) + { + ((ISerializable)file).Serialize(writer); + ms.Seek(0, SeekOrigin.Begin); + ms.Write(wrongMagic, 0, 4); + ISerializable newFile = (NefFile)RuntimeHelpers.GetUninitializedObject(typeof(NefFile)); + Assert.ThrowsExactly(() => MemoryReaderDeserialize(ms.ToArray(), newFile)); + } + + file.CheckSum = 0; + using (MemoryStream ms = new(1024)) + using (BinaryWriter writer = new(ms)) + { + ((ISerializable)file).Serialize(writer); + ISerializable newFile = (NefFile)RuntimeHelpers.GetUninitializedObject(typeof(NefFile)); + Assert.ThrowsExactly(() => MemoryReaderDeserialize(ms.ToArray(), newFile)); + } + + file.Script = Array.Empty(); + file.CheckSum = NefFile.ComputeChecksum(file); + using (MemoryStream ms = new(1024)) + using (BinaryWriter writer = new(ms)) + { + ((ISerializable)file).Serialize(writer); + ISerializable newFile = (NefFile)RuntimeHelpers.GetUninitializedObject(typeof(NefFile)); + Assert.ThrowsExactly(() => MemoryReaderDeserialize(ms.ToArray(), newFile)); + } + + file.Script = new byte[] { 0x01, 0x02, 0x03 }; + file.CheckSum = NefFile.ComputeChecksum(file); + var data = file.ToArray(); + var newFile1 = data.AsSerializable(); + Assert.AreEqual(file.Compiler, newFile1.Compiler); + Assert.AreEqual(file.CheckSum, newFile1.CheckSum); + Assert.IsTrue(newFile1.Script.Span.SequenceEqual(file.Script.Span)); + + static void MemoryReaderDeserialize(byte[] buffer, ISerializable obj) + { + var reader = new MemoryReader(buffer); + obj.Deserialize(ref reader); + } + } + + [TestMethod] + public void TestGetSize() + { + Assert.AreEqual(4 + 32 + 32 + 2 + 1 + 2 + 4 + 4, file.Size); + } + + [TestMethod] + public void ParseTest() + { + var file = new NefFile() + { + Compiler = "".PadLeft(32, ' '), + Source = string.Empty, + Tokens = Array.Empty(), + Script = new byte[] { 0x01, 0x02, 0x03 } + }; + + file.CheckSum = NefFile.ComputeChecksum(file); + + var data = file.ToArray(); + file = data.AsSerializable(); + + Assert.AreEqual("".PadLeft(32, ' '), file.Compiler); + CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03 }, file.Script.ToArray()); + } + + [TestMethod] + public void LimitTest() + { + var file = new NefFile() + { + Compiler = "".PadLeft(byte.MaxValue, ' '), + Source = string.Empty, + Tokens = Array.Empty(), + Script = new byte[1024 * 1024], + CheckSum = 0 + }; + + // Wrong compiler + + Assert.ThrowsExactly(() => _ = file.ToArray()); + + // Wrong script + + file.Compiler = ""; + file.Script = new byte[(1024 * 1024) + 1]; + var data = file.ToArray(); + + Assert.ThrowsExactly(() => _ = data.AsSerializable()); + + // Wrong script hash + + file.Script = new byte[1024 * 1024]; + data = file.ToArray(); + + Assert.ThrowsExactly(() => _ = data.AsSerializable()); + + // Wrong checksum + + file.Script = new byte[1024]; + data = file.ToArray(); + file.CheckSum = NefFile.ComputeChecksum(file) + 1; + + Assert.ThrowsExactly(() => _ = data.AsSerializable()); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_NotifyEventArgs.cs b/tests/Neo.UnitTests/SmartContract/UT_NotifyEventArgs.cs new file mode 100644 index 0000000000..c1bd080551 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_NotifyEventArgs.cs @@ -0,0 +1,70 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NotifyEventArgs.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.VM; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_NotifyEventArgs +{ + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + } + + [TestMethod] + public void TestGetScriptContainer() + { + IVerifiable container = new TestVerifiable(); + UInt160 script_hash = new byte[] { 0x00 }.ToScriptHash(); + var args = new NotifyEventArgs(container, script_hash, "Test", null!); + Assert.AreEqual(container, args.ScriptContainer); + } + + [TestMethod] + public void TestIssue3300() // https://github.com/neo-project/neo/issues/3300 + { + var snapshot = _snapshotCache.CloneCache(); + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default, gas: 1100_00000000); + using (var script = new ScriptBuilder()) + { + // Build call script calling disallowed method. + script.Emit(OpCode.NOP); + // Mock executing state to be a contract-based. + engine.LoadScript(script.ToArray()); + } + + var ns = new Neo.VM.Types.Array(engine.ReferenceCounter); + for (var i = 0; i < 500; i++) + { + ns.Add(""); + } + + var hash = UInt160.Parse("0x179ab5d297fd34ecd48643894242fc3527f42853"); + engine.SendNotification(hash, "Test", ns); + // This should have being 0, but we have optimized the vm to not clean the reference counter + // unless it is necessary, so the reference counter will be 1000. + // Same reason why its 1504 instead of 504. + Assert.AreEqual(1000, engine.ReferenceCounter.Count); + // This will make a deepcopy for the notification, along with the 500 state items. + engine.GetNotifications(hash); + // With the fix of issue 3300, the reference counter calculates not only + // the notifaction items, but also the subitems of the notification state. + Assert.AreEqual(1504, engine.ReferenceCounter.Count); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_OpCodePrices.cs b/tests/Neo.UnitTests/SmartContract/UT_OpCodePrices.cs new file mode 100644 index 0000000000..bf2bb05b20 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_OpCodePrices.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_OpCodePrices.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.UnitTests.SmartContract; + +[TestClass] +public class UT_OpCodePrices +{ + [TestMethod] + public void AllOpcodePriceAreSet() + { + foreach (OpCode opcode in Enum.GetValues()) + { + if (opcode == OpCode.RET || + opcode == OpCode.SYSCALL || + opcode == OpCode.ABORT || + opcode == OpCode.ABORTMSG) + continue; + Assert.AreNotEqual(0, ApplicationEngine.OpCodePriceTable[(byte)opcode], $"{opcode} without price"); + } + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_SmartContractHelper.cs b/tests/Neo.UnitTests/SmartContract/UT_SmartContractHelper.cs new file mode 100644 index 0000000000..239bb80860 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_SmartContractHelper.cs @@ -0,0 +1,226 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_SmartContractHelper.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.SmartContract.Native; +using Neo.UnitTests.Extensions; +using Neo.Wallets; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using ECPoint = Neo.Cryptography.ECC.ECPoint; +using Helper = Neo.SmartContract.Helper; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_SmartContractHelper +{ + + [TestMethod] + public void TestIsMultiSigContract() + { + ECPoint[] publicKeys1 = new ECPoint[20]; + for (int i = 0; i < 20; i++) + { + byte[] privateKey1 = new byte[32]; + RandomNumberGenerator rng1 = RandomNumberGenerator.Create(); + rng1.GetBytes(privateKey1); + KeyPair key1 = new(privateKey1); + publicKeys1[i] = key1.PublicKey; + } + byte[] script1 = Contract.CreateMultiSigRedeemScript(20, publicKeys1); + Assert.IsTrue(Helper.IsMultiSigContract(script1, out _, out ECPoint[]? p1)); + CollectionAssert.AreEqual(publicKeys1.OrderBy(p => p).ToArray(), p1); + + ECPoint[] publicKeys2 = new ECPoint[256]; + for (int i = 0; i < 256; i++) + { + byte[] privateKey2 = new byte[32]; + RandomNumberGenerator rng2 = RandomNumberGenerator.Create(); + rng2.GetBytes(privateKey2); + KeyPair key2 = new(privateKey2); + publicKeys2[i] = key2.PublicKey; + } + byte[] script2 = Contract.CreateMultiSigRedeemScript(256, publicKeys2); + Assert.IsTrue(Helper.IsMultiSigContract(script2, out _, out ECPoint[]? p2)); + CollectionAssert.AreEqual(publicKeys2.OrderBy(p => p).ToArray(), p2); + + ECPoint[] publicKeys3 = new ECPoint[3]; + for (int i = 0; i < 3; i++) + { + byte[] privateKey3 = new byte[32]; + RandomNumberGenerator rng3 = RandomNumberGenerator.Create(); + rng3.GetBytes(privateKey3); + KeyPair key3 = new(privateKey3); + publicKeys3[i] = key3.PublicKey; + } + byte[] script3 = Contract.CreateMultiSigRedeemScript(3, publicKeys3); + Assert.IsTrue(Helper.IsMultiSigContract(script3, out _, out ECPoint[]? p3)); + CollectionAssert.AreEqual(publicKeys3.OrderBy(p => p).ToArray(), p3); + + ECPoint[] publicKeys4 = new ECPoint[3]; + for (int i = 0; i < 3; i++) + { + byte[] privateKey4 = new byte[32]; + RandomNumberGenerator rng4 = RandomNumberGenerator.Create(); + rng4.GetBytes(privateKey4); + KeyPair key4 = new(privateKey4); + publicKeys4[i] = key4.PublicKey; + } + byte[] script4 = Contract.CreateMultiSigRedeemScript(3, publicKeys4); + script4[^1] = 0x00; + Assert.IsFalse(Helper.IsMultiSigContract(script4, out _, out ECPoint[]? p4)); + Assert.IsNull(p4); + } + + [TestMethod] + public void TestIsSignatureContract() + { + byte[] privateKey = new byte[32]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetBytes(privateKey); + KeyPair key = new(privateKey); + byte[] script = Contract.CreateSignatureRedeemScript(key.PublicKey); + Assert.IsTrue(Helper.IsSignatureContract(script)); + script[0] = 0x22; + Assert.IsFalse(Helper.IsSignatureContract(script)); + } + + [TestMethod] + public void TestIsStandardContract() + { + byte[] privateKey1 = new byte[32]; + RandomNumberGenerator rng1 = RandomNumberGenerator.Create(); + rng1.GetBytes(privateKey1); + KeyPair key1 = new(privateKey1); + byte[] script1 = Contract.CreateSignatureRedeemScript(key1.PublicKey); + Assert.IsTrue(Helper.IsStandardContract(script1)); + + ECPoint[] publicKeys2 = new ECPoint[3]; + for (int i = 0; i < 3; i++) + { + byte[] privateKey2 = new byte[32]; + RandomNumberGenerator rng2 = RandomNumberGenerator.Create(); + rng2.GetBytes(privateKey2); + KeyPair key2 = new(privateKey2); + publicKeys2[i] = key2.PublicKey; + } + byte[] script2 = Contract.CreateMultiSigRedeemScript(3, publicKeys2); + Assert.IsTrue(Helper.IsStandardContract(script2)); + } + + [TestMethod] + public void TestVerifyWitnesses() + { + var snapshotCache1 = TestBlockchain.GetTestSnapshotCache().CloneCache(); + var index1 = UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff01"); + TestUtils.BlocksAdd(snapshotCache1, index1, new TrimmedBlock() + { + Header = new Header + { + Timestamp = 1, + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + Witness = Witness.Empty + }, + Hashes = [UInt256.Zero], + }); + TestUtils.BlocksDelete(snapshotCache1, index1); + Assert.IsFalse(Helper.VerifyWitnesses(new Header() + { + PrevHash = index1, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + Witness = null! + }, TestProtocolSettings.Default, snapshotCache1, 100)); + + var snapshotCache2 = TestBlockchain.GetTestSnapshotCache(); + var index2 = UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff01"); + TrimmedBlock block2 = new() + { + Header = new Header + { + Timestamp = 2, + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + Witness = Witness.Empty + }, + Hashes = [UInt256.Zero], + }; + TestUtils.BlocksAdd(snapshotCache2, index2, block2); + Header header2 = new() + { + PrevHash = index2, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + Witness = Witness.Empty + }; + + snapshotCache2.AddContract(UInt160.Zero, (ContractState)RuntimeHelpers.GetUninitializedObject(typeof(ContractState))); + snapshotCache2.DeleteContract(UInt160.Zero); + Assert.IsFalse(Helper.VerifyWitnesses(header2, TestProtocolSettings.Default, snapshotCache2, 100)); + + var snapshotCache3 = TestBlockchain.GetTestSnapshotCache(); + var index3 = UInt256.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff01"); + TrimmedBlock block3 = new() + { + Header = new Header + { + Timestamp = 3, + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + Witness = Witness.Empty + }, + Hashes = [UInt256.Zero], + }; + TestUtils.BlocksAdd(snapshotCache3, index3, block3); + Header header3 = new() + { + PrevHash = index3, + MerkleRoot = UInt256.Zero, + NextConsensus = UInt160.Zero, + Witness = Witness.Empty + }; + snapshotCache3.AddContract(UInt160.Zero, new ContractState() + { + Nef = (NefFile)RuntimeHelpers.GetUninitializedObject(typeof(NefFile)), + Hash = Array.Empty().ToScriptHash(), + Manifest = TestUtils.CreateManifest("verify", ContractParameterType.Boolean, ContractParameterType.Signature), + }); + Assert.IsFalse(Helper.VerifyWitnesses(header3, TestProtocolSettings.Default, snapshotCache3, 100)); + + // Smart contract verification + + var contract = new ContractState() + { + Nef = new NefFile + { + Compiler = "", + Source = "", + Tokens = [], + Script = "11".HexToBytes() + }, // 17 PUSH1 + Hash = "11".HexToBytes().ToScriptHash(), + Manifest = TestUtils.CreateManifest("verify", ContractParameterType.Boolean, ContractParameterType.Signature), // Offset = 0 + }; + snapshotCache3.AddContract(contract.Hash, contract); + var tx = new Nep17NativeContractExtensions.ManualWitness(contract.Hash) + { + Witnesses = [Witness.Empty] + }; + + Assert.IsTrue(Helper.VerifyWitnesses(tx, TestProtocolSettings.Default, snapshotCache3, 1000)); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_Storage.cs b/tests/Neo.UnitTests/SmartContract/UT_Storage.cs new file mode 100644 index 0000000000..22c6b77eab --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_Storage.cs @@ -0,0 +1,100 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Storage.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 System.Numerics; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public class UT_Storage +{ + [TestMethod] + public void TestImplicit() + { + // Test data + byte[] keyData = [0x00, 0x00, 0x00, 0x00, 0x12]; + StorageKey keyA = keyData; + StorageKey keyB = new ReadOnlyMemory(keyData); + StorageKey keyC = new ReadOnlySpan(keyData); + + Assert.AreEqual(0, keyA.Id); + Assert.AreEqual(keyA.Id, keyB.Id); + Assert.AreEqual(keyB.Id, keyC.Id); + + CollectionAssert.AreEqual(new byte[] { 0x12 }, keyA.Key.Span.ToArray()); + CollectionAssert.AreEqual(keyA.Key.Span.ToArray(), keyB.Key.Span.ToArray()); + CollectionAssert.AreEqual(keyB.Key.Span.ToArray(), keyC.Key.Span.ToArray()); + } + + [TestMethod] + public void TestStorageKey() + { + // Test data + byte[] keyData = [0x00, 0x00, 0x00, 0x00, 0x12]; + var keyMemory = new ReadOnlyMemory(keyData); + + // Test implicit conversion from byte[] to StorageKey + StorageKey storageKeyFromArray = keyData; + Assert.AreEqual(0, storageKeyFromArray.Id); + Assert.IsTrue(keyMemory.Span.ToArray().Skip(sizeof(int)).SequenceEqual(storageKeyFromArray.Key.Span.ToArray())); + + // Test implicit conversion from ReadOnlyMemory to StorageKey + StorageKey storageKeyFromMemory = keyMemory; + Assert.AreEqual(0, storageKeyFromMemory.Id); + Assert.IsTrue(keyMemory.Span.ToArray().Skip(sizeof(int)).SequenceEqual(storageKeyFromMemory.Key.Span.ToArray())); + + // Test CreateSearchPrefix method + byte[] prefix = { 0xAA }; + var searchPrefix = StorageKey.CreateSearchPrefix(0, prefix); + var expectedPrefix = BitConverter.GetBytes(0).Concat(prefix).ToArray(); + Assert.IsTrue(expectedPrefix.SequenceEqual(searchPrefix)); + + // Test Equals method + var storageKey1 = new StorageKey { Id = 0, Key = keyMemory }; + var storageKey2 = new StorageKey { Id = 0, Key = keyMemory }; + var storageKeyDifferentId = new StorageKey { Id = 0 + 1, Key = keyMemory }; + var storageKeyDifferentKey = new StorageKey { Id = 0, Key = new ReadOnlyMemory([0x04]) }; + Assert.AreEqual(storageKey1, storageKey2); + Assert.AreNotEqual(storageKey1, storageKeyDifferentId); + Assert.AreNotEqual(storageKey1, storageKeyDifferentKey); + + // Testing to see if we are using same pointers. + // Make sure we create copies of the memory in StorageKey class + // WE DO NOT WANT DATA REFERENCED TO OVER THE MEMORY REGION + byte[] dataCopy = [0xff, 0xff, 0xff, 0xfe, 0xff]; + storageKey2 = new StorageKey(dataCopy); + Assert.IsTrue(storageKey2.Key.Span.SequenceEqual([(byte)0xff])); + Assert.AreNotEqual(storageKey1, storageKey2); + ((byte[])[0x00, 0x00, 0x00, 0x00, 0x01]).CopyTo(dataCopy.AsMemory()); + Assert.IsTrue(storageKey2.Key.Span.SequenceEqual([(byte)0xff])); + + // This shows data isn't referenced + dataCopy.CopyTo(keyData.AsMemory()); + Assert.IsFalse(storageKey1.Key.Span.SequenceEqual(dataCopy)); + } + + [TestMethod] + public void TestStorageItem() + { + // Test data + byte[] keyData = [0x00, 0x00, 0x00, 0x00, 0x12]; + var bigInteger = new BigInteger(1234567890); + + // Test implicit conversion from byte[] to StorageItem + StorageItem storageItemFromArray = keyData; + Assert.IsTrue(keyData.SequenceEqual(storageItemFromArray.Value.Span.ToArray())); + + // Test implicit conversion from BigInteger to StorageItem + StorageItem storageItemFromBigInteger = bigInteger; + Assert.AreEqual(bigInteger, (BigInteger)storageItemFromBigInteger); + } +} diff --git a/tests/Neo.UnitTests/SmartContract/UT_Syscalls.cs b/tests/Neo.UnitTests/SmartContract/UT_Syscalls.cs new file mode 100644 index 0000000000..8d0125ab4e --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/UT_Syscalls.cs @@ -0,0 +1,274 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Syscalls.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.MsTest; +using Neo.Extensions.IO; +using Neo.Extensions.VM; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.UnitTests.Extensions; +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.UnitTests.SmartContract; + +[TestClass] +public partial class UT_Syscalls : TestKit +{ + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + } + + [TestMethod] + public void System_Blockchain_GetBlock() + { + var tx = new Transaction() + { + Script = new byte[] { 0x01 }, + Attributes = [], + Signers = [], + NetworkFee = 0x02, + SystemFee = 0x03, + Nonce = 0x04, + ValidUntilBlock = 0x05, + Version = 0x06, + Witnesses = [new() { VerificationScript = new byte[] { 0x07 } }], + }; + + var block = new TrimmedBlock() + { + Header = new Header + { + Index = 0, + Timestamp = 2, + Witness = Witness.Empty, + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + PrimaryIndex = 1, + NextConsensus = UInt160.Zero, + }, + Hashes = [tx.Hash] + }; + + var snapshot = _snapshotCache.CloneCache(); + + using ScriptBuilder script = new(); + script.EmitDynamicCall(NativeContract.Ledger.Hash, "getBlock", block.Hash.ToArray()); + + // Without block + + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsTrue(engine.ResultStack.Peek().IsNull); + + // Not traceable block + + const byte Prefix_Transaction = 11; + const byte Prefix_CurrentBlock = 12; + + TestUtils.BlocksAdd(snapshot, block.Hash, block); + + var height = snapshot[NativeContract.Ledger.CreateStorageKey(Prefix_CurrentBlock)].GetInteroperable(); + height.Index = block.Index + TestProtocolSettings.Default.MaxTraceableBlocks; + + snapshot.Add(NativeContract.Ledger.CreateStorageKey(Prefix_Transaction, tx.Hash), new StorageItem(new TransactionState + { + BlockIndex = block.Index, + Transaction = tx + })); + + engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsTrue(engine.ResultStack.Peek().IsNull); + + // With block + + height = snapshot[NativeContract.Ledger.CreateStorageKey(Prefix_CurrentBlock)].GetInteroperable(); + height.Index = block.Index; + + engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + + var array = engine.ResultStack.Pop(); + Assert.AreEqual(block.Hash, new UInt256(array[0].GetSpan())); + } + + [TestMethod] + public void System_ExecutionEngine_GetScriptContainer() + { + var snapshot = _snapshotCache.CloneCache(); + using ScriptBuilder script = new(); + script.EmitSysCall(ApplicationEngine.System_Runtime_GetScriptContainer); + + // Without tx + + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.FAULT, engine.Execute()); + Assert.IsEmpty(engine.ResultStack); + + // With tx + + var tx = new Transaction() + { + Script = new byte[] { 0x01 }, + Signers = + [ + new() + { + Account = UInt160.Zero, + Scopes = WitnessScope.None, + AllowedContracts = [], + AllowedGroups = [], + Rules = [], + } + ], + Attributes = [], + NetworkFee = 0x02, + SystemFee = 0x03, + Nonce = 0x04, + ValidUntilBlock = 0x05, + Version = 0x06, + Witnesses = [new() { VerificationScript = new byte[] { 0x07 } }], + }; + + engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshot); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + + var array = engine.ResultStack.Pop(); + Assert.AreEqual(tx.Hash, new UInt256(array[0].GetSpan())); + } + + [TestMethod] + public void System_Runtime_GasLeft() + { + var snapshot = _snapshotCache.CloneCache(); + + using (var script = new ScriptBuilder()) + { + script.Emit(OpCode.NOP); + script.EmitSysCall(ApplicationEngine.System_Runtime_GasLeft); + script.Emit(OpCode.NOP); + script.EmitSysCall(ApplicationEngine.System_Runtime_GasLeft); + script.Emit(OpCode.NOP); + script.Emit(OpCode.NOP); + script.Emit(OpCode.NOP); + script.EmitSysCall(ApplicationEngine.System_Runtime_GasLeft); + + // Execute + + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, gas: 100_000_000); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + + // Check the results + + CollectionAssert.AreEqual( + engine.ResultStack.Select(u => (int)u.GetInteger()).ToArray(), + new int[] { 99_999_490, 99_998_980, 99_998_410 } + ); + } + + // Check test mode + + using (var script = new ScriptBuilder()) + { + script.EmitSysCall(ApplicationEngine.System_Runtime_GasLeft); + + // Execute + + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + engine.LoadScript(script.ToArray()); + + // Check the results + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.HasCount(1, engine.ResultStack); + Assert.IsInstanceOfType(engine.ResultStack.Peek()); + Assert.AreEqual(1999999520, engine.ResultStack.Pop().GetInteger()); + } + } + + [TestMethod] + public void System_Runtime_GetInvocationCounter() + { + var snapshot = _snapshotCache.CloneCache(); + ContractState contractA, contractB, contractC; + + // Create dummy contracts + + using (var script = new ScriptBuilder()) + { + script.EmitSysCall(ApplicationEngine.System_Runtime_GetInvocationCounter); + + contractA = TestUtils.GetContract(new byte[] { (byte)OpCode.DROP, (byte)OpCode.DROP }.Concat(script.ToArray()).ToArray()); + contractB = TestUtils.GetContract(new byte[] { (byte)OpCode.DROP, (byte)OpCode.DROP, (byte)OpCode.NOP }.Concat(script.ToArray()).ToArray()); + contractC = TestUtils.GetContract(new byte[] { (byte)OpCode.DROP, (byte)OpCode.DROP, (byte)OpCode.NOP, (byte)OpCode.NOP }.Concat(script.ToArray()).ToArray()); + contractA.Hash = contractA.Script.Span.ToScriptHash(); + contractB.Hash = contractB.Script.Span.ToScriptHash(); + contractC.Hash = contractC.Script.Span.ToScriptHash(); + + // Init A,B,C contracts + // First two drops is for drop method and arguments + + snapshot.DeleteContract(contractA.Hash); + snapshot.DeleteContract(contractB.Hash); + snapshot.DeleteContract(contractC.Hash); + contractA.Manifest = TestUtils.CreateManifest("dummyMain", ContractParameterType.Any, ContractParameterType.String, ContractParameterType.Integer); + contractB.Manifest = TestUtils.CreateManifest("dummyMain", ContractParameterType.Any, ContractParameterType.String, ContractParameterType.Integer); + contractC.Manifest = TestUtils.CreateManifest("dummyMain", ContractParameterType.Any, ContractParameterType.String, ContractParameterType.Integer); + snapshot.AddContract(contractA.Hash, contractA); + snapshot.AddContract(contractB.Hash, contractB); + snapshot.AddContract(contractC.Hash, contractC); + } + + // Call A,B,B,C + + using (var script = new ScriptBuilder()) + { + script.EmitDynamicCall(contractA.Hash, "dummyMain", "0", 1); + script.EmitDynamicCall(contractB.Hash, "dummyMain", "0", 1); + script.EmitDynamicCall(contractB.Hash, "dummyMain", "0", 1); + script.EmitDynamicCall(contractC.Hash, "dummyMain", "0", 1); + + // Execute + + var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, null, ProtocolSettings.Default); + engine.LoadScript(script.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + + // Check the results + + CollectionAssert.AreEqual( + engine.ResultStack.Select(u => (int)u.GetInteger()).ToArray(), + new int[] { 1 /* A */, 1 /* B */, 2 /* B */, 1 /* C */}); + } + } +} diff --git a/tests/Neo.UnitTests/TestBlockchain.cs b/tests/Neo.UnitTests/TestBlockchain.cs new file mode 100644 index 0000000000..3b44b85047 --- /dev/null +++ b/tests/Neo.UnitTests/TestBlockchain.cs @@ -0,0 +1,69 @@ +// Copyright (C) 2015-2025 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 Akka.Actor; +using Neo.Ledger; +using Neo.Persistence; +using Neo.Persistence.Providers; + +#nullable enable + +namespace Neo.UnitTests; + +public static class TestBlockchain +{ + private class TestStoreProvider : IStoreProvider + { + public readonly Dictionary Stores = []; + + public string Name => "TestProvider"; + + public IStore GetStore(string? path) + { + path ??= ""; + + lock (Stores) + { + if (Stores.TryGetValue(path, out var store)) + return store; + + return Stores[path] = new MemoryStore(); + } + } + } + + public class TestNeoSystem(ProtocolSettings settings) : NeoSystem(settings, new TestStoreProvider()) + { + public void ResetStore() + { + if (StorageProvider is TestStoreProvider testStore) + { + foreach (var store in testStore.Stores) + store.Value.Reset(); + } + Blockchain.Ask(new Blockchain.Initialize()).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public StoreCache GetTestSnapshotCache(bool reset = true) + { + if (reset) + ResetStore(); + return GetSnapshotCache(); + } + } + + public static readonly UInt160[]? DefaultExtensibleWitnessWhiteList; + + public static TestNeoSystem GetSystem() => new(TestProtocolSettings.Default); + public static StoreCache GetTestSnapshotCache() => GetSystem().GetSnapshotCache(); +} + +#nullable disable diff --git a/tests/Neo.UnitTests/TestProtocolSettings.cs b/tests/Neo.UnitTests/TestProtocolSettings.cs new file mode 100644 index 0000000000..3eec09447d --- /dev/null +++ b/tests/Neo.UnitTests/TestProtocolSettings.cs @@ -0,0 +1,76 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TestProtocolSettings.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.UnitTests; + +public static class TestProtocolSettings +{ + public static readonly ProtocolSettings Default = ProtocolSettings.Default with + { + Network = 0x334F454Eu, + StandbyCommittee = + [ + //Validators + ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1), + ECPoint.Parse("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", ECCurve.Secp256r1), + ECPoint.Parse("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", ECCurve.Secp256r1), + ECPoint.Parse("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", ECCurve.Secp256r1), + ECPoint.Parse("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", ECCurve.Secp256r1), + ECPoint.Parse("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", ECCurve.Secp256r1), + ECPoint.Parse("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", ECCurve.Secp256r1), + //Other Members + ECPoint.Parse("023a36c72844610b4d34d1968662424011bf783ca9d984efa19a20babf5582f3fe", ECCurve.Secp256r1), + ECPoint.Parse("03708b860c1de5d87f5b151a12c2a99feebd2e8b315ee8e7cf8aa19692a9e18379", ECCurve.Secp256r1), + ECPoint.Parse("03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050", ECCurve.Secp256r1), + ECPoint.Parse("03204223f8c86b8cd5c89ef12e4f0dbb314172e9241e30c9ef2293790793537cf0", ECCurve.Secp256r1), + ECPoint.Parse("02a62c915cf19c7f19a50ec217e79fac2439bbaad658493de0c7d8ffa92ab0aa62", ECCurve.Secp256r1), + ECPoint.Parse("03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0", ECCurve.Secp256r1), + ECPoint.Parse("0288342b141c30dc8ffcde0204929bb46aed5756b41ef4a56778d15ada8f0c6654", ECCurve.Secp256r1), + ECPoint.Parse("020f2887f41474cfeb11fd262e982051c1541418137c02a0f4961af911045de639", ECCurve.Secp256r1), + ECPoint.Parse("0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30", ECCurve.Secp256r1), + ECPoint.Parse("03d281b42002647f0113f36c7b8efb30db66078dfaaa9ab3ff76d043a98d512fde", ECCurve.Secp256r1), + ECPoint.Parse("02504acbc1f4b3bdad1d86d6e1a08603771db135a73e61c9d565ae06a1938cd2ad", ECCurve.Secp256r1), + ECPoint.Parse("0226933336f1b75baa42d42b71d9091508b638046d19abd67f4e119bf64a7cfb4d", ECCurve.Secp256r1), + ECPoint.Parse("03cdcea66032b82f5c30450e381e5295cae85c5e6943af716cc6b646352a6067dc", ECCurve.Secp256r1), + ECPoint.Parse("02cd5a5547119e24feaa7c2a0f37b8c9366216bab7054de0065c9be42084003c8a", ECCurve.Secp256r1) + ], + ValidatorsCount = 7, + SeedList = + [ + "seed1.neo.org:10333", + "seed2.neo.org:10333", + "seed3.neo.org:10333", + "seed4.neo.org:10333", + "seed5.neo.org:10333" + ], + }; + + public static readonly ProtocolSettings SoleNode = ProtocolSettings.Default with + { + Network = 0x334F454Eu, + StandbyCommittee = + [ + //Validators + ECPoint.Parse("0278ed78c917797b637a7ed6e7a9d94e8c408444c41ee4c0a0f310a256b9271eda", ECCurve.Secp256r1) + ], + ValidatorsCount = 1, + 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.UnitTests/TestUtils.Block.cs b/tests/Neo.UnitTests/TestUtils.Block.cs new file mode 100644 index 0000000000..699cd2f2ad --- /dev/null +++ b/tests/Neo.UnitTests/TestUtils.Block.cs @@ -0,0 +1,200 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TestUtils.Block.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Akka.Util.Internal; +using Neo.Cryptography; +using Neo.Extensions.IO; +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.Runtime.CompilerServices; + +namespace Neo.UnitTests; + +public partial class TestUtils +{ + const byte Prefix_Block = 5; + const byte Prefix_BlockHash = 9; + const byte Prefix_Transaction = 11; + const byte Prefix_CurrentBlock = 12; + + /// + /// Test Util function MakeHeader + /// + /// The snapshot of the current storage provider. Can be null. + /// The previous block hash + public static Header MakeHeader(DataCache? snapshot, UInt256 prevHash) + { + return new Header + { + PrevHash = prevHash, + MerkleRoot = UInt256.Parse("0x6226416a0e5aca42b5566f5a19ab467692688ba9d47986f6981a7f747bba2772"), + Timestamp = new DateTime(2024, 06, 05, 0, 33, 1, 001, DateTimeKind.Utc).ToTimestampMS(), + Index = snapshot != null ? NativeContract.Ledger.CurrentIndex(snapshot) + 1 : 0, + Nonce = 0, + NextConsensus = UInt160.Zero, + Witness = new Witness + { + InvocationScript = ReadOnlyMemory.Empty, + VerificationScript = new[] { (byte)OpCode.PUSH1 } + } + }; + } + + public static Block MakeBlock(DataCache? snapshot, UInt256 prevHash, int numberOfTransactions) + { + var block = (Block)RuntimeHelpers.GetUninitializedObject(typeof(Block)); + var header = MakeHeader(snapshot, prevHash); + var transactions = new Transaction[numberOfTransactions]; + if (numberOfTransactions > 0) + { + for (var i = 0; i < numberOfTransactions; i++) + { + transactions[i] = GetTransaction(UInt160.Zero); + } + } + + block.Header = header; + block.Transactions = transactions; + header.MerkleRoot = MerkleTree.ComputeRoot(block.Transactions.Select(p => p.Hash).ToArray()); + return block; + } + + public static Block CreateBlockWithValidTransactions(DataCache snapshot, + NEP6Wallet wallet, WalletAccount account, int numberOfTransactions) + { + var transactions = new List(); + for (var i = 0; i < numberOfTransactions; i++) + { + transactions.Add(CreateValidTx(snapshot, wallet, account)); + } + + return CreateBlockWithValidTransactions(snapshot, account, [.. transactions]); + } + + public static Block CreateBlockWithValidTransactions(DataCache snapshot, + WalletAccount account, Transaction[] transactions) + { + var block = (Block)RuntimeHelpers.GetUninitializedObject(typeof(Block)); + var key = NativeContract.Ledger.CreateStorageKey(Prefix_CurrentBlock); + var state = snapshot.TryGet(key)!.GetInteroperable(); + var header = MakeHeader(snapshot, state.Hash); + + block.Header = header; + block.Transactions = transactions; + + header.MerkleRoot = MerkleTree.ComputeRoot(block.Transactions.Select(p => p.Hash).ToArray()); + var contract = Contract.CreateMultiSigContract(1, TestProtocolSettings.SoleNode.StandbyCommittee); + var sc = new ContractParametersContext(snapshot, header, TestProtocolSettings.SoleNode.Network); + var signature = header.Sign(account.GetKey()!, TestProtocolSettings.SoleNode.Network); + sc.AddSignature(contract, TestProtocolSettings.SoleNode.StandbyCommittee[0], [.. signature]); + block.Header.Witness = sc.GetWitnesses()[0]; + + return block; + } + + public static void BlocksDelete(DataCache snapshot, UInt256 hash) + { + snapshot.Delete(NativeContract.Ledger.CreateStorageKey(Prefix_BlockHash, hash)); + snapshot.Delete(NativeContract.Ledger.CreateStorageKey(Prefix_Block, hash)); + } + + public static void TransactionAdd(DataCache snapshot, params TransactionState[] txs) + { + foreach (var tx in txs) + { + var key = NativeContract.Ledger.CreateStorageKey(Prefix_Transaction, tx.Transaction!.Hash); + snapshot.Add(key, new StorageItem(tx)); + } + } + + public static void BlocksAdd(DataCache snapshot, UInt256 hash, TrimmedBlock block) + { + var indexKey = NativeContract.Ledger.CreateStorageKey(Prefix_BlockHash, block.Index); + snapshot.Add(indexKey, new StorageItem(hash.ToArray())); + + var hashKey = NativeContract.Ledger.CreateStorageKey(Prefix_Block, hash); + snapshot.Add(hashKey, new StorageItem(block.ToArray())); + + var key = NativeContract.Ledger.CreateStorageKey(Prefix_CurrentBlock); + var state = snapshot.GetAndChange(key, () => new(new HashIndexState())).GetInteroperable(); + state.Hash = hash; + state.Index = block.Index; + } + + public static void BlocksAdd(DataCache snapshot, UInt256 hash, Block block) + { + + block.Transactions.ForEach(tx => + { + var state = new TransactionState + { + BlockIndex = block.Index, + Transaction = tx + }; + TransactionAdd(snapshot, state); + }); + + var indexKey = NativeContract.Ledger.CreateStorageKey(Prefix_BlockHash, block.Index); + snapshot.Add(indexKey, new StorageItem(hash.ToArray())); + + var hashKey = NativeContract.Ledger.CreateStorageKey(Prefix_Block, hash); + snapshot.Add(hashKey, new StorageItem(block.ToTrimmedBlock().ToArray())); + + var key = NativeContract.Ledger.CreateStorageKey(Prefix_CurrentBlock); + var state = snapshot.GetAndChange(key, () => new(new HashIndexState())).GetInteroperable(); + state.Hash = hash; + state.Index = block.Index; + } + + public static string CreateInvalidBlockFormat() + { + // Create a valid block + var validBlock = new Block + { + Header = new Header + { + Version = 0, + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Timestamp = 0, + Index = 0, + NextConsensus = UInt160.Zero, + Witness = Witness.Empty, + }, + Transactions = [] + }; + + // Serialize the valid block + var validBlockBytes = validBlock.ToArray(); + + // Corrupt the serialized data + // For example, we can truncate the data by removing the last few bytes + var invalidBlockBytes = new byte[validBlockBytes.Length - 5]; + Array.Copy(validBlockBytes, invalidBlockBytes, invalidBlockBytes.Length); + + // Convert the corrupted data to a Base64 string + return Convert.ToBase64String(invalidBlockBytes); + } + + public static TrimmedBlock ToTrimmedBlock(this Block block) + { + return new TrimmedBlock + { + Header = block.Header, + Hashes = block.Transactions.Select(p => p.Hash).ToArray() + }; + } +} diff --git a/tests/Neo.UnitTests/TestUtils.Contract.cs b/tests/Neo.UnitTests/TestUtils.Contract.cs new file mode 100644 index 0000000000..7242e46e33 --- /dev/null +++ b/tests/Neo.UnitTests/TestUtils.Contract.cs @@ -0,0 +1,103 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TestUtils.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. + +using Neo.SmartContract; +using Neo.SmartContract.Manifest; + +namespace Neo.UnitTests; + +partial class TestUtils +{ + public static ContractManifest CreateDefaultManifest() + { + return new ContractManifest + { + Name = "testManifest", + Groups = [], + SupportedStandards = [], + Abi = new ContractAbi + { + Events = [], + Methods = + [ + new ContractMethodDescriptor + { + Name = "testMethod", + Parameters = [], + ReturnType = ContractParameterType.Void, + Offset = 0, + Safe = true + } + ] + }, + Permissions = [ContractPermission.DefaultPermission], + Trusts = WildcardContainer.Create(), + Extra = null + }; + } + + public static ContractManifest CreateManifest(string method, ContractParameterType returnType, params ContractParameterType[] parameterTypes) + { + var manifest = CreateDefaultManifest(); + manifest.Abi.Methods = + [ + new ContractMethodDescriptor() + { + Name = method, + Parameters = parameterTypes.Select((p, i) => new ContractParameterDefinition + { + Name = $"p{i}", + Type = p + }).ToArray(), + ReturnType = returnType + } + ]; + return manifest; + } + + public static ContractState GetContract(string method = "test", int parametersCount = 0) + { + NefFile nef = new() + { + Compiler = "", + Source = "", + Tokens = [], + Script = new byte[] { 0x01, 0x01, 0x01, 0x01 } + }; + nef.CheckSum = NefFile.ComputeChecksum(nef); + return new ContractState + { + Id = 0x43000000, + Nef = nef, + Hash = nef.Script.Span.ToScriptHash(), + Manifest = CreateManifest(method, ContractParameterType.Any, Enumerable.Repeat(ContractParameterType.Any, parametersCount).ToArray()) + }; + } + + internal static ContractState GetContract(byte[] script, ContractManifest? manifest = null) + { + NefFile nef = new() + { + Compiler = "", + Source = "", + Tokens = [], + Script = script + }; + nef.CheckSum = NefFile.ComputeChecksum(nef); + return new ContractState + { + Id = 1, + Hash = script.ToScriptHash(), + Nef = nef, + Manifest = manifest ?? CreateDefaultManifest() + }; + } +} diff --git a/tests/Neo.UnitTests/TestUtils.Transaction.cs b/tests/Neo.UnitTests/TestUtils.Transaction.cs new file mode 100644 index 0000000000..a378b845c7 --- /dev/null +++ b/tests/Neo.UnitTests/TestUtils.Transaction.cs @@ -0,0 +1,193 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TestUtils.Transaction.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Extensions.IO; +using Neo.Factories; +using Neo.IO; +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.Numerics; + +namespace Neo.UnitTests; + +public partial class TestUtils +{ + public static Transaction CreateValidTx(DataCache snapshot, NEP6Wallet wallet, WalletAccount account) + { + return CreateValidTx(snapshot, wallet, account.ScriptHash, RandomNumberFactory.NextUInt32()); + } + + public static Transaction CreateValidTx(DataCache snapshot, NEP6Wallet wallet, UInt160 account, uint nonce) + { + var tx = wallet.MakeTransaction(snapshot, [ + new TransferOutput + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = account, + Value = new BigDecimal(BigInteger.One, 8) + } + ], + account); + + tx.Nonce = nonce; + tx.Signers = [new Signer { Account = account, Scopes = WitnessScope.CalledByEntry }]; + var data = new ContractParametersContext(snapshot, tx, TestProtocolSettings.Default.Network); + Assert.IsNull(data.GetSignatures(tx.Sender)); + Assert.IsTrue(wallet.Sign(data)); + Assert.IsTrue(data.Completed); + Assert.HasCount(1, data.GetSignatures(tx.Sender)!); + + tx.Witnesses = data.GetWitnesses(); + return tx; + } + + public static Transaction CreateValidTx(DataCache snapshot, NEP6Wallet wallet, UInt160 account, uint nonce, UInt256[] conflicts) + { + var tx = wallet.MakeTransaction(snapshot, [ + new TransferOutput + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = account, + Value = new BigDecimal(BigInteger.One, 8) + } + ], + account); + tx.Attributes = conflicts.Select(conflict => new Conflicts { Hash = conflict }).ToArray(); + tx.Nonce = nonce; + tx.Signers = [new Signer { Account = account, Scopes = WitnessScope.CalledByEntry }]; + var data = new ContractParametersContext(snapshot, tx, TestProtocolSettings.Default.Network); + Assert.IsNull(data.GetSignatures(tx.Sender)); + Assert.IsTrue(wallet.Sign(data)); + Assert.IsTrue(data.Completed); + Assert.HasCount(1, data.GetSignatures(tx.Sender)!); + tx.Witnesses = data.GetWitnesses(); + return tx; + } + + public static Transaction CreateRandomHashTransaction() + { + var randomBytes = new byte[16]; + TestRandom.NextBytes(randomBytes); + return new Transaction + { + Script = randomBytes, + Attributes = [], + Signers = [new Signer { Account = UInt160.Zero }], + Witnesses = [Witness.Empty], + }; + } + + public static Transaction GetTransaction(UInt160 sender) + { + return new Transaction + { + Script = new[] { (byte)OpCode.PUSH2 }, + Attributes = [], + Signers = + [ + new() + { + Account = sender, + Scopes = WitnessScope.CalledByEntry, + AllowedContracts = [], + AllowedGroups = [], + Rules = [], + } + ], + Witnesses = [Witness.Empty], + }; + } + + public static Transaction GetTransaction(UInt160 sender, UInt160 signer) + { + return new Transaction + { + Script = new[] { (byte)OpCode.PUSH2 }, + Attributes = [], + Signers = + [ + new Signer + { + Account = sender, + Scopes = WitnessScope.CalledByEntry, + AllowedContracts = [], + AllowedGroups = [], + Rules = [], + }, + new Signer + { + Account = signer, + Scopes = WitnessScope.CalledByEntry, + AllowedContracts = [], + AllowedGroups = [], + Rules = [], + } + ], + Witnesses = + [ + new Witness + { + InvocationScript = Memory.Empty, + VerificationScript = Memory.Empty, + }, + new Witness + { + InvocationScript = Memory.Empty, + VerificationScript = Memory.Empty, + } + ] + }; + } + + public enum InvalidTransactionType + { + InsufficientBalance, + InvalidSignature, + InvalidScript, + InvalidAttribute, + Oversized, + Expired, + Conflicting + } + + class InvalidAttribute : TransactionAttribute + { + public override TransactionAttributeType Type => (TransactionAttributeType)0xFF; + public override bool AllowMultiple { get; } + protected override void DeserializeWithoutType(ref MemoryReader reader) { } + protected override void SerializeWithoutType(BinaryWriter writer) { } + } + + public static void AddTransactionToBlockchain(DataCache snapshot, Transaction tx) + { + var block = new Block + { + Header = new Header + { + Index = NativeContract.Ledger.CurrentIndex(snapshot) + 1, + PrevHash = NativeContract.Ledger.CurrentHash(snapshot), + MerkleRoot = new UInt256(Crypto.Hash256(tx.Hash.ToArray())), + Timestamp = TimeProvider.Current.UtcNow.ToTimestampMS(), + NextConsensus = UInt160.Zero, + Witness = Witness.Empty, + }, + Transactions = [tx] + }; + + BlocksAdd(snapshot, block.Hash, block); + } +} diff --git a/tests/Neo.UnitTests/TestUtils.cs b/tests/Neo.UnitTests/TestUtils.cs new file mode 100644 index 0000000000..268d8be1f4 --- /dev/null +++ b/tests/Neo.UnitTests/TestUtils.cs @@ -0,0 +1,124 @@ +// Copyright (C) 2015-2025 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.Extensions.IO; +using Neo.IO; +using Neo.Json; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.Wallets; +using Neo.Wallets.NEP6; + +namespace Neo.UnitTests; + +public static partial class TestUtils +{ + public static readonly Random TestRandom = new(1337); // use fixed seed for guaranteed determinism + + public static UInt256 RandomUInt256() + { + var data = new byte[32]; + TestRandom.NextBytes(data); + return new UInt256(data); + } + + public static UInt160 RandomUInt160() + { + var data = new byte[20]; + TestRandom.NextBytes(data); + return new UInt160(data); + } + + public static StorageKey CreateStorageKey(this NativeContract contract, byte prefix, ISerializableSpan? key = null) + { + 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).Add(value); + } + + public static byte[] GetByteArray(int length, byte firstByte) + { + var array = new byte[length]; + array[0] = firstByte; + for (var i = 1; i < length; i++) + { + array[i] = 0x20; + } + return array; + } + + public static NEP6Wallet GenerateTestWallet(string password) + { + var wallet = new JObject() + { + ["name"] = "noname", + ["version"] = new Version("1.0").ToString(), + ["scrypt"] = new ScryptParameters(2, 1, 1).ToJson(), + ["accounts"] = new JArray(), + ["extra"] = null + }; + Assert.AreEqual("{\"name\":\"noname\",\"version\":\"1.0\",\"scrypt\":{\"n\":2,\"r\":1,\"p\":1},\"accounts\":[],\"extra\":null}", wallet.ToString()); + return new NEP6Wallet(null!, password, TestProtocolSettings.Default, wallet); + } + + internal static StorageItem GetStorageItem(byte[] value) + { + return new StorageItem + { + Value = value + }; + } + + internal static StorageKey GetStorageKey(int id, byte[] keyValue) + { + return new StorageKey + { + Id = id, + Key = keyValue + }; + } + + public static void StorageItemAdd(DataCache snapshot, int id, byte[] keyValue, byte[] value) + { + snapshot.Add(new StorageKey + { + Id = id, + Key = keyValue + }, new StorageItem(value)); + } + + public static void FillMemoryPool(DataCache snapshot, NeoSystem system, NEP6Wallet wallet, WalletAccount account) + { + for (var i = 0; i < system.Settings.MemoryPoolMaxTransactions; i++) + { + var tx = CreateValidTx(snapshot, wallet, account); + system.MemPool.TryAdd(tx, snapshot); + } + } + + public static T CopyMsgBySerialization(T serializableObj, T newObj) where T : ISerializable + { + MemoryReader reader = new(serializableObj.ToArray()); + newObj.Deserialize(ref reader); + return newObj; + } + + public static bool EqualsTo(this StorageItem item, StorageItem other) + { + return item.Value.Span.SequenceEqual(other.Value.Span); + } +} diff --git a/tests/Neo.UnitTests/TestVerifiable.cs b/tests/Neo.UnitTests/TestVerifiable.cs new file mode 100644 index 0000000000..0addace527 --- /dev/null +++ b/tests/Neo.UnitTests/TestVerifiable.cs @@ -0,0 +1,54 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TestVerifiable.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 Neo.Persistence; + +namespace Neo.UnitTests; + +public class TestVerifiable : IVerifiable +{ + private readonly string testStr = "testStr"; + + public Witness[] Witnesses + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public int Size => throw new NotImplementedException(); + + public void Deserialize(ref MemoryReader reader) + { + throw new NotImplementedException(); + } + + public void DeserializeUnsigned(ref MemoryReader reader) + { + throw new NotImplementedException(); + } + + public UInt160[] GetScriptHashesForVerifying(IReadOnlyStore? snapshot = null) + { + throw new NotImplementedException(); + } + + public void Serialize(BinaryWriter writer) + { + throw new NotImplementedException(); + } + + public void SerializeUnsigned(BinaryWriter writer) + { + writer.Write(testStr); + } +} diff --git a/tests/Neo.UnitTests/TestWalletAccount.cs b/tests/Neo.UnitTests/TestWalletAccount.cs new file mode 100644 index 0000000000..f5dc3829b8 --- /dev/null +++ b/tests/Neo.UnitTests/TestWalletAccount.cs @@ -0,0 +1,43 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TestWalletAccount.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Moq; +using Neo.Factories; +using Neo.SmartContract; +using Neo.Wallets; + +namespace Neo.UnitTests; + +class TestWalletAccount : WalletAccount +{ + private static readonly KeyPair key; + + public override bool HasKey => true; + public override KeyPair GetKey() => key; + + public TestWalletAccount(UInt160 hash) + : base(hash, TestProtocolSettings.Default) + { + var mock = new Mock(() => new Contract + { + Script = Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = new[] { ContractParameterType.Signature } + }); + mock.SetupGet(p => p.ScriptHash).Returns(hash); + Contract = mock.Object; + } + + static TestWalletAccount() + { + byte[] prikey = RandomNumberFactory.NextBytes(32); + key = new KeyPair(prikey); + } +} diff --git a/tests/Neo.UnitTests/UT_BigDecimal.cs b/tests/Neo.UnitTests/UT_BigDecimal.cs new file mode 100644 index 0000000000..d1089de1fb --- /dev/null +++ b/tests/Neo.UnitTests/UT_BigDecimal.cs @@ -0,0 +1,304 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_BigDecimal.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.UnitTests; + +[TestClass] +public class UT_BigDecimal +{ + [TestMethod] + public void TestChangeDecimals() + { + BigDecimal originalValue = new(new BigInteger(12300), 5); + BigDecimal result1 = originalValue.ChangeDecimals(7); + Assert.AreEqual(new BigInteger(1230000), result1.Value); + Assert.AreEqual(7, result1.Decimals); + BigDecimal result2 = originalValue.ChangeDecimals(3); + Assert.AreEqual(new BigInteger(123), result2.Value); + Assert.AreEqual(3, result2.Decimals); + BigDecimal result3 = originalValue.ChangeDecimals(5); + Assert.AreEqual(originalValue.Value, result3.Value); + Assert.ThrowsExactly(() => originalValue.ChangeDecimals(2)); + } + + [TestMethod] + public void TestBigDecimalConstructor() + { + BigDecimal value = new(new BigInteger(45600), 7); + Assert.AreEqual(new BigInteger(45600), value.Value); + Assert.AreEqual(7, value.Decimals); + + value = new BigDecimal(new BigInteger(0), 5); + Assert.AreEqual(new BigInteger(0), value.Value); + Assert.AreEqual(5, value.Decimals); + + value = new BigDecimal(new BigInteger(-10), 0); + Assert.AreEqual(new BigInteger(-10), value.Value); + Assert.AreEqual(0, value.Decimals); + + value = new BigDecimal(123.456789M, 6); + Assert.AreEqual(new BigInteger(123456789), value.Value); + Assert.AreEqual(6, value.Decimals); + + value = new BigDecimal(-123.45M, 3); + Assert.AreEqual(new BigInteger(-123450), value.Value); + Assert.AreEqual(3, value.Decimals); + + value = new BigDecimal(123.45M, 2); + Assert.AreEqual(new BigInteger(12345), value.Value); + Assert.AreEqual(2, value.Decimals); + + value = new BigDecimal(123M, 0); + Assert.AreEqual(new BigInteger(123), value.Value); + Assert.AreEqual(0, value.Decimals); + + value = new BigDecimal(0M, 0); + Assert.AreEqual(new BigInteger(0), value.Value); + Assert.AreEqual(0, value.Decimals); + + value = new BigDecimal(5.5M, 1); + var b = new BigDecimal(55M); + Assert.AreEqual(b.Value, value.Value); + } + + [TestMethod] + public void TestGetDecimals() + { + BigDecimal value = new(new BigInteger(45600), 7); + Assert.AreEqual(1, value.Sign); + value = new BigDecimal(new BigInteger(0), 5); + Assert.AreEqual(0, value.Sign); + value = new BigDecimal(new BigInteger(-10), 0); + Assert.AreEqual(-1, value.Sign); + } + + [TestMethod] + public void TestCompareDecimals() + { + BigDecimal a = new(5.5M, 1); + BigDecimal b = new(55M); + BigDecimal c = new(55M, 1); + Assert.IsFalse(a.Equals(b)); + Assert.IsFalse(a.Equals(c)); + Assert.IsTrue(b.Equals(c)); + Assert.AreEqual(-1, a.CompareTo(b)); + Assert.AreEqual(-1, a.CompareTo(c)); + Assert.AreEqual(0, b.CompareTo(c)); + } + + [TestMethod] + public void TestCompareDecimalsObject() + { + var a = new BigDecimal(new BigInteger(12345), 2); + var b = new BigDecimal(new BigInteger(12345), 2); + var c = new BigDecimal(new BigInteger(54321), 2); + var d = new BigDecimal(new BigInteger(12345), 3); + var e = new BigInteger(12345); + + // Check same value and decimal + Assert.IsTrue(a.Equals((object)b)); + + // Check different value and decimal + Assert.IsFalse(a.Equals((object)c)); + + // Check same value and different decimal + Assert.IsFalse(a.Equals((object)d)); + + // Check different data type + Assert.IsFalse(a.Equals(e)); + } + + [TestMethod] + public void TestGetSign() + { + BigDecimal value = new(new BigInteger(45600), 7); + Assert.AreEqual(1, value.Sign); + value = new BigDecimal(new BigInteger(0), 5); + Assert.AreEqual(0, value.Sign); + value = new BigDecimal(new BigInteger(-10), 0); + Assert.AreEqual(-1, value.Sign); + } + + [TestMethod] + public void TestParse() + { + string s = "12345"; + byte decimals = 0; + Assert.AreEqual(new BigDecimal(new BigInteger(12345), 0), BigDecimal.Parse(s, decimals)); + + s = "abcdEfg"; + Assert.ThrowsExactly(() => BigDecimal.Parse(s, decimals)); + } + + [TestMethod] + public void TestToString() + { + BigDecimal value = new(new BigInteger(100000), 5); + Assert.AreEqual("1", value.ToString()); + value = new BigDecimal(new BigInteger(123456), 5); + Assert.AreEqual("1.23456", value.ToString()); + } + + [TestMethod] + public void TestTryParse() + { + string s = "12345"; + byte decimals = 0; + Assert.IsTrue(BigDecimal.TryParse(s, decimals, out BigDecimal result)); + Assert.AreEqual(new BigDecimal(new BigInteger(12345), 0), result); + + s = "12345E-5"; + decimals = 5; + Assert.IsTrue(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(new BigDecimal(new BigInteger(12345), 5), result); + + s = "abcdEfg"; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "123.45"; + decimals = 2; + Assert.IsTrue(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(new BigDecimal(new BigInteger(12345), 2), result); + + s = "123.45E-5"; + decimals = 7; + Assert.IsTrue(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(new BigDecimal(new BigInteger(12345), 7), result); + + s = "12345E-5"; + decimals = 3; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "1.2345"; + decimals = 3; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "1.2345E-5"; + decimals = 3; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "12345"; + decimals = 3; + Assert.IsTrue(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(new BigDecimal(new BigInteger(12345000), 3), result); + + s = "12345E-2"; + decimals = 3; + Assert.IsTrue(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(new BigDecimal(new BigInteger(123450), 3), result); + + s = "123.45"; + decimals = 3; + Assert.IsTrue(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(new BigDecimal(new BigInteger(123450), 3), result); + + s = "123.45E3"; + decimals = 3; + Assert.IsTrue(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(new BigDecimal(new BigInteger(123450000), 3), result); + + s = "a456bcdfg"; + decimals = 0; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "a456bce-5"; + decimals = 5; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "a4.56bcd"; + decimals = 5; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "a4.56bce3"; + decimals = 2; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "a456bcd"; + decimals = 2; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "a456bcdE3"; + decimals = 2; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "a456b.cd"; + decimals = 5; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + + s = "a456b.cdE3"; + decimals = 5; + Assert.IsFalse(BigDecimal.TryParse(s, decimals, out result)); + Assert.AreEqual(default, result); + } + + [TestMethod] + public void TestOperators() + { + var a = new BigDecimal(new BigInteger(1000), 2); + var b = new BigDecimal(new BigInteger(10000), 3); + var c = new BigDecimal(new BigInteger(10001), 2); + + // Check equal operator + Assert.IsTrue(a == b); + Assert.IsFalse(a == c); + + // Check different operator + Assert.IsFalse(a != b); + Assert.IsTrue(a != c); + + // Check less operator + Assert.IsTrue(a < c); + Assert.IsFalse(a < b); + + // Check less or equal operator + Assert.IsTrue(a <= c); + Assert.IsTrue(a <= b); + Assert.IsFalse(c <= a); + + // Check greater operator + Assert.IsFalse(a > c); + Assert.IsFalse(a > b); + Assert.IsTrue(c > a); + + // Check greater or equal operator + Assert.IsFalse(a >= c); + Assert.IsTrue(a >= b); + Assert.IsTrue(c >= a); + } + + [TestMethod] + public void TestGetHashCode() + { + var a = new BigDecimal(new BigInteger(123450), 3); + var b = new BigDecimal(new BigInteger(123450), 3); + var c = new BigDecimal(new BigInteger(12345), 2); + var d = new BigDecimal(new BigInteger(123451), 3); + // Check hash codes are equal for equivalent decimals + Assert.AreEqual(a.GetHashCode(), b.GetHashCode()); + // Check hash codes may differ for semantically equivalent values + Assert.AreNotEqual(a.GetHashCode(), c.GetHashCode()); + // Check hash codes are not equal for different values + Assert.AreNotEqual(a.GetHashCode(), d.GetHashCode()); + } +} diff --git a/tests/Neo.UnitTests/UT_DataCache.cs b/tests/Neo.UnitTests/UT_DataCache.cs new file mode 100644 index 0000000000..13c2bf8f8b --- /dev/null +++ b/tests/Neo.UnitTests/UT_DataCache.cs @@ -0,0 +1,104 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_DataCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Persistence; +using Neo.SmartContract; + +namespace Neo.UnitTests; + +[TestClass] +public class UT_DataCache +{ + [TestMethod] + public void TestCachedFind_Between() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var storages = snapshotCache.CloneCache(); + var cache = new ClonedCache(storages); + + storages.Add( + new StorageKey() { Key = new byte[] { 0x01, 0x01 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + storages.Add( + new StorageKey() { Key = new byte[] { 0x00, 0x01 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + storages.Add( + new StorageKey() { Key = new byte[] { 0x00, 0x03 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + cache.Add( + new StorageKey() { Key = new byte[] { 0x01, 0x02 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + cache.Add( + new StorageKey() { Key = new byte[] { 0x00, 0x02 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + + CollectionAssert.AreEqual( + cache.Find(new byte[5]).Select(u => u.Key.Key.Span[1]).ToArray(), + new byte[] { 0x01, 0x02, 0x03 } + ); + } + + [TestMethod] + public void TestCachedFind_Last() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var storages = snapshotCache.CloneCache(); + var cache = new ClonedCache(storages); + + storages.Add( + new StorageKey() { Key = new byte[] { 0x00, 0x01 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + storages.Add( + new StorageKey() { Key = new byte[] { 0x01, 0x01 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + cache.Add( + new StorageKey() { Key = new byte[] { 0x00, 0x02 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + cache.Add( + new StorageKey() { Key = new byte[] { 0x01, 0x02 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + CollectionAssert.AreEqual( + cache.Find(new byte[5]).Select(u => u.Key.Key.Span[1]).ToArray(), + new byte[] { 0x01, 0x02 } + ); + } + + [TestMethod] + public void TestCachedFind_Empty() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var storages = snapshotCache.CloneCache(); + var cache = new ClonedCache(storages); + + cache.Add( + new StorageKey() { Key = new byte[] { 0x00, 0x02 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + cache.Add( + new StorageKey() { Key = new byte[] { 0x01, 0x02 }, Id = 0 }, + new StorageItem() { Value = ReadOnlyMemory.Empty } + ); + + CollectionAssert.AreEqual( + cache.Find(new byte[5]).Select(u => u.Key.Key.Span[1]).ToArray(), + new byte[] { 0x02 } + ); + } +} diff --git a/tests/Neo.UnitTests/UT_Helper.cs b/tests/Neo.UnitTests/UT_Helper.cs new file mode 100644 index 0000000000..263a51a66a --- /dev/null +++ b/tests/Neo.UnitTests/UT_Helper.cs @@ -0,0 +1,138 @@ +// Copyright (C) 2015-2025 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 Neo.Collections; +using Neo.IO.Caching; +using Neo.Network; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.Wallets; +using System.Net; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Neo.UnitTests; + +[TestClass] +public class UT_Helper +{ + [TestMethod] + public void GetSignData() + { + TestVerifiable verifiable = new(); + var res = verifiable.GetSignData(TestProtocolSettings.Default.Network); + + Assert.AreEqual("4e454f3350b51da6bb366be3ea50140cda45ba7df575287c0371000b2037ed3898ff8bf5", res.ToHexString()); + + res = verifiable.CalculateHash().GetSignData(TestProtocolSettings.Default.Network); + Assert.AreEqual("4e454f3350b51da6bb366be3ea50140cda45ba7df575287c0371000b2037ed3898ff8bf5", res.ToHexString()); + } + + [TestMethod] + public void TestTryGetHash() + { + var tx = (Transaction)RuntimeHelpers.GetUninitializedObject(typeof(Transaction)); + Assert.IsFalse(tx.TryGetHash(out _)); + } + + [TestMethod] + public void Sign() + { + TestVerifiable verifiable = new(); + byte[] res = verifiable.Sign(new KeyPair(TestUtils.GetByteArray(32, 0x42)), TestProtocolSettings.Default.Network); + Assert.HasCount(64, res); + } + + [TestMethod] + public void ToScriptHash() + { + byte[] testByteArray = TestUtils.GetByteArray(64, 0x42); + UInt160 res = testByteArray.ToScriptHash(); + Assert.AreEqual(UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302"), res); + } + + [TestMethod] + public void TestRemoveHashsetHashSetCache() + { + var a = new HashSet { 1, 2, 3 }; + var b = new HashSetCache(10); + b.TryAdd(2); + + a.Remove(b); + CollectionAssert.AreEqual(new int[] { 1, 3 }, a.ToArray()); + + b.TryAdd(4); + b.TryAdd(5); + b.TryAdd(1); + a.Remove(b); + + CollectionAssert.AreEqual(new int[] { 3 }, a.ToArray()); + } + + [TestMethod] + public void TestToHexString() + { + byte[] empty = Array.Empty(); + Assert.AreEqual("", empty.ToHexString()); + Assert.AreEqual("", empty.ToHexString(false)); + Assert.AreEqual("", empty.ToHexString(true)); + + byte[] str1 = [(byte)'n', (byte)'e', (byte)'o']; + Assert.AreEqual("6e656f", str1.ToHexString()); + Assert.AreEqual("6e656f", str1.ToHexString(false)); + Assert.AreEqual("6f656e", str1.ToHexString(true)); + } + + [TestMethod] + public void TestGetVersion() + { + // assembly without version + + var asm = AppDomain.CurrentDomain.GetAssemblies() + .Where(u => u.FullName == "Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null") + .FirstOrDefault(); + string version = asm?.GetVersion() ?? ""; + Assert.AreEqual("0.0.0", version); + } + + [TestMethod] + public void TestToByteArrayStandard() + { + BigInteger number = BigInteger.Zero; + Assert.AreEqual("", number.ToByteArrayStandard().ToHexString()); + + number = BigInteger.One; + Assert.AreEqual("01", number.ToByteArrayStandard().ToHexString()); + } + + [TestMethod] + public void TestUnmapForIPAddress() + { + var addr = new IPAddress([127, 0, 0, 1]); + Assert.AreEqual(addr, addr.UnMap()); + + var addr2 = addr.MapToIPv6(); + Assert.AreEqual(addr, addr2.UnMap()); + } + + [TestMethod] + public void TestUnmapForIPEndPoin() + { + var addr = new IPAddress([127, 0, 0, 1]); + var endPoint = new IPEndPoint(addr, 8888); + Assert.AreEqual(endPoint, endPoint.UnMap()); + + var addr2 = addr.MapToIPv6(); + var endPoint2 = new IPEndPoint(addr2, 8888); + Assert.AreEqual(endPoint, endPoint2.UnMap()); + } +} diff --git a/tests/Neo.UnitTests/UT_NeoSystem.cs b/tests/Neo.UnitTests/UT_NeoSystem.cs new file mode 100644 index 0000000000..956c1246bd --- /dev/null +++ b/tests/Neo.UnitTests/UT_NeoSystem.cs @@ -0,0 +1,97 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NeoSystem.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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; + +namespace Neo.UnitTests; + +[TestClass] +public class UT_NeoSystem +{ + private NeoSystem _system = null!; + + [TestInitialize] + public void Setup() + { + _system = TestBlockchain.GetSystem(); + } + + [TestMethod] + public void TestGetBlockchain() => Assert.IsNotNull(_system.Blockchain); + + [TestMethod] + public void TestGetLocalNode() => Assert.IsNotNull(_system.LocalNode); + + [TestMethod] + public void TestGetTaskManager() => Assert.IsNotNull(_system.TaskManager); + + [TestMethod] + public void TestAddAndGetService() + { + var service = new object(); + _system.AddService(service); + + var result = _system.GetService(); + Assert.AreEqual(service, result); + } + + [TestMethod] + public void TestGetServiceWithFilter() + { + _system.AddService("match"); + _system.AddService("skip"); + + var result = _system.GetService(s => s == "match"); + Assert.AreEqual("match", result); + } + + [TestMethod] + public void TestResumeNodeStartup() + { + _system.SuspendNodeStartup(); + _system.SuspendNodeStartup(); + Assert.IsFalse(_system.ResumeNodeStartup()); + Assert.IsTrue(_system.ResumeNodeStartup()); // now it should resume + } + + [TestMethod] + public void TestStartNodeWhenNoSuspended() + { + var config = new ChannelsConfig(); + _system.StartNode(config); + } + + [TestMethod] + public void TestStartNodeWhenSuspended() + { + _system.SuspendNodeStartup(); + _system.SuspendNodeStartup(); + var config = new ChannelsConfig(); + _system.StartNode(config); + Assert.IsFalse(_system.ResumeNodeStartup()); + Assert.IsTrue(_system.ResumeNodeStartup()); + } + + [TestMethod] + public void TestEnsureStoppedStopsActor() + { + var sys = TestBlockchain.GetSystem(); + sys.EnsureStopped(sys.LocalNode); + } + + [TestMethod] + public void TestContainsTransactionNotExist() + { + var txHash = new UInt256(new byte[32]); + var result = _system.ContainsTransaction(txHash); + Assert.AreEqual(ContainsTransactionType.NotExist, result); + } +} diff --git a/tests/Neo.UnitTests/UT_ProtocolSettings.cs b/tests/Neo.UnitTests/UT_ProtocolSettings.cs new file mode 100644 index 0000000000..2b3e9ed2e2 --- /dev/null +++ b/tests/Neo.UnitTests/UT_ProtocolSettings.cs @@ -0,0 +1,286 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ProtocolSettings.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Wallets; +using System.Text.RegularExpressions; + +namespace Neo.UnitTests; + +[TestClass] +public class UT_ProtocolSettings +{ + [TestMethod] + public void CheckFirstLetterOfAddresses() + { + UInt160 min = UInt160.Parse("0x0000000000000000000000000000000000000000"); + Assert.AreEqual('N', min.ToAddress(TestProtocolSettings.Default.AddressVersion)[0]); + UInt160 max = UInt160.Parse("0xffffffffffffffffffffffffffffffffffffffff"); + Assert.AreEqual('N', max.ToAddress(TestProtocolSettings.Default.AddressVersion)[0]); + } + + [TestMethod] + public void Default_Network_should_be_mainnet_Network_value() + { + var mainNetNetwork = 0x334F454Eu; + Assert.AreEqual(mainNetNetwork, TestProtocolSettings.Default.Network); + } + + [TestMethod] + public void TestGetMemoryPoolMaxTransactions() + { + Assert.AreEqual(50000, TestProtocolSettings.Default.MemoryPoolMaxTransactions); + } + + [TestMethod] + public void TestGetMillisecondsPerBlock() + { + Assert.AreEqual((uint)15000, (uint)TestProtocolSettings.Default.MillisecondsPerBlock); + } + + internal static string CreateHFSettings(string hf) + { + return """ + { + "ProtocolConfiguration": { + "Network": 860833102, + "AddressVersion": 53, + "MillisecondsPerBlock": 15000, + "MaxTransactionsPerBlock": 512, + "MemoryPoolMaxTransactions": 50000, + "MaxTraceableBlocks": 2102400, + "Hardforks": { + """ + hf + """ + }, + "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" + ] + } + } + """; + } + + [TestMethod] + public void TestGetSeedList() + { + CollectionAssert.AreEqual(new string[] { + "seed1.neo.org:10333", + "seed2.neo.org:10333", + "seed3.neo.org:10333", + "seed4.neo.org:10333", + "seed5.neo.org:10333" + }, TestProtocolSettings.Default.SeedList.ToArray()); + } + + [TestMethod] + public void TestStandbyCommitteeAddressesFormat() + { + foreach (var point in TestProtocolSettings.Default.StandbyCommittee) + { + Assert.MatchesRegex(new Regex("^[0-9A-Fa-f]{66}$"), point.ToString()); // ECPoint is 66 hex characters + } + } + + [TestMethod] + public void TestValidatorsCount() + { + Assert.HasCount(TestProtocolSettings.Default.ValidatorsCount * 3, TestProtocolSettings.Default.StandbyCommittee); + } + + [TestMethod] + public void TestMaxTransactionsPerBlock() + { + Assert.IsGreaterThan(0u, TestProtocolSettings.Default.MaxTransactionsPerBlock); + Assert.IsLessThanOrEqualTo(50000u, TestProtocolSettings.Default.MaxTransactionsPerBlock); // Assuming 50000 as a reasonable upper limit + } + + [TestMethod] + public void TestMaxTraceableBlocks() + { + Assert.IsGreaterThan(0u, TestProtocolSettings.Default.MaxTraceableBlocks); + } + + [TestMethod] + public void TestMaxValidUntilBlockIncrement() + { + Assert.IsGreaterThan(0u, TestProtocolSettings.Default.MaxValidUntilBlockIncrement); + } + + [TestMethod] + public void TestInitialGasDistribution() + { + Assert.IsGreaterThan(0ul, TestProtocolSettings.Default.InitialGasDistribution); + } + + [TestMethod] + public void TestHardforksSettings() + { + Assert.IsNotNull(TestProtocolSettings.Default.Hardforks); + } + + [TestMethod] + public void TestAddressVersion() + { + Assert.IsGreaterThanOrEqualTo(0, TestProtocolSettings.Default.AddressVersion); + Assert.IsLessThanOrEqualTo(255, TestProtocolSettings.Default.AddressVersion); // Address version is a byte + } + + [TestMethod] + public void TestNetworkSettingsConsistency() + { + Assert.IsGreaterThan(0u, TestProtocolSettings.Default.Network); + Assert.IsNotNull(TestProtocolSettings.Default.SeedList); + } + + [TestMethod] + public void TestECPointParsing() + { + foreach (var point in TestProtocolSettings.Default.StandbyCommittee) + { + try + { + ECPoint.Parse(point.ToString(), ECCurve.Secp256r1); + } + catch (Exception ex) + { + Assert.Fail($"Expected no exception, but got: {ex.Message}"); + } + } + } + + [TestMethod] + public void TestSeedListFormatAndReachability() + { + foreach (var seed in TestProtocolSettings.Default.SeedList) + { + Assert.MatchesRegex(new Regex(@"^[\w.-]+:\d+$"), seed); // Format: domain:port + } + } + + [TestMethod] + public void TestDefaultNetworkValue() + { + Assert.AreEqual((uint)0, ProtocolSettings.Default.Network); + } + + [TestMethod] + public void TestDefaultAddressVersionValue() + { + Assert.AreEqual(ProtocolSettings.Default.AddressVersion, TestProtocolSettings.Default.AddressVersion); + } + + [TestMethod] + public void TestDefaultValidatorsCountValue() + { + Assert.AreEqual(0, ProtocolSettings.Default.ValidatorsCount); + } + + [TestMethod] + public void TestDefaultMillisecondsPerBlockValue() + { + Assert.AreEqual(ProtocolSettings.Default.MillisecondsPerBlock, TestProtocolSettings.Default.MillisecondsPerBlock); + } + + [TestMethod] + public void TestDefaultMaxTransactionsPerBlockValue() + { + Assert.AreEqual(ProtocolSettings.Default.MaxTransactionsPerBlock, TestProtocolSettings.Default.MaxTransactionsPerBlock); + } + + [TestMethod] + public void TestDefaultMemoryPoolMaxTransactionsValue() + { + Assert.AreEqual(ProtocolSettings.Default.MemoryPoolMaxTransactions, TestProtocolSettings.Default.MemoryPoolMaxTransactions); + } + + [TestMethod] + public void TestDefaultMaxTraceableBlocksValue() + { + Assert.AreEqual(ProtocolSettings.Default.MaxTraceableBlocks, TestProtocolSettings.Default.MaxTraceableBlocks); + } + + [TestMethod] + public void TestDefaultMaxValidUntilBlockIncrementValue() + { + Assert.AreEqual(ProtocolSettings.Default.MaxValidUntilBlockIncrement, TestProtocolSettings.Default.MaxValidUntilBlockIncrement); + } + + [TestMethod] + public void TestDefaultInitialGasDistributionValue() + { + Assert.AreEqual(ProtocolSettings.Default.InitialGasDistribution, TestProtocolSettings.Default.InitialGasDistribution); + } + + [TestMethod] + public void TestDefaultHardforksValue() + { + CollectionAssert.AreEqual(ProtocolSettings.Default.Hardforks, TestProtocolSettings.Default.Hardforks); + } + + [TestMethod] + public void TestTimePerBlockCalculation() + { + var expectedTimeSpan = TimeSpan.FromMilliseconds(TestProtocolSettings.Default.MillisecondsPerBlock); + Assert.AreEqual(expectedTimeSpan, TestProtocolSettings.Default.TimePerBlock); + } + + [TestMethod] + public void TestLoad() + { + var loadedSetting = ProtocolSettings.Load("test.config.json"); + + // Comparing all properties + Assert.AreEqual(TestProtocolSettings.Default.Network, loadedSetting.Network); + Assert.AreEqual(TestProtocolSettings.Default.AddressVersion, loadedSetting.AddressVersion); + CollectionAssert.AreEqual(TestProtocolSettings.Default.StandbyCommittee.ToList(), loadedSetting.StandbyCommittee.ToList()); + Assert.AreEqual(TestProtocolSettings.Default.ValidatorsCount, loadedSetting.ValidatorsCount); + CollectionAssert.AreEqual(TestProtocolSettings.Default.SeedList, loadedSetting.SeedList); + Assert.AreEqual(TestProtocolSettings.Default.MillisecondsPerBlock, loadedSetting.MillisecondsPerBlock); + Assert.AreEqual(TestProtocolSettings.Default.MaxTransactionsPerBlock, loadedSetting.MaxTransactionsPerBlock); + Assert.AreEqual(TestProtocolSettings.Default.MemoryPoolMaxTransactions, loadedSetting.MemoryPoolMaxTransactions); + Assert.AreEqual(TestProtocolSettings.Default.MaxTraceableBlocks, loadedSetting.MaxTraceableBlocks); + Assert.AreEqual(TestProtocolSettings.Default.MaxValidUntilBlockIncrement, loadedSetting.MaxValidUntilBlockIncrement); + Assert.AreEqual(TestProtocolSettings.Default.InitialGasDistribution, loadedSetting.InitialGasDistribution); + CollectionAssert.AreEqual(TestProtocolSettings.Default.Hardforks, loadedSetting.Hardforks); + + // If StandbyValidators is a derived property, comparing it as well + CollectionAssert.AreEqual(TestProtocolSettings.Default.StandbyValidators.ToList(), loadedSetting.StandbyValidators.ToList()); + } +} diff --git a/tests/Neo.UnitTests/UT_UInt160.cs b/tests/Neo.UnitTests/UT_UInt160.cs new file mode 100644 index 0000000000..99080744c8 --- /dev/null +++ b/tests/Neo.UnitTests/UT_UInt160.cs @@ -0,0 +1,178 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_UInt160.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable CS1718 // Comparison made to same variable + +using Neo.Extensions.IO; +using Neo.Factories; +using Neo.IO; + +namespace Neo.UnitTests; + +[TestClass] +public class UT_UInt160 +{ + [TestMethod] + public void TestFail() + { + Assert.ThrowsExactly(() => _ = new UInt160(new byte[UInt160.Length + 1])); + } + + [TestMethod] + public void TestGernerator1() + { + var uInt160 = new UInt160(); + Assert.IsNotNull(uInt160); + } + + [TestMethod] + public void TestGernerator2() + { + UInt160 uInt160 = new byte[20]; + Assert.IsNotNull(uInt160); + } + + [TestMethod] + public void TestGernerator3() + { + UInt160 uInt160 = "0xff00000000000000000000000000000000000001"; + Assert.IsNotNull(uInt160); + Assert.AreEqual("0xff00000000000000000000000000000000000001", uInt160.ToString()); + + UInt160 value = "0x0102030405060708090a0b0c0d0e0f1011121314"; + Assert.IsNotNull(value); + Assert.AreEqual("0x0102030405060708090a0b0c0d0e0f1011121314", value.ToString()); + } + + [TestMethod] + public void TestCompareTo() + { + var temp = new byte[20]; + temp[19] = 0x01; + var result = new UInt160(temp); + Assert.AreEqual(0, UInt160.Zero.CompareTo(UInt160.Zero)); + Assert.AreEqual(-1, UInt160.Zero.CompareTo(result)); + Assert.AreEqual(1, result.CompareTo(UInt160.Zero)); + Assert.AreEqual(0, result.CompareTo(temp)); + } + + [TestMethod] + public void TestEquals() + { + byte[] temp = new byte[20]; + temp[19] = 0x01; + var result = new UInt160(temp); + Assert.IsTrue(UInt160.Zero.Equals(UInt160.Zero)); + Assert.IsFalse(UInt160.Zero.Equals(result)); + Assert.IsFalse(result.Equals(null)); + Assert.IsTrue(UInt160.Zero == UInt160.Zero); + Assert.IsFalse(UInt160.Zero != UInt160.Zero); + Assert.IsTrue(UInt160.Zero == "0x0000000000000000000000000000000000000000"); + Assert.IsFalse(UInt160.Zero == "0x0000000000000000000000000000000000000001"); + } + + [TestMethod] + public void TestParse() + { + UInt160 result = UInt160.Parse("0x0000000000000000000000000000000000000000"); + Assert.AreEqual(UInt160.Zero, result); + Assert.ThrowsExactly(() => UInt160.Parse("000000000000000000000000000000000000000")); + UInt160 result1 = UInt160.Parse("0000000000000000000000000000000000000000"); + Assert.AreEqual(UInt160.Zero, result1); + } + + [TestMethod] + public void TestTryParse() + { + Assert.IsTrue(UInt160.TryParse("0x0000000000000000000000000000000000000000", out var temp)); + Assert.AreEqual("0x0000000000000000000000000000000000000000", temp.ToString()); + Assert.AreEqual(UInt160.Zero, temp); + Assert.IsTrue(UInt160.TryParse("0x1230000000000000000000000000000000000000", out temp)); + Assert.AreEqual("0x1230000000000000000000000000000000000000", temp.ToString()); + Assert.IsFalse(UInt160.TryParse("000000000000000000000000000000000000000", out _)); + Assert.IsFalse(UInt160.TryParse("0xKK00000000000000000000000000000000000000", out _)); + Assert.IsFalse(UInt160.TryParse(" 1 2 3 45 000000000000000000000000000000", out _)); + } + + [TestMethod] + public void TestOperatorLarger() + { + Assert.IsFalse(UInt160.Zero > UInt160.Zero); + Assert.IsFalse(UInt160.Zero > "0x0000000000000000000000000000000000000000"); + } + + [TestMethod] + public void TestOperatorLargerAndEqual() + { + Assert.IsTrue(UInt160.Zero >= UInt160.Zero); + Assert.IsTrue(UInt160.Zero >= "0x0000000000000000000000000000000000000000"); + } + + [TestMethod] + public void TestOperatorSmaller() + { + Assert.IsFalse(UInt160.Zero < UInt160.Zero); + Assert.IsFalse(UInt160.Zero < "0x0000000000000000000000000000000000000000"); + } + + [TestMethod] + public void TestOperatorSmallerAndEqual() + { + Assert.IsTrue(UInt160.Zero <= UInt160.Zero); + Assert.IsTrue(UInt160.Zero >= "0x0000000000000000000000000000000000000000"); + } + + [TestMethod] + public void TestSpanAndSerialize() + { + // random data + var data = RandomNumberFactory.NextBytes(UInt160.Length); + + var value = new UInt160(data); + var span = value.GetSpan(); + Assert.IsTrue(span.SequenceEqual(value.ToArray())); + + data = new byte[UInt160.Length]; + value.Serialize(data.AsSpan()); + CollectionAssert.AreEqual(data, value.ToArray()); + + data = new byte[UInt160.Length]; + ((ISerializableSpan)value).Serialize(data.AsSpan()); + CollectionAssert.AreEqual(data, value.ToArray()); + } + + [TestMethod] + public void TestSpanAndSerializeLittleEndian() + { + // random data + var data = RandomNumberFactory.NextBytes(UInt160.Length); + + var value = new UInt160(data); + + var spanLittleEndian = value.GetSpanLittleEndian(); + CollectionAssert.AreEqual(data, spanLittleEndian.ToArray()); + + var dataLittleEndian = new byte[UInt160.Length]; + value.SafeSerialize(dataLittleEndian.AsSpan()); + CollectionAssert.AreEqual(data, dataLittleEndian); + + // Check that Serialize LittleEndian and Serialize BigEndian are equals + var dataSerialized = new byte[UInt160.Length]; + value.Serialize(dataSerialized.AsSpan()); + CollectionAssert.AreEqual(value.ToArray(), dataSerialized); + + var shortBuffer = new byte[UInt160.Length - 1]; + Assert.ThrowsExactly(() => value.Serialize(shortBuffer.AsSpan())); + Assert.ThrowsExactly(() => value.SafeSerialize(shortBuffer.AsSpan())); + } +} + +#pragma warning restore CS1718 // Comparison made to same variable diff --git a/tests/Neo.UnitTests/UT_UInt256.cs b/tests/Neo.UnitTests/UT_UInt256.cs new file mode 100644 index 0000000000..981854494e --- /dev/null +++ b/tests/Neo.UnitTests/UT_UInt256.cs @@ -0,0 +1,211 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_UInt256.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#pragma warning disable CS1718 // Comparison made to same variable + +using Neo.Extensions.IO; +using Neo.Factories; +using Neo.IO; + +namespace Neo.UnitTests; + +[TestClass] +public class UT_UInt256 +{ + [TestMethod] + public void TestFail() + { + Assert.ThrowsExactly(() => _ = new UInt256(new byte[UInt256.Length + 1])); + } + + [TestMethod] + public void TestGernerator1() + { + UInt256 uInt256 = new(); + Assert.IsNotNull(uInt256); + } + + [TestMethod] + public void TestGernerator2() + { + UInt256 uInt256 = new byte[32]; + Assert.IsNotNull(uInt256); + Assert.AreEqual(UInt256.Zero, uInt256); + } + + [TestMethod] + public void TestGernerator3() + { + UInt256 uInt256 = "0xff00000000000000000000000000000000000000000000000000000000000001"; + Assert.IsNotNull(uInt256); + Assert.AreEqual("0xff00000000000000000000000000000000000000000000000000000000000001", uInt256.ToString()); + + UInt256 value = "0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; + Assert.IsNotNull(value); + Assert.AreEqual("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", value.ToString()); + } + + [TestMethod] + public void TestCompareTo() + { + byte[] temp = new byte[32]; + temp[31] = 0x01; + UInt256 result = new(temp); + Assert.AreEqual(0, UInt256.Zero.CompareTo(UInt256.Zero)); + Assert.AreEqual(-1, UInt256.Zero.CompareTo(result)); + Assert.AreEqual(1, result.CompareTo(UInt256.Zero)); + Assert.AreEqual(0, result.CompareTo(temp)); + } + + [TestMethod] + public void TestDeserialize() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + writer.Write(new byte[20]); + UInt256 uInt256 = new(); + Assert.ThrowsExactly(() => MemoryReaderDeserialize(stream.ToArray(), uInt256)); + + static void MemoryReaderDeserialize(byte[] buffer, ISerializable obj) + { + MemoryReader reader = new(buffer); + obj.Deserialize(ref reader); + } + } + + [TestMethod] + public void TestEquals() + { + var temp = new byte[32]; + temp[31] = 0x01; + + var result = new UInt256(temp); + Assert.IsTrue(UInt256.Zero.Equals(UInt256.Zero)); + Assert.IsFalse(UInt256.Zero.Equals(result)); + Assert.IsFalse(result.Equals(null)); + } + + [TestMethod] + public void TestEquals1() + { + var temp1 = new UInt256(); + var temp2 = new UInt256(); + var temp3 = new UInt160(); + Assert.IsFalse(temp1.Equals(null)); + Assert.IsTrue(temp1.Equals(temp1)); + Assert.IsTrue(temp1.Equals(temp2)); + Assert.IsFalse(temp1.Equals(temp3)); + } + + [TestMethod] + public void TestEquals2() + { + UInt256 temp1 = new(); + object? temp2 = null; + object temp3 = new(); + Assert.IsFalse(temp1.Equals(temp2)); + Assert.IsFalse(temp1.Equals(temp3)); + } + + [TestMethod] + public void TestParse() + { + UInt256 result = UInt256.Parse("0x0000000000000000000000000000000000000000000000000000000000000000"); + Assert.AreEqual(UInt256.Zero, result); + Assert.ThrowsExactly(() => UInt256.Parse("000000000000000000000000000000000000000000000000000000000000000")); + UInt256 result1 = UInt256.Parse("0000000000000000000000000000000000000000000000000000000000000000"); + Assert.AreEqual(UInt256.Zero, result1); + } + + [TestMethod] + public void TestTryParse() + { + Assert.IsTrue(UInt256.TryParse("0x0000000000000000000000000000000000000000000000000000000000000000", out var temp)); + Assert.AreEqual(UInt256.Zero, temp); + Assert.IsTrue(UInt256.TryParse("0x1230000000000000000000000000000000000000000000000000000000000000", out temp)); + Assert.AreEqual("0x1230000000000000000000000000000000000000000000000000000000000000", temp.ToString()); + Assert.IsFalse(UInt256.TryParse("000000000000000000000000000000000000000000000000000000000000000", out _)); + Assert.IsFalse(UInt256.TryParse("0xKK00000000000000000000000000000000000000000000000000000000000000", out _)); + } + + [TestMethod] + public void TestOperatorEqual() + { + Assert.IsFalse(new UInt256() == null); + Assert.IsFalse(null == new UInt256()); + } + + [TestMethod] + public void TestOperatorLarger() + { + Assert.IsFalse(UInt256.Zero > UInt256.Zero); + } + + [TestMethod] + public void TestOperatorLargerAndEqual() + { + Assert.IsTrue(UInt256.Zero >= UInt256.Zero); + } + + [TestMethod] + public void TestOperatorSmaller() + { + Assert.IsFalse(UInt256.Zero < UInt256.Zero); + } + + [TestMethod] + public void TestOperatorSmallerAndEqual() + { + Assert.IsTrue(UInt256.Zero <= UInt256.Zero); + } + + [TestMethod] + public void TestSpanAndSerialize() + { + var data = RandomNumberFactory.NextBytes(UInt256.Length); + + var value = new UInt256(data); + var span = value.GetSpan(); + Assert.IsTrue(span.SequenceEqual(value.ToArray())); + + data = new byte[UInt256.Length]; + value.Serialize(data.AsSpan()); + CollectionAssert.AreEqual(data, value.ToArray()); + + data = new byte[UInt256.Length]; + ((ISerializableSpan)value).Serialize(data.AsSpan()); + CollectionAssert.AreEqual(data, value.ToArray()); + } + + [TestMethod] + public void TestSpanAndSerializeLittleEndian() + { + var data = RandomNumberFactory.NextBytes(UInt256.Length); + + var value = new UInt256(data); + var spanLittleEndian = value.GetSpanLittleEndian(); + CollectionAssert.AreEqual(data, spanLittleEndian.ToArray()); + + // Check that Serialize LittleEndian and Serialize BigEndian are equals + var dataLittleEndian = new byte[UInt256.Length]; + value.SafeSerialize(dataLittleEndian.AsSpan()); + CollectionAssert.AreEqual(value.ToArray(), dataLittleEndian); + + // Check that Serialize LittleEndian and Serialize BigEndian are equals + var dataSerialized = new byte[UInt256.Length]; + value.Serialize(dataSerialized.AsSpan()); + CollectionAssert.AreEqual(value.ToArray(), dataSerialized); + + var shortBuffer = new byte[UInt256.Length - 1]; + Assert.ThrowsExactly(() => value.Serialize(shortBuffer.AsSpan())); + Assert.ThrowsExactly(() => value.SafeSerialize(shortBuffer.AsSpan())); + } +} diff --git a/tests/Neo.UnitTests/VM/UT_Helper.cs b/tests/Neo.UnitTests/VM/UT_Helper.cs new file mode 100644 index 0000000000..3b6afac9d3 --- /dev/null +++ b/tests/Neo.UnitTests/VM/UT_Helper.cs @@ -0,0 +1,713 @@ +// Copyright (C) 2015-2025 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 Neo.Cryptography.ECC; +using Neo.Extensions; +using Neo.Extensions.IO; +using Neo.Extensions.SmartContract; +using Neo.Extensions.VM; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using Neo.VM.Types; +using System.Numerics; +using System.Text; +using System.Text.RegularExpressions; +using Array = System.Array; + +namespace Neo.UnitTests.VM; + +[TestClass] +public class UT_Helper +{ + private DataCache _snapshotCache = null!; + + [TestInitialize] + public void TestSetup() + { + _snapshotCache = TestBlockchain.GetTestSnapshotCache(); + } + + [TestMethod] + public void TestEmit() + { + ScriptBuilder sb = new(); + sb.Emit([OpCode.PUSH0]); + CollectionAssert.AreEqual(new[] { (byte)OpCode.PUSH0 }, sb.ToArray()); + } + + [TestMethod] + public void TestToJson() + { + var item = new Neo.VM.Types.Array + { + 5, + "hello world", + new byte[] { 1, 2, 3 }, + true + }; + + Assert.AreEqual("{\"type\":\"Integer\",\"value\":\"5\"}", item[0].ToJson().ToString()); + Assert.AreEqual("{\"type\":\"ByteString\",\"value\":\"aGVsbG8gd29ybGQ=\"}", item[1].ToJson().ToString()); + Assert.AreEqual("{\"type\":\"ByteString\",\"value\":\"AQID\"}", item[2].ToJson().ToString()); + Assert.AreEqual("{\"type\":\"Boolean\",\"value\":true}", item[3].ToJson().ToString()); + Assert.AreEqual("{\"type\":\"Array\",\"value\":[{\"type\":\"Integer\",\"value\":\"5\"},{\"type\":\"ByteString\",\"value\":\"aGVsbG8gd29ybGQ=\"},{\"type\":\"ByteString\",\"value\":\"AQID\"},{\"type\":\"Boolean\",\"value\":true}]}", item.ToJson().ToString()); + + var item2 = new Map(); + item2[1] = new Pointer(new Script(ReadOnlyMemory.Empty), 0); + + Assert.AreEqual("{\"type\":\"Map\",\"value\":[{\"key\":{\"type\":\"Integer\",\"value\":\"1\"},\"value\":{\"type\":\"Pointer\",\"value\":0}}]}", item2.ToJson().ToString()); + } + + [TestMethod] + public void TestEmitAppCall1() + { + ScriptBuilder sb = new(); + sb.EmitDynamicCall(UInt160.Zero, "AAAAA"); + byte[] tempArray = new byte[36]; + tempArray[0] = (byte)OpCode.NEWARRAY0; + tempArray[1] = (byte)OpCode.PUSH15;//(byte)CallFlags.All; + tempArray[2] = (byte)OpCode.PUSHDATA1; + tempArray[3] = 5;//operation.Length + Array.Copy(Encoding.UTF8.GetBytes("AAAAA"), 0, tempArray, 4, 5);//operation.data + tempArray[9] = (byte)OpCode.PUSHDATA1; + tempArray[10] = 0x14;//scriptHash.Length + Array.Copy(UInt160.Zero.ToArray(), 0, tempArray, 11, 20);//operation.data + uint api = ApplicationEngine.System_Contract_Call; + tempArray[31] = (byte)OpCode.SYSCALL; + Array.Copy(BitConverter.GetBytes(api), 0, tempArray, 32, 4);//api.data + Assert.AreEqual(tempArray.ToHexString(), sb.ToArray().ToHexString()); + } + + [TestMethod] + public void TestEmitArray() + { + var snapshot = _snapshotCache.CloneCache(); + var expected = new BigInteger[] { 1, 2, 3 }; + var sb = new ScriptBuilder(); + sb.CreateArray(expected); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + engine.LoadScript(sb.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + + CollectionAssert.AreEqual(expected, engine.ResultStack.Pop().Select(u => u.GetInteger()).ToArray()); + + expected = []; + sb = new ScriptBuilder(); + sb.CreateArray(expected); + + using var engine2 = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + engine2.LoadScript(sb.ToArray()); + Assert.AreEqual(VMState.HALT, engine2.Execute()); + + Assert.IsEmpty(engine2.ResultStack.Pop()); + } + + [TestMethod] + public void TestEmitStruct() + { + var snapshot = _snapshotCache.CloneCache(); + var expected = new BigInteger[] { 1, 2, 3 }; + var sb = new ScriptBuilder(); + sb.CreateStruct(expected); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + engine.LoadScript(sb.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + + CollectionAssert.AreEqual(expected, engine.ResultStack.Pop().Select(u => u.GetInteger()).ToArray()); + + expected = []; + sb = new ScriptBuilder(); + sb.CreateStruct(expected); + + using var engine2 = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + engine2.LoadScript(sb.ToArray()); + Assert.AreEqual(VMState.HALT, engine2.Execute()); + + Assert.IsEmpty(engine2.ResultStack.Pop()); + } + + [TestMethod] + public void TestEmitMap() + { + var snapshot = _snapshotCache.CloneCache(); + var expected = new Dictionary() { { 1, 2 }, { 3, 4 } }; + var sb = new ScriptBuilder(); + sb.CreateMap(expected); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot); + engine.LoadScript(sb.ToArray()); + Assert.AreEqual(VMState.HALT, engine.Execute()); + + var map = engine.ResultStack.Pop(); + var dic = map.ToDictionary(u => u.Key, u => u.Value); + + CollectionAssert.AreEqual(expected.Keys, dic.Keys.Select(u => u.GetInteger()).ToArray()); + CollectionAssert.AreEqual(expected.Values, dic.Values.Select(u => u.GetInteger()).ToArray()); + } + + [TestMethod] + public void TestEmitAppCall2() + { + ScriptBuilder sb = new(); + sb.EmitDynamicCall(UInt160.Zero, "AAAAA", [new ContractParameter(ContractParameterType.Integer)]); + byte[] tempArray = new byte[38]; + tempArray[0] = (byte)OpCode.PUSH0; + tempArray[1] = (byte)OpCode.PUSH1; + tempArray[2] = (byte)OpCode.PACK; + tempArray[3] = (byte)OpCode.PUSH15;//(byte)CallFlags.All; + tempArray[4] = (byte)OpCode.PUSHDATA1; + tempArray[5] = 0x05;//operation.Length + Array.Copy(Encoding.UTF8.GetBytes("AAAAA"), 0, tempArray, 6, 5);//operation.data + tempArray[11] = (byte)OpCode.PUSHDATA1; + tempArray[12] = 0x14;//scriptHash.Length + Array.Copy(UInt160.Zero.ToArray(), 0, tempArray, 13, 20);//operation.data + uint api = ApplicationEngine.System_Contract_Call; + tempArray[33] = (byte)OpCode.SYSCALL; + Array.Copy(BitConverter.GetBytes(api), 0, tempArray, 34, 4);//api.data + Assert.AreEqual(tempArray.ToHexString(), sb.ToArray().ToHexString()); + } + + [TestMethod] + public void TestEmitAppCall3() + { + ScriptBuilder sb = new(); + sb.EmitDynamicCall(UInt160.Zero, "AAAAA", true); + byte[] tempArray = new byte[38]; + tempArray[0] = (byte)OpCode.PUSHT; + tempArray[1] = (byte)OpCode.PUSH1;//arg.Length + tempArray[2] = (byte)OpCode.PACK; + tempArray[3] = (byte)OpCode.PUSH15;//(byte)CallFlags.All; + tempArray[4] = (byte)OpCode.PUSHDATA1; + tempArray[5] = 0x05;//operation.Length + Array.Copy(Encoding.UTF8.GetBytes("AAAAA"), 0, tempArray, 6, 5);//operation.data + tempArray[11] = (byte)OpCode.PUSHDATA1; + tempArray[12] = 0x14;//scriptHash.Length + Array.Copy(UInt160.Zero.ToArray(), 0, tempArray, 13, 20);//operation.data + uint api = ApplicationEngine.System_Contract_Call; + tempArray[33] = (byte)OpCode.SYSCALL; + Array.Copy(BitConverter.GetBytes(api), 0, tempArray, 34, 4);//api.data + Assert.AreEqual(tempArray.ToHexString(), sb.ToArray().ToHexString()); + } + + [TestMethod] + public void TestMakeScript() + { + byte[] testScript = NativeContract.GAS.Hash.MakeScript("balanceOf", UInt160.Zero); + + Assert.AreEqual("0c14000000000000000000000000000000000000000011c01f0c0962616c616e63654f660c14cf76e28bd0062c4a478ee35561011319f3cfa4d241627d5b52", + testScript.ToHexString()); + } + + [TestMethod] + public void TestToParameter() + { + StackItem byteItem = "00e057eb481b".HexToBytes(); + Assert.AreEqual(30000000000000L, (long)new BigInteger((byte[])byteItem.ToParameter().Value!)); + + StackItem boolItem = false; + Assert.IsFalse((bool)boolItem.ToParameter().Value!); + + StackItem intItem = new BigInteger(1000); + Assert.AreEqual(1000, (BigInteger)intItem.ToParameter().Value!); + + StackItem interopItem = new InteropInterface("test"); + Assert.AreEqual(ContractParameterType.InteropInterface, interopItem.ToParameter().Type); + + StackItem arrayItem = new Neo.VM.Types.Array([byteItem, boolItem, intItem, interopItem]); + Assert.AreEqual(1000, (BigInteger)((List)arrayItem.ToParameter().Value!)[2].Value!); + + StackItem mapItem = new Map { [(PrimitiveType)byteItem] = intItem }; + Assert.AreEqual(1000, (BigInteger)((List>)mapItem.ToParameter().Value!)[0].Value.Value!); + } + + [TestMethod] + public void TestToStackItem() + { + var byteParameter = new ContractParameter { Type = ContractParameterType.ByteArray, Value = "00e057eb481b".HexToBytes() }; + Assert.AreEqual(30000000000000L, (long)byteParameter.ToStackItem().GetInteger()); + + var boolParameter = new ContractParameter { Type = ContractParameterType.Boolean, Value = false }; + Assert.IsFalse(boolParameter.ToStackItem().GetBoolean()); + + var intParameter = new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(1000) }; + Assert.AreEqual(1000, intParameter.ToStackItem().GetInteger()); + + var h160Parameter = new ContractParameter { Type = ContractParameterType.Hash160, Value = UInt160.Zero }; + Assert.AreEqual(0, h160Parameter.ToStackItem().GetInteger()); + + var h256Parameter = new ContractParameter { Type = ContractParameterType.Hash256, Value = UInt256.Zero }; + Assert.AreEqual(0, h256Parameter.ToStackItem().GetInteger()); + + var pkParameter = new ContractParameter + { + Type = ContractParameterType.PublicKey, + Value = ECPoint.Parse("02f9ec1fd0a98796cf75b586772a4ddd41a0af07a1dbdf86a7238f74fb72503575", ECCurve.Secp256r1) + }; + Assert.IsInstanceOfType(pkParameter.ToStackItem()); + Assert.AreEqual("02f9ec1fd0a98796cf75b586772a4ddd41a0af07a1dbdf86a7238f74fb72503575", pkParameter.ToStackItem().GetSpan().ToHexString()); + + var strParameter = new ContractParameter { Type = ContractParameterType.String, Value = "test😂👍" }; + Assert.AreEqual("test😂👍", strParameter.ToStackItem().GetString()); + + var interopParameter = new ContractParameter { Type = ContractParameterType.InteropInterface, Value = new object() }; + Assert.ThrowsExactly(() => _ = interopParameter.ToStackItem()); + + var interopParameter2 = new ContractParameter { Type = ContractParameterType.InteropInterface }; + Assert.AreEqual(StackItem.Null, interopParameter2.ToStackItem()); + + var arrayParameter = new ContractParameter + { + Type = ContractParameterType.Array, + Value = new[] { byteParameter, boolParameter, intParameter, h160Parameter, h256Parameter, pkParameter, strParameter }.ToList() + }; + Assert.AreEqual(1000, ((Neo.VM.Types.Array)arrayParameter.ToStackItem())[2].GetInteger()); + + var mapParameter = new ContractParameter + { + Type = ContractParameterType.Map, + Value = new[] { new KeyValuePair(byteParameter, pkParameter) } + }; + Assert.AreEqual(30000000000000L, (long)((Map)mapParameter.ToStackItem()).Keys.First().GetInteger()); + } + + [TestMethod] + public void TestEmitPush1() + { + ScriptBuilder sb = new(); + sb.EmitPush(UInt160.Zero); + byte[] tempArray = new byte[22]; + tempArray[0] = (byte)OpCode.PUSHDATA1; + tempArray[1] = 0x14; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + [TestMethod] + public void TestEmitPush2() + { + TestEmitPush2Signature(); + TestEmitPush2ByteArray(); + TestEmitPush2Boolean(); + TestEmitPush2Integer(); + TestEmitPush2BigInteger(); + TestEmitPush2Hash160(); + TestEmitPush2Hash256(); + TestEmitPush2PublicKey(); + TestEmitPush2String(); + TestEmitPush2Array(); + TestEmitPush2Map(); + } + + private static void TestEmitPush2Map() + { + ScriptBuilder sb = new(); + sb.EmitPush(new ContractParameter(ContractParameterType.Map)); + CollectionAssert.AreEqual(new[] { (byte)OpCode.NEWMAP }, sb.ToArray()); + } + + private static void TestEmitPush2Array() + { + ScriptBuilder sb = new(); + ContractParameter parameter = new(ContractParameterType.Array); + IList values = [ + new(ContractParameterType.Integer), + new(ContractParameterType.Integer) + ]; + parameter.Value = values; + sb.EmitPush(parameter); + byte[] tempArray = + [ + (byte)OpCode.PUSH0, + (byte)OpCode.PUSH0, + (byte)OpCode.PUSH2, + (byte)OpCode.PACK, + ]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush2String() + { + ScriptBuilder sb = new(); + sb.EmitPush(new ContractParameter(ContractParameterType.String)); + byte[] tempArray = [(byte)OpCode.PUSHDATA1, 0x00]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush2PublicKey() + { + ScriptBuilder sb = new(); + sb.EmitPush(new ContractParameter(ContractParameterType.PublicKey)); + byte[] tempArray = new byte[35]; + tempArray[0] = (byte)OpCode.PUSHDATA1; + tempArray[1] = 0x21; + Array.Copy(ECCurve.Secp256r1.G.EncodePoint(true), 0, tempArray, 2, 33); + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush2Hash256() + { + ScriptBuilder sb = new(); + sb.EmitPush(new ContractParameter(ContractParameterType.Hash256)); + byte[] tempArray = new byte[34]; + tempArray[0] = (byte)OpCode.PUSHDATA1; + tempArray[1] = 0x20; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush2Hash160() + { + ScriptBuilder sb = new(); + sb.EmitPush(new ContractParameter(ContractParameterType.Hash160)); + byte[] tempArray = new byte[22]; + tempArray[0] = (byte)OpCode.PUSHDATA1; + tempArray[1] = 0x14; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush2BigInteger() + { + ScriptBuilder sb = new(); + ContractParameter parameter = new(ContractParameterType.Integer) + { + Value = BigInteger.Zero + }; + sb.EmitPush(parameter); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush2Integer() + { + ScriptBuilder sb = new(); + ContractParameter parameter = new(ContractParameterType.Integer); + sb.EmitPush(parameter); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush2Boolean() + { + ScriptBuilder sb = new(); + sb.EmitPush(new ContractParameter(ContractParameterType.Boolean)); + byte[] tempArray = [(byte)OpCode.PUSHF]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush2ByteArray() + { + ScriptBuilder sb = new(); + sb.EmitPush(new ContractParameter(ContractParameterType.ByteArray)); + byte[] tempArray = [(byte)OpCode.PUSHDATA1, 0x00]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush2Signature() + { + ScriptBuilder sb = new(); + sb.EmitPush(new ContractParameter(ContractParameterType.Signature)); + byte[] tempArray = new byte[66]; + tempArray[0] = (byte)OpCode.PUSHDATA1; + tempArray[1] = 0x40; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + enum TestEnum : byte + { + case1 = 0 + } + + [TestMethod] + public void TestEmitPush3() + { + TestEmitPush3Bool(); + TestEmitPush3ByteArray(); + TestEmitPush3String(); + TestEmitPush3BigInteger(); + TestEmitPush3ISerializable(); + TestEmitPush3Sbyte(); + TestEmitPush3Byte(); + TestEmitPush3Short(); + TestEmitPush3Ushort(); + TestEmitPush3Char(); + TestEmitPush3Int(); + TestEmitPush3Uint(); + TestEmitPush3Long(); + TestEmitPush3Ulong(); + TestEmitPush3Enum(); + + ScriptBuilder sb = new(); + Assert.ThrowsExactly(() => sb.EmitPush(new object())); + } + + + private static void TestEmitPush3Enum() + { + ScriptBuilder sb = new(); + sb.EmitPush(TestEnum.case1); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Ulong() + { + ScriptBuilder sb = new(); + ulong temp = 0; + sb.EmitPush(temp); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Long() + { + ScriptBuilder sb = new(); + long temp = 0; + sb.EmitPush(temp); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Uint() + { + ScriptBuilder sb = new(); + uint temp = 0; + sb.EmitPush(temp); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Int() + { + ScriptBuilder sb = new(); + int temp = 0; + sb.EmitPush(temp); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Ushort() + { + ScriptBuilder sb = new(); + ushort temp = 0; + sb.EmitPush(temp); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Char() + { + ScriptBuilder sb = new(); + char temp = char.MinValue; + sb.EmitPush(temp); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Short() + { + ScriptBuilder sb = new(); + short temp = 0; + sb.EmitPush(temp); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Byte() + { + ScriptBuilder sb = new(); + byte temp = 0; + sb.EmitPush(temp); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Sbyte() + { + ScriptBuilder sb = new(); + sbyte temp = 0; + sb.EmitPush(temp); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3ISerializable() + { + ScriptBuilder sb = new(); + sb.EmitPush(UInt160.Zero); + var tempArray = new byte[22]; + tempArray[0] = (byte)OpCode.PUSHDATA1; + tempArray[1] = 0x14; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3BigInteger() + { + ScriptBuilder sb = new(); + sb.EmitPush(BigInteger.Zero); + byte[] tempArray = [(byte)OpCode.PUSH0]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3String() + { + ScriptBuilder sb = new(); + sb.EmitPush(""); + byte[] tempArray = [(byte)OpCode.PUSHDATA1, 0x00]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3ByteArray() + { + ScriptBuilder sb = new(); + sb.EmitPush([0x01]); + byte[] tempArray = [(byte)OpCode.PUSHDATA1, 0x01, 0x01]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + private static void TestEmitPush3Bool() + { + ScriptBuilder sb = new(); + sb.EmitPush(true); + byte[] tempArray = [(byte)OpCode.PUSHT]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + [TestMethod] + public void TestEmitSysCall() + { + ScriptBuilder sb = new(); + sb.EmitSysCall(0, true); + byte[] tempArray = + [ + (byte)OpCode.PUSHT, + (byte)OpCode.SYSCALL, + 0x00, + 0x00, + 0x00, + 0x00, + ]; + CollectionAssert.AreEqual(tempArray, sb.ToArray()); + } + + [TestMethod] + public void TestToParameter2() + { + TestToParaMeter2VMArray(); + TestToParameter2Map(); + TestToParameter2VMBoolean(); + TestToParameter2ByteArray(); + TestToParameter2Integer(); + TestToParameter2InteropInterface(); + } + + private static void TestToParameter2InteropInterface() + { + StackItem item = new InteropInterface(new object()); + ContractParameter parameter = item.ToParameter(); + Assert.AreEqual(ContractParameterType.InteropInterface, parameter.Type); + } + + private static void TestToParameter2Integer() + { + StackItem item = new Integer(0); + ContractParameter parameter = item.ToParameter(); + Assert.AreEqual(ContractParameterType.Integer, parameter.Type); + Assert.AreEqual(BigInteger.Zero, parameter.Value); + } + + private static void TestToParameter2ByteArray() + { + StackItem item = new ByteString(new byte[] { 0x00 }); + ContractParameter parameter = item.ToParameter(); + Assert.AreEqual(ContractParameterType.ByteArray, parameter.Type); + Assert.AreEqual(Encoding.Default.GetString([0x00]), Encoding.Default.GetString((byte[])parameter.Value!)); + } + + private static void TestToParameter2VMBoolean() + { + StackItem item = StackItem.True; + ContractParameter parameter = item.ToParameter(); + Assert.AreEqual(ContractParameterType.Boolean, parameter.Type); + Assert.IsTrue((bool?)parameter.Value); + } + + private static void TestToParameter2Map() + { + StackItem item = new Map(); + ContractParameter parameter = item.ToParameter(); + Assert.AreEqual(ContractParameterType.Map, parameter.Type); + Assert.IsEmpty((List>)parameter.Value!); + } + + private static void TestToParaMeter2VMArray() + { + Neo.VM.Types.Array item = new(); + ContractParameter parameter = item.ToParameter(); + Assert.AreEqual(ContractParameterType.Array, parameter.Type); + Assert.IsEmpty((List)parameter.Value!); + } + + [TestMethod] + public void TestCharAsUInt16() + { + // test every char in a loop + for (int i = ushort.MinValue; i < char.MinValue; i++) + { + var c = Convert.ToChar(i); + Assert.AreEqual(i, c); + } + + for (int i = ushort.MinValue; i < ushort.MaxValue; i++) + { + using var sbUInt16 = new ScriptBuilder(); + using var sbChar = new ScriptBuilder(); + sbUInt16.EmitPush((ushort)i); + sbChar.EmitPush(Convert.ToChar(i)); + CollectionAssert.AreEqual(sbUInt16.ToArray(), sbChar.ToArray()); + } + } + + [TestMethod] + public void TestCyclicReference() + { + var map = new Map { [1] = 2 }; + var item = new Neo.VM.Types.Array { map, map }; + + // just check there is no exception + var expected = """ + { + "type":"Array", + "value":[ + { + "type":"Map", + "value":[{ + "key":{"type":"Integer","value":"1"}, + "value":{"type":"Integer","value":"2"} + }] + },{ + "type":"Map", + "value":[{ + "key":{"type":"Integer","value":"1"}, + "value":{"type":"Integer","value":"2"} + }] + }] + } + """; + + var json = item.ToJson(); + Assert.AreEqual(Regex.Replace(expected, @"\s+", ""), json.ToString()); + // check cyclic reference + map[2] = item; + Assert.ThrowsExactly(() => item.ToJson()); + } +} diff --git a/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Account.cs b/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Account.cs new file mode 100644 index 0000000000..4b05f79ad5 --- /dev/null +++ b/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Account.cs @@ -0,0 +1,165 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NEP6Account.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Json; +using Neo.SmartContract; +using Neo.Wallets; +using Neo.Wallets.NEP6; + +namespace Neo.UnitTests.Wallets.NEP6; + +[TestClass] +public class UT_NEP6Account +{ + NEP6Account _account = null!; + UInt160 _hash = null!; + NEP6Wallet _wallet = null!; + private static string _nep2 = null!; + private static KeyPair _keyPair = null!; + + [ClassInitialize] + public static void ClassSetup(TestContext ctx) + { + byte[] privateKey = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + _keyPair = new KeyPair(privateKey); + _nep2 = _keyPair.Export("Satoshi", TestProtocolSettings.Default.AddressVersion, 2, 1, 1); + } + + [TestInitialize] + public void TestSetup() + { + _wallet = TestUtils.GenerateTestWallet("Satoshi"); + byte[] array1 = { 0x01 }; + _hash = new UInt160(Crypto.Hash160(array1)); + _account = new NEP6Account(_wallet, _hash); + } + + [TestMethod] + public void TestChangePassword() + { + _account = new NEP6Account(_wallet, _hash, _nep2); + Assert.IsTrue(_account.ChangePasswordPrepare("b", "Satoshi")); + _account.ChangePasswordCommit(); + _account.Contract = new Contract(); + Assert.IsFalse(_account.ChangePasswordPrepare("b", "Satoshi")); + Assert.IsTrue(_account.ChangePasswordPrepare("Satoshi", "b")); + _account.ChangePasswordCommit(); + Assert.IsTrue(_account.VerifyPassword("b")); + Assert.IsTrue(_account.ChangePasswordPrepare("b", "Satoshi")); + _account.ChangePasswordCommit(); + Assert.IsTrue(_account.ChangePasswordPrepare("Satoshi", "b")); + _account.ChangePasswordRollback(); + Assert.IsTrue(_account.VerifyPassword("Satoshi")); + } + + [TestMethod] + public void TestConstructorWithNep2Key() + { + Assert.AreEqual(_hash, _account.ScriptHash); + Assert.IsTrue(_account.Decrypted); + Assert.IsFalse(_account.HasKey); + } + + [TestMethod] + public void TestConstructorWithKeyPair() + { + string password = "hello world"; + var wallet = TestUtils.GenerateTestWallet(password); + byte[] array1 = { 0x01 }; + var hash = new UInt160(Crypto.Hash160(array1)); + NEP6Account account = new(wallet, hash, _keyPair, password); + Assert.AreEqual(hash, account.ScriptHash); + Assert.IsTrue(account.Decrypted); + Assert.IsTrue(account.HasKey); + } + + [TestMethod] + public void TestFromJson() + { + JObject json = new(); + json["address"] = "NdtB8RXRmJ7Nhw1FPTm7E6HoDZGnDw37nf"; + json["key"] = null; + json["label"] = null; + json["isDefault"] = true; + json["lock"] = false; + json["contract"] = null; + json["extra"] = null; + NEP6Account account = NEP6Account.FromJson(json, _wallet); + Assert.AreEqual("NdtB8RXRmJ7Nhw1FPTm7E6HoDZGnDw37nf".ToScriptHash(TestProtocolSettings.Default.AddressVersion), account.ScriptHash); + Assert.IsNull(account.Label); + Assert.IsTrue(account.IsDefault); + Assert.IsFalse(account.Lock); + Assert.IsNull(account.Contract); + Assert.IsNull(account.Extra); + Assert.IsNull(account.GetKey()); + + json["key"] = "6PYRjVE1gAbCRyv81FTiFz62cxuPGw91vMjN4yPa68bnoqJtioreTznezn"; + json["label"] = "label"; + account = NEP6Account.FromJson(json, _wallet); + Assert.AreEqual("label", account.Label); + Assert.IsTrue(account.HasKey); + } + + [TestMethod] + public void TestGetKey() + { + Assert.IsNull(_account.GetKey()); + _account = new NEP6Account(_wallet, _hash, _nep2); + Assert.AreEqual(_keyPair, _account.GetKey()); + } + + [TestMethod] + public void TestGetKeyWithString() + { + Assert.IsNull(_account.GetKey("Satoshi")); + _account = new NEP6Account(_wallet, _hash, _nep2); + Assert.AreEqual(_keyPair, _account.GetKey("Satoshi")); + } + + [TestMethod] + public void TestToJson() + { + JObject nep6contract = new(); + nep6contract["script"] = "IQNgPziA63rqCtRQCJOSXkpC/qSKRO5viYoQs8fOBdKiZ6w="; + JObject parameters = new(); + parameters["type"] = 0x00; + parameters["name"] = "Sig"; + JArray array = new() + { + parameters + }; + nep6contract["parameters"] = array; + nep6contract["deployed"] = false; + _account.Contract = NEP6Contract.FromJson(nep6contract); + JObject json = _account.ToJson(); + Assert.AreEqual("NdtB8RXRmJ7Nhw1FPTm7E6HoDZGnDw37nf", json["address"]!.AsString()); + Assert.IsNull(json["label"]); + Assert.AreEqual("false", json["isDefault"]!.ToString()); + Assert.AreEqual("false", json["lock"]!.ToString()); + Assert.IsNull(json["key"]); + Assert.AreEqual(@"""IQNgPziA63rqCtRQCJOSXkpC/qSKRO5viYoQs8fOBdKiZ6w=""", json["contract"]!["script"]!.ToString()); + Assert.IsNull(json["extra"]); + + _account.Contract = null; + json = _account.ToJson(); + Assert.IsNull(json["contract"]); + } + + [TestMethod] + public void TestVerifyPassword() + { + _account = new NEP6Account(_wallet, _hash, _nep2); + Assert.IsTrue(_account.VerifyPassword("Satoshi")); + Assert.IsFalse(_account.VerifyPassword("b")); + } +} diff --git a/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Contract.cs b/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Contract.cs new file mode 100644 index 0000000000..4616180935 --- /dev/null +++ b/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Contract.cs @@ -0,0 +1,75 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NEP6Contract.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Wallets.NEP6; + +namespace Neo.UnitTests.Wallets.NEP6; + +[TestClass] +public class UT_NEP6Contract +{ + [TestMethod] + public void TestFromNullJson() + { + NEP6Contract? nep6Contract = NEP6Contract.FromJson(null); + Assert.IsNull(nep6Contract); + } + + [TestMethod] + public void TestFromJson() + { + string json = "{\"script\":\"IQPviR30wLfu+5N9IeoPuIzejg2Cp/8RhytecEeWna+062h0dHaq\"," + + "\"parameters\":[{\"name\":\"signature\",\"type\":\"Signature\"}],\"deployed\":false}"; + JObject @object = (JObject)JToken.Parse(json)!; + + NEP6Contract nep6Contract = NEP6Contract.FromJson(@object)!; + CollectionAssert.AreEqual("2103ef891df4c0b7eefb937d21ea0fb88cde8e0d82a7ff11872b5e7047969dafb4eb68747476aa".HexToBytes(), nep6Contract.Script); + Assert.HasCount(1, nep6Contract.ParameterList); + Assert.AreEqual(ContractParameterType.Signature, nep6Contract.ParameterList[0]); + Assert.HasCount(1, nep6Contract.ParameterNames); + Assert.AreEqual("signature", nep6Contract.ParameterNames[0]); + Assert.IsFalse(nep6Contract.Deployed); + } + + [TestMethod] + public void TestToJson() + { + NEP6Contract nep6Contract = new() + { + Script = new byte[] { 0x00, 0x01 }, + ParameterList = new ContractParameterType[] { ContractParameterType.Boolean, ContractParameterType.Integer }, + ParameterNames = new string[] { "param1", "param2" }, + Deployed = false + }; + + JObject @object = nep6Contract.ToJson(); + JString jString = (JString)@object["script"]!; + Assert.AreEqual(Convert.ToBase64String(nep6Contract.Script, Base64FormattingOptions.None), jString.Value); + + JBoolean jBoolean = (JBoolean)@object["deployed"]!; + Assert.IsFalse(jBoolean.Value); + + JArray parameters = (JArray)@object["parameters"]!; + Assert.HasCount(2, parameters); + + jString = (JString)parameters[0]!["name"]!; + Assert.AreEqual("param1", jString.Value); + jString = (JString)parameters[0]!["type"]!; + Assert.AreEqual(ContractParameterType.Boolean.ToString(), jString.Value); + + jString = (JString)parameters[1]!["name"]!; + Assert.AreEqual("param2", jString.Value); + jString = (JString)parameters[1]!["type"]!; + Assert.AreEqual(ContractParameterType.Integer.ToString(), jString.Value); + } +} diff --git a/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Wallet.cs b/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Wallet.cs new file mode 100644 index 0000000000..33f6ca4ff9 --- /dev/null +++ b/tests/Neo.UnitTests/Wallets/NEP6/UT_NEP6Wallet.cs @@ -0,0 +1,483 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_NEP6Wallet.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using Neo.Json; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.Wallets; +using Neo.Wallets.NEP6; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Contract = Neo.SmartContract.Contract; + +namespace Neo.UnitTests.Wallets.NEP6; + +[TestClass] +public class UT_NEP6Wallet +{ + private NEP6Wallet uut = null!; + private string wPath = null!; + private static KeyPair keyPair = null!; + private static string nep2key = null!; + private static UInt160 testScriptHash = null!; + private string rootPath = null!; + + public static string GetRandomPath(string? ext = null) + { + var rnd = RandomNumberFactory.NextUInt32(1000000); + var threadName = Environment.CurrentManagedThreadId.ToString(); + return Path.GetFullPath($"Wallet_{rnd:X8}{threadName}{ext}"); + } + + [ClassInitialize] + public static void ClassInit(TestContext context) + { + var privateKey = new byte[32]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(privateKey); + } + keyPair = new KeyPair(privateKey); + testScriptHash = Contract.CreateSignatureContract(keyPair.PublicKey).ScriptHash; + nep2key = keyPair.Export("123", TestProtocolSettings.Default.AddressVersion, 2, 1, 1); + } + + private string CreateWalletFile() + { + rootPath = GetRandomPath(); + if (!Directory.Exists(rootPath)) Directory.CreateDirectory(rootPath); + + var path = Path.Combine(rootPath, "wallet.json"); + File.WriteAllText(path, "{\"name\":\"name\",\"version\":\"1.0\",\"scrypt\":{\"n\":2,\"r\":1,\"p\":1},\"accounts\":[],\"extra\":{}}"); + return path; + } + + [TestInitialize] + public void TestSetup() + { + uut = TestUtils.GenerateTestWallet("123"); + wPath = CreateWalletFile(); + } + + [TestCleanup] + public void TestCleanUp() + { + if (File.Exists(wPath)) File.Delete(wPath); + if (Directory.Exists(rootPath)) Directory.Delete(rootPath); + } + + [TestMethod] + public void TestCreateAccount() + { + var acc = uut.CreateAccount("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632549".HexToBytes()); + var tx = new Transaction() + { + Attributes = [], + Script = new byte[1], + Signers = [new Signer() { Account = acc.ScriptHash }], + Witnesses = [] + }; + var ctx = new ContractParametersContext(TestBlockchain.GetTestSnapshotCache(), tx, TestProtocolSettings.Default.Network); + Assert.IsTrue(uut.Sign(ctx)); + tx.Witnesses = ctx.GetWitnesses(); + Assert.IsTrue(tx.VerifyWitnesses(TestProtocolSettings.Default, TestBlockchain.GetTestSnapshotCache(), long.MaxValue)); + Assert.ThrowsExactly(() => _ = uut.CreateAccount("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551".HexToBytes())); + } + + [TestMethod] + public void TestChangePassword() + { + var wallet = new JObject() + { + ["name"] = "name", + ["version"] = new Version("1.0").ToString(), + ["scrypt"] = new ScryptParameters(2, 1, 1).ToJson(), + ["accounts"] = new JArray(), + ["extra"] = new JObject() + }; + File.WriteAllText(wPath, wallet.ToString()); + + uut = new NEP6Wallet(wPath, "123", TestProtocolSettings.Default); + uut.CreateAccount(keyPair.PrivateKey); + Assert.IsFalse(uut.ChangePassword("456", "123")); + Assert.IsTrue(uut.ChangePassword("123", "456")); + Assert.IsTrue(uut.VerifyPassword("456")); + Assert.IsTrue(uut.ChangePassword("456", "123")); + } + + [TestMethod] + public void TestConstructorWithPathAndName() + { + var wallet = new NEP6Wallet(wPath, "123", TestProtocolSettings.Default); + Assert.AreEqual("name", wallet.Name); + Assert.AreEqual(new ScryptParameters(2, 1, 1).ToJson().ToString(), wallet.Scrypt.ToJson().ToString()); + Assert.AreEqual(new Version("1.0").ToString(), wallet.Version.ToString()); + + wallet = new NEP6Wallet("", "123", TestProtocolSettings.Default, "test"); + Assert.AreEqual("test", wallet.Name); + Assert.AreEqual(ScryptParameters.Default.ToJson().ToString(), wallet.Scrypt.ToJson().ToString()); + Assert.AreEqual(Version.Parse("1.0"), wallet.Version); + + wallet = new NEP6Wallet("wallet.json", "123", TestProtocolSettings.Default, ""); + Assert.AreEqual("wallet", wallet.Name); + } + + [TestMethod] + public void TestConstructorWithJObject() + { + JObject wallet = new(); + wallet["name"] = "test"; + wallet["version"] = Version.Parse("1.0").ToString(); + wallet["scrypt"] = ScryptParameters.Default.ToJson(); + wallet["accounts"] = new JArray(); + wallet["extra"] = new JObject(); + Assert.AreEqual( + "{\"name\":\"test\",\"version\":\"1.0\",\"scrypt\":{\"n\":16384,\"r\":8,\"p\":8},\"accounts\":[],\"extra\":{}}", + wallet.ToString()); + + var w = new NEP6Wallet(null!, "123", TestProtocolSettings.Default, wallet); + Assert.AreEqual("test", w.Name); + Assert.AreEqual(Version.Parse("1.0").ToString(), w.Version.ToString()); + } + + [TestMethod] + public void TestGetName() + { + Assert.AreEqual("noname", uut.Name); + } + + [TestMethod] + public void TestGetVersion() + { + Assert.AreEqual(new Version("1.0").ToString(), uut.Version.ToString()); + } + + [TestMethod] + public void TestContains() + { + var result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + + uut.CreateAccount(testScriptHash); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + } + + [TestMethod] + public void TestAddCount() + { + uut.CreateAccount(testScriptHash); + Assert.IsTrue(uut.Contains(testScriptHash)); + + var account = uut.GetAccount(testScriptHash)!; + Assert.IsTrue(account.WatchOnly); + Assert.IsFalse(account.HasKey); + + uut.CreateAccount(keyPair.PrivateKey); + account = uut.GetAccount(testScriptHash)!; + Assert.IsFalse(account.WatchOnly); + Assert.IsTrue(account.HasKey); + + uut.CreateAccount(testScriptHash); + account = uut.GetAccount(testScriptHash)!; + Assert.IsFalse(account.WatchOnly); + Assert.IsFalse(account.HasKey); + + uut.CreateAccount(keyPair.PrivateKey); + account = uut.GetAccount(testScriptHash)!; + Assert.IsFalse(account.WatchOnly); + Assert.IsTrue(account.HasKey); + } + + [TestMethod] + public void TestCreateAccountWithPrivateKey() + { + var result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + uut.CreateAccount(keyPair.PrivateKey); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + } + + [TestMethod] + public void TestCreateAccountWithKeyPair() + { + var contract = Contract.CreateSignatureContract(keyPair.PublicKey); + var result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + uut.CreateAccount(contract); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + uut.DeleteAccount(testScriptHash); + result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + uut.CreateAccount(contract, keyPair); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + } + + [TestMethod] + public void TestCreateAccountWithScriptHash() + { + bool result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + uut.CreateAccount(testScriptHash); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + } + + [TestMethod] + public void TestDecryptKey() + { + var nep2key = keyPair.Export("123", ProtocolSettings.Default.AddressVersion, 2, 1, 1); + var key1 = uut.DecryptKey(nep2key); + var result = key1.Equals(keyPair); + Assert.IsTrue(result); + } + + [TestMethod] + public void TestDeleteAccount() + { + var result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + uut.CreateAccount(testScriptHash); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + uut.DeleteAccount(testScriptHash); + result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + } + + [TestMethod] + public void TestGetAccount() + { + var result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + uut.CreateAccount(keyPair.PrivateKey); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + var account = uut.GetAccount(testScriptHash)!; + var address = Contract.CreateSignatureRedeemScript(keyPair.PublicKey) + .ToScriptHash() + .ToAddress(ProtocolSettings.Default.AddressVersion); + Assert.AreEqual(address, account.Address); + } + + [TestMethod] + public void TestGetAccounts() + { + var keys = new Dictionary(); + var privateKey = new byte[32]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(privateKey); + } + + var key = new KeyPair(privateKey); + keys.Add(Contract.CreateSignatureRedeemScript(key.PublicKey).ToScriptHash(), key); + keys.Add(Contract.CreateSignatureRedeemScript(keyPair.PublicKey).ToScriptHash(), keyPair); + uut.CreateAccount(key.PrivateKey); + uut.CreateAccount(keyPair.PrivateKey); + foreach (var account in uut.GetAccounts()) + { + if (!keys.ContainsKey(account.ScriptHash)) + { + Assert.Fail(); + } + } + } + + public static X509Certificate2 NewCertificate() + { + var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var request = new CertificateRequest( + new X500DistinguishedName("CN=Self-Signed ECDSA"), + key, + HashAlgorithmName.SHA256); + request.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: false)); + request.CertificateExtensions.Add( + new X509BasicConstraintsExtension(false, false, 0, false)); + var start = DateTimeOffset.UtcNow; + var cert = request.CreateSelfSigned(notBefore: start, notAfter: start.AddMonths(3)); + return cert; + } + + [TestMethod] + public void TestImportCert() + { + var cert = NewCertificate(); + Assert.IsNotNull(cert); + Assert.IsTrue(cert.HasPrivateKey); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Assert.ThrowsExactly(() => _ = uut.Import(cert)); + return; + } + var account = uut.Import(cert); + Assert.IsNotNull(account); + } + + [TestMethod] + public void TestImportWif() + { + var wif = keyPair.Export(); + var result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + + uut.Import(wif); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + } + + [TestMethod] + public void TestImportNep2() + { + var result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + + uut.Import(nep2key, "123", 2, 1, 1); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + + uut.DeleteAccount(testScriptHash); + result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + + var wallet = new JObject() + { + ["name"] = "name", + ["version"] = new Version("1.0").ToString(), + ["scrypt"] = new ScryptParameters(2, 1, 1).ToJson(), + ["accounts"] = new JArray(), + ["extra"] = new JObject() + }; + + uut = new NEP6Wallet(null!, "123", ProtocolSettings.Default, wallet); + result = uut.Contains(testScriptHash); + Assert.IsFalse(result); + + uut.Import(nep2key, "123", 2, 1, 1); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + } + + [TestMethod] + public void TestMigrate() + { + var path = GetRandomPath(".json"); + var uw = Wallet.Create(null, path, "123", ProtocolSettings.Default)!; + uw.CreateAccount(keyPair.PrivateKey); + uw.Save(); + var npath = GetRandomPath(".json"); + var nw = Wallet.Migrate(npath, path, "123", ProtocolSettings.Default); + var result = nw.Contains(testScriptHash); + Assert.IsTrue(result); + uw.Delete(); + nw.Delete(); + } + + [TestMethod] + public void TestSave() + { + var wallet = new JObject() + { + ["name"] = "name", + ["version"] = new Version("1.0").ToString(), + ["scrypt"] = new ScryptParameters(2, 1, 1).ToJson(), + ["accounts"] = new JArray(), + ["extra"] = new JObject() + }; + File.WriteAllText(wPath, wallet.ToString()); + + uut = new NEP6Wallet(wPath, "123", ProtocolSettings.Default); + uut.CreateAccount(keyPair.PrivateKey); + + var result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + uut.Save(); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + } + + [TestMethod] + public void TestToJson() + { + Assert.AreEqual( + "{\"name\":\"noname\",\"version\":\"1.0\",\"scrypt\":{\"n\":2,\"r\":1,\"p\":1},\"accounts\":[],\"extra\":null}", + uut.ToJson().ToString()); + } + + [TestMethod] + public void TestVerifyPassword() + { + var result = uut.VerifyPassword("123"); + Assert.IsTrue(result); + + uut.CreateAccount(keyPair.PrivateKey); + result = uut.Contains(testScriptHash); + Assert.IsTrue(result); + + result = uut.VerifyPassword("123"); + Assert.IsTrue(result); + + uut.DeleteAccount(testScriptHash); + Assert.IsFalse(uut.Contains(testScriptHash)); + + var wallet = new JObject() + { + ["name"] = "name", + ["version"] = new Version("1.0").ToString(), + ["scrypt"] = new ScryptParameters(2, 1, 1).ToJson(), + ["accounts"] = new JArray(), + ["extra"] = new JObject() + }; + + uut = new NEP6Wallet(null!, "123", ProtocolSettings.Default, wallet); + nep2key = keyPair.Export("123", ProtocolSettings.Default.AddressVersion, 2, 1, 1); + uut.Import(nep2key, "123", 2, 1, 1); + Assert.IsFalse(uut.VerifyPassword("1")); + Assert.IsTrue(uut.VerifyPassword("123")); + } + + [TestMethod] + public void Test_NEP6Wallet_Json() + { + Assert.AreEqual("noname", uut.Name); + Assert.AreEqual(new Version("1.0"), uut.Version); + Assert.IsNotNull(uut.Scrypt); + Assert.AreEqual(new ScryptParameters(2, 1, 1).N, uut.Scrypt.N); + } + + [TestMethod] + public void TestIsDefault() + { + var wallet = new JObject() + { + ["name"] = "name", + ["version"] = new Version("1.0").ToString(), + ["scrypt"] = new ScryptParameters(2, 1, 1).ToJson(), + ["accounts"] = new JArray(), + ["extra"] = new JObject() + }; + + var w = new NEP6Wallet(null!, "", ProtocolSettings.Default, wallet); + var ac = w.CreateAccount(); + Assert.AreEqual(ac.Address, w.GetDefaultAccount()!.Address); + + var ac2 = w.CreateAccount(); + Assert.AreEqual(ac.Address, w.GetDefaultAccount()!.Address); + ac2.IsDefault = true; + Assert.AreEqual(ac2.Address, w.GetDefaultAccount()!.Address); + } +} diff --git a/tests/Neo.UnitTests/Wallets/NEP6/UT_ScryptParameters.cs b/tests/Neo.UnitTests/Wallets/NEP6/UT_ScryptParameters.cs new file mode 100644 index 0000000000..c88eae8cda --- /dev/null +++ b/tests/Neo.UnitTests/Wallets/NEP6/UT_ScryptParameters.cs @@ -0,0 +1,70 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_ScryptParameters.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.NEP6; + +namespace Neo.UnitTests.Wallets.NEP6; + +[TestClass] +public class UT_ScryptParameters +{ + ScryptParameters uut = null!; + + [TestInitialize] + public void TestSetup() + { + uut = ScryptParameters.Default; + } + + [TestMethod] + public void Test_Default_ScryptParameters() + { + Assert.AreEqual(16384, uut.N); + Assert.AreEqual(8, uut.R); + Assert.AreEqual(8, uut.P); + } + + [TestMethod] + public void Test_ScryptParameters_Default_ToJson() + { + var json = ScryptParameters.Default.ToJson(); + Assert.AreEqual(ScryptParameters.Default.N, json["n"]!.AsNumber()); + Assert.AreEqual(ScryptParameters.Default.R, json["r"]!.AsNumber()); + Assert.AreEqual(ScryptParameters.Default.P, json["p"]!.AsNumber()); + } + + [TestMethod] + public void Test_Default_ScryptParameters_FromJson() + { + var json = new JObject() + { + ["n"] = 16384, + ["r"] = 8, + ["p"] = 8 + }; + + ScryptParameters uut2 = ScryptParameters.FromJson(json); + Assert.AreEqual(ScryptParameters.Default.N, uut2.N); + Assert.AreEqual(ScryptParameters.Default.R, uut2.R); + Assert.AreEqual(ScryptParameters.Default.P, uut2.P); + } + + [TestMethod] + public void TestScryptParametersConstructor() + { + int n = 1, r = 2, p = 3; + var parameter = new ScryptParameters(n, r, p); + Assert.AreEqual(n, parameter.N); + Assert.AreEqual(r, parameter.R); + Assert.AreEqual(p, parameter.P); + } +} diff --git a/tests/Neo.UnitTests/Wallets/UT_AssetDescriptor.cs b/tests/Neo.UnitTests/Wallets/UT_AssetDescriptor.cs new file mode 100644 index 0000000000..d0a54a9c35 --- /dev/null +++ b/tests/Neo.UnitTests/Wallets/UT_AssetDescriptor.cs @@ -0,0 +1,50 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_AssetDescriptor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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 Neo.Wallets; + +namespace Neo.UnitTests.Wallets; + +[TestClass] +public class UT_AssetDescriptor +{ + [TestMethod] + public void TestConstructorWithNonexistAssetId() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + Assert.ThrowsExactly(() => new AssetDescriptor(snapshotCache, TestProtocolSettings.Default, UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4"))); + } + + [TestMethod] + public void Check_GAS() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var descriptor = new AssetDescriptor(snapshotCache, TestProtocolSettings.Default, NativeContract.GAS.Hash); + Assert.AreEqual(NativeContract.GAS.Hash, descriptor.AssetId); + Assert.AreEqual(nameof(GasToken), descriptor.AssetName); + Assert.AreEqual(nameof(GasToken), descriptor.ToString()); + Assert.AreEqual("GAS", descriptor.Symbol); + Assert.AreEqual(8, descriptor.Decimals); + } + + [TestMethod] + public void Check_NEO() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var descriptor = new AssetDescriptor(snapshotCache, TestProtocolSettings.Default, NativeContract.NEO.Hash); + Assert.AreEqual(NativeContract.NEO.Hash, descriptor.AssetId); + Assert.AreEqual(nameof(NeoToken), descriptor.AssetName); + Assert.AreEqual(nameof(NeoToken), descriptor.ToString()); + Assert.AreEqual("NEO", descriptor.Symbol); + Assert.AreEqual(0, descriptor.Decimals); + } +} diff --git a/tests/Neo.UnitTests/Wallets/UT_KeyPair.cs b/tests/Neo.UnitTests/Wallets/UT_KeyPair.cs new file mode 100644 index 0000000000..2dd1a8d08e --- /dev/null +++ b/tests/Neo.UnitTests/Wallets/UT_KeyPair.cs @@ -0,0 +1,118 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_KeyPair.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Factories; +using Neo.Wallets; + +namespace Neo.UnitTests.Wallets; + +[TestClass] +public class UT_KeyPair +{ + [TestMethod] + public void TestConstructor() + { + byte[] privateKey = new byte[32]; + for (int i = 0; i < privateKey.Length; i++) + privateKey[i] = RandomNumberFactory.NextByte(); + var keyPair = new KeyPair(privateKey); + ECPoint publicKey = ECCurve.Secp256r1.G * privateKey; + CollectionAssert.AreEqual(privateKey, keyPair.PrivateKey); + Assert.AreEqual(publicKey, keyPair.PublicKey); + + byte[] privateKey96 = new byte[96]; + for (int i = 0; i < privateKey96.Length; i++) + privateKey96[i] = RandomNumberFactory.NextByte(); + keyPair = new KeyPair(privateKey96); + publicKey = ECPoint.DecodePoint(new byte[] { 0x04 }.Concat(privateKey96.Skip(privateKey96.Length - 96).Take(64)).ToArray(), ECCurve.Secp256r1); + CollectionAssert.AreEqual(privateKey96.Skip(64).Take(32).ToArray(), keyPair.PrivateKey); + Assert.AreEqual(publicKey, keyPair.PublicKey); + + byte[] privateKey31 = new byte[31]; + for (int i = 0; i < privateKey31.Length; i++) + privateKey31[i] = RandomNumberFactory.NextByte(); + Assert.ThrowsExactly(() => new KeyPair(privateKey31)); + } + + [TestMethod] + public void TestEquals() + { + byte[] privateKey = new byte[32]; + for (int i = 0; i < privateKey.Length; i++) + privateKey[i] = RandomNumberFactory.NextByte(); + var keyPair = new KeyPair(privateKey); + KeyPair keyPair2 = keyPair; + Assert.IsTrue(keyPair.Equals(keyPair2)); + + KeyPair? keyPair3 = null; + Assert.IsFalse(keyPair.Equals(keyPair3)); + + byte[] privateKey1 = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + byte[] privateKey2 = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02}; + var keyPair4 = new KeyPair(privateKey1); + var keyPair5 = new KeyPair(privateKey2); + Assert.IsFalse(keyPair4.Equals(keyPair5)); + } + + [TestMethod] + public void TestEqualsWithObj() + { + byte[] privateKey = new byte[32]; + for (int i = 0; i < privateKey.Length; i++) + privateKey[i] = RandomNumberFactory.NextByte(); + var keyPair = new KeyPair(privateKey); + object keyPair2 = keyPair; + Assert.IsTrue(keyPair.Equals(keyPair2)); + } + + [TestMethod] + public void TestExport() + { + byte[] privateKey = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + byte[] data = { 0x80, 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + var keyPair = new KeyPair(privateKey); + Assert.AreEqual(Base58.Base58CheckEncode(data), keyPair.Export()); + } + + [TestMethod] + public void TestGetPublicKeyHash() + { + byte[] privateKey = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + var keyPair = new KeyPair(privateKey); + Assert.AreEqual("0x4ab3d6ac3a0609e87af84599c93d57c2d0890406", keyPair.PublicKeyHash.ToString()); + } + + [TestMethod] + public void TestGetHashCode() + { + byte[] privateKey = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + var keyPair1 = new KeyPair(privateKey); + var keyPair2 = new KeyPair(privateKey); + Assert.AreEqual(keyPair2.GetHashCode(), keyPair1.GetHashCode()); + } + + [TestMethod] + public void TestToString() + { + byte[] privateKey = { 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + var keyPair = new KeyPair(privateKey); + Assert.AreEqual("026ff03b949241ce1dadd43519e6960e0a85b41a69a05c328103aa2bce1594ca16", keyPair.ToString()); + } +} diff --git a/tests/Neo.UnitTests/Wallets/UT_Wallet.cs b/tests/Neo.UnitTests/Wallets/UT_Wallet.cs new file mode 100644 index 0000000000..2e6f736999 --- /dev/null +++ b/tests/Neo.UnitTests/Wallets/UT_Wallet.cs @@ -0,0 +1,540 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_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 Neo.Cryptography; +using Neo.Cryptography.ECC; +using Neo.Factories; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Sign; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.UnitTests.Cryptography; +using Neo.Wallets; +using System.Numerics; +using Helper = Neo.SmartContract.Helper; + +namespace Neo.UnitTests.Wallets; + +internal class MyWallet : Wallet +{ + public override string Name => "MyWallet"; + + public override Version Version => Version.Parse("0.0.1"); + + private readonly Dictionary accounts = new(); + + public MyWallet() : base(null!, TestProtocolSettings.Default) { } + + public override bool ChangePassword(string oldPassword, string newPassword) + { + throw new NotImplementedException(); + } + + public override bool Contains(UInt160 scriptHash) + { + return accounts.ContainsKey(scriptHash); + } + + public void AddAccount(WalletAccount account) + { + accounts.Add(account.ScriptHash, account); + } + + public override WalletAccount CreateAccount(byte[] privateKey) + { + KeyPair key = new(privateKey); + var contract = new Contract + { + Script = Contract.CreateSignatureRedeemScript(key.PublicKey), + ParameterList = [ContractParameterType.Signature] + }; + MyWalletAccount account = new(contract.ScriptHash); + account.SetKey(key); + account.Contract = contract; + AddAccount(account); + return account; + } + + public override WalletAccount CreateAccount(Contract contract, KeyPair? key = null) + { + MyWalletAccount account = new(contract.ScriptHash) + { + Contract = contract + }; + account.SetKey(key!); + AddAccount(account); + return account; + } + + public override WalletAccount CreateAccount(UInt160 scriptHash) + { + MyWalletAccount account = new(scriptHash); + AddAccount(account); + return account; + } + + public override void Delete() { } + + public override bool DeleteAccount(UInt160 scriptHash) + { + return accounts.Remove(scriptHash); + } + + public override WalletAccount? GetAccount(UInt160 scriptHash) + { + accounts.TryGetValue(scriptHash, out WalletAccount? account); + return account; + } + + public override IEnumerable GetAccounts() + { + return accounts.Values; + } + + public override bool VerifyPassword(string password) + { + return true; + } + + public override void Save() { } +} + +[TestClass] +public class UT_Wallet +{ + private static KeyPair glkey = null!; + private static string nep2Key = null!; + + [ClassInitialize] + public static void ClassInit(TestContext ctx) + { + glkey = UT_Crypto.GenerateCertainKey(32); + nep2Key = glkey.Export("pwd", TestProtocolSettings.Default.AddressVersion, 2, 1, 1); + } + + [TestMethod] + public void TestContains() + { + MyWallet wallet = new(); + try + { + wallet.Contains(UInt160.Zero); + } + catch (Exception) + { + Assert.Fail(); + } + } + + [TestMethod] + public void TestCreateAccount1() + { + var wallet = new MyWallet(); + Assert.IsNotNull(wallet.CreateAccount(new byte[32])); + } + + [TestMethod] + public void TestCreateAccount2() + { + var wallet = new MyWallet(); + var contract = Contract.Create([ContractParameterType.Boolean], [1]); + var account = wallet.CreateAccount(contract, UT_Crypto.GenerateCertainKey(32).PrivateKey); + Assert.IsNotNull(account); + } + + [TestMethod] + public void TestCreateAccount3() + { + var wallet = new MyWallet(); + var contract = Contract.Create([ContractParameterType.Boolean], [1]); + Assert.IsNotNull(wallet.CreateAccount(contract, glkey)); + } + + [TestMethod] + public void TestCreateAccount4() + { + var wallet = new MyWallet(); + Assert.IsNotNull(wallet.CreateAccount(UInt160.Zero)); + } + + [TestMethod] + public void TestGetName() + { + var wallet = new MyWallet(); + Assert.AreEqual("MyWallet", wallet.Name); + } + + [TestMethod] + public void TestGetVersion() + { + var wallet = new MyWallet(); + Assert.AreEqual(Version.Parse("0.0.1"), wallet.Version); + } + + [TestMethod] + public void TestGetAccount1() + { + var wallet = new MyWallet(); + wallet.CreateAccount(UInt160.Parse("0x7efe7ee0d3e349e085388c351955e5172605de66")); + var account = wallet.GetAccount(ECCurve.Secp256r1.G)!; + Assert.AreEqual(UInt160.Parse("0x7efe7ee0d3e349e085388c351955e5172605de66"), account.ScriptHash); + } + + [TestMethod] + public void TestGetAccount2() + { + var wallet = new MyWallet(); + + try + { + wallet.GetAccount(UInt160.Zero); + } + catch (Exception) + { + Assert.Fail(); + } + } + + [TestMethod] + public void TestGetAccounts() + { + var wallet = new MyWallet(); + try + { + wallet.GetAccounts(); + } + catch (Exception) + { + Assert.Fail(); + } + } + + [TestMethod] + public void TestGetAvailable() + { + var wallet = new MyWallet(); + var contract = Contract.Create([ContractParameterType.Boolean], [1]); + var account = wallet.CreateAccount(contract, glkey.PrivateKey); + account.Lock = false; + + // Fake balance + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var key = NativeContract.GAS.CreateStorageKey(20, account.ScriptHash); + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + Assert.AreEqual(new BigDecimal(new BigInteger(1000000000000M), 8), wallet.GetAvailable(snapshotCache, NativeContract.GAS.Hash)); + + entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 0; + } + + [TestMethod] + public void TestGetBalance() + { + var wallet = new MyWallet(); + var contract = Contract.Create([ContractParameterType.Boolean], [1]); + var account = wallet.CreateAccount(contract, glkey.PrivateKey); + account.Lock = false; + + // Fake balance + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var key = NativeContract.GAS.CreateStorageKey(20, account.ScriptHash); + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + Assert.AreEqual(new BigDecimal(BigInteger.Zero, 0), + wallet.GetBalance(snapshotCache, UInt160.Zero, [account.ScriptHash])); + Assert.AreEqual(new BigDecimal(new BigInteger(1000000000000M), 8), + wallet.GetBalance(snapshotCache, NativeContract.GAS.Hash, [account.ScriptHash])); + + entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 0; + } + + [TestMethod] + public void TestGetPrivateKeyFromNEP2() + { + Action action = () => Wallet.GetPrivateKeyFromNEP2("3vQB7B6MrGQZaxCuFg4oh", "TestGetPrivateKeyFromNEP2", + ProtocolSettings.Default.AddressVersion, 2, 1, 1); + Assert.ThrowsExactly(action); + + action = () => Wallet.GetPrivateKeyFromNEP2(nep2Key, "Test", ProtocolSettings.Default.AddressVersion, 2, 1, 1); + Assert.ThrowsExactly(action); + + CollectionAssert.AreEqual("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".HexToBytes(), + Wallet.GetPrivateKeyFromNEP2(nep2Key, "pwd", ProtocolSettings.Default.AddressVersion, 2, 1, 1)); + } + + [TestMethod] + public void TestGetPrivateKeyFromWIF() + { + Assert.ThrowsExactly(() => Wallet.GetPrivateKeyFromWIF("3vQB7B6MrGQZaxCuFg4oh")); + + CollectionAssert.AreEqual("c7134d6fd8e73d819e82755c64c93788d8db0961929e025a53363c4cc02a6962".HexToBytes(), + Wallet.GetPrivateKeyFromWIF("L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU")); + } + + [TestMethod] + public void TestImport1() + { + var wallet = new MyWallet(); + Assert.IsNotNull(wallet.Import("L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU")); + } + + [TestMethod] + public void TestImport2() + { + var wallet = new MyWallet(); + Assert.IsNotNull(wallet.Import(nep2Key, "pwd", 2, 1, 1)); + } + + [TestMethod] + public void TestMakeTransaction1() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var wallet = new MyWallet(); + var contract = Contract.Create([ContractParameterType.Boolean], [1]); + var account = wallet.CreateAccount(contract, glkey.PrivateKey); + account.Lock = false; + + Action action = () => wallet.MakeTransaction(snapshotCache, [ + new() + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = account.ScriptHash, + Value = new BigDecimal(BigInteger.One, 8), + Data = "Dec 12th" + } + ], UInt160.Zero); + Assert.ThrowsExactly(action); + + action = () => wallet.MakeTransaction(snapshotCache, [ + new() + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = account.ScriptHash, + Value = new BigDecimal(BigInteger.One, 8), + Data = "Dec 12th" + } + ], account.ScriptHash); + Assert.ThrowsExactly(action); + + action = () => wallet.MakeTransaction(snapshotCache, [ + new() + { + AssetId = UInt160.Zero, + ScriptHash = account.ScriptHash, + Value = new BigDecimal(BigInteger.One,8), + Data = "Dec 12th" + } + ], account.ScriptHash); + Assert.ThrowsExactly(action); + + // Fake balance + var key = NativeContract.GAS.CreateStorageKey(20, account.ScriptHash); + var entry1 = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry1.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + key = NativeContract.NEO.CreateStorageKey(20, account.ScriptHash); + var entry2 = snapshotCache.GetAndChange(key, () => new StorageItem(new NeoToken.NeoAccountState())); + entry2.GetInteroperable().Balance = 10000 * NativeContract.NEO.Factor; + + var tx = wallet.MakeTransaction(snapshotCache, [ + new() + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = account.ScriptHash, + Value = new BigDecimal(BigInteger.One,8) + } + ]); + Assert.IsNotNull(tx); + + tx = wallet.MakeTransaction(snapshotCache, [ + new() + { + AssetId = NativeContract.NEO.Hash, + ScriptHash = account.ScriptHash, + Value = new BigDecimal(BigInteger.One,8), + Data = "Dec 12th" + } + ]); + Assert.IsNotNull(tx); + + entry1 = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry2 = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry1.GetInteroperable().Balance = 0; + entry2.GetInteroperable().Balance = 0; + } + + [TestMethod] + public void TestMakeTransaction2() + { + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var wallet = new MyWallet(); + Assert.ThrowsExactly(() => wallet.MakeTransaction(snapshotCache, Array.Empty(), null, null, [])); + + var contract = Contract.Create([ContractParameterType.Boolean], [1]); + var account = wallet.CreateAccount(contract, glkey.PrivateKey); + account.Lock = false; + + // Fake balance + var key = NativeContract.GAS.CreateStorageKey(20, account.ScriptHash); + var entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 1000000 * NativeContract.GAS.Factor; + + var tx = wallet.MakeTransaction(snapshotCache, Array.Empty(), account.ScriptHash, [ + new() + { + Account = account.ScriptHash, + Scopes = WitnessScope.CalledByEntry + } + ], []); + + Assert.IsNotNull(tx); + + tx = wallet.MakeTransaction(snapshotCache, Array.Empty(), null, null, []); + Assert.IsNotNull(tx); + + entry = snapshotCache.GetAndChange(key, () => new StorageItem(new AccountState())); + entry.GetInteroperable().Balance = 0; + } + + [TestMethod] + public void TestVerifyPassword() + { + var wallet = new MyWallet(); + try + { + wallet.VerifyPassword("Test"); + } + catch (Exception) + { + Assert.Fail(); + } + } + + [TestMethod] + public void TestSign() + { + var wallet = new MyWallet(); + var snapshotCache = TestBlockchain.GetTestSnapshotCache(); + var network = TestProtocolSettings.Default.Network; + var block = TestUtils.MakeBlock(snapshotCache, UInt256.Zero, 0); + + Action action = () => wallet.SignBlock(block, glkey.PublicKey, network); + Assert.ThrowsExactly(action); // no account + + wallet.CreateAccount(glkey.PrivateKey); + + var signature = wallet.SignBlock(block, glkey.PublicKey, network); + Assert.AreEqual(64, signature.Length); + + var signData = block.GetSignData(network); + var isValid = Crypto.VerifySignature(signData, signature.Span, glkey.PublicKey); + Assert.IsTrue(isValid); + + var key = new byte[32]; + Array.Fill(key, (byte)0x02); + + var pair = new KeyPair(key); + var scriptHash = Contract.CreateSignatureRedeemScript(pair.PublicKey).ToScriptHash(); + wallet.CreateAccount(scriptHash); + Assert.IsNotNull(pair.PublicKey); + + action = () => wallet.SignBlock(block, pair.PublicKey, network); + Assert.ThrowsExactly(action); // no private key + + wallet.GetAccount(scriptHash)!.Lock = true; + action = () => wallet.SignBlock(block, pair.PublicKey, network); + Assert.ThrowsExactly(action); // locked + } + + [TestMethod] + public void TestContainsKeyPair() + { + var wallet = new MyWallet(); + var contains = wallet.ContainsSignable(glkey.PublicKey); + Assert.IsFalse(contains); + + wallet.CreateAccount(glkey.PrivateKey); + + contains = wallet.ContainsSignable(glkey.PublicKey); + Assert.IsTrue(contains); + + var key = new byte[32]; + Array.Fill(key, (byte)0x01); + + var pair = new KeyPair(key); + contains = wallet.ContainsSignable(pair.PublicKey); + Assert.IsFalse(contains); + + wallet.CreateAccount(pair.PrivateKey); + contains = wallet.ContainsSignable(pair.PublicKey); + Assert.IsTrue(contains); + + contains = wallet.ContainsSignable(glkey.PublicKey); + Assert.IsTrue(contains); + + key = new byte[32]; + Array.Fill(key, (byte)0x02); + + pair = new KeyPair(key); + var scriptHash = Contract.CreateSignatureRedeemScript(pair.PublicKey).ToScriptHash(); + wallet.CreateAccount(scriptHash); + + contains = wallet.ContainsSignable(pair.PublicKey); + Assert.IsFalse(contains); // no private key + + wallet.GetAccount(scriptHash)!.Lock = true; + contains = wallet.ContainsSignable(pair.PublicKey); + Assert.IsFalse(contains); // locked + } + + [TestMethod] + public void TestMultiSigAccount() + { + var expectedWallet = new MyWallet(); + var expectedPrivateKey1 = RandomNumberFactory.NextBytes(32, cryptography: true); + var expectedPrivateKey2 = RandomNumberFactory.NextBytes(32, cryptography: true); + var expectedPrivateKey3 = RandomNumberFactory.NextBytes(32, cryptography: true); + + var expectedWalletAccount1 = expectedWallet.CreateAccount(expectedPrivateKey1); + var expectedWalletAccount2 = expectedWallet.CreateAccount(expectedPrivateKey2); + var expectedWalletAccount3 = expectedWallet.CreateAccount(expectedPrivateKey3); + + var expectedAccountKey1 = expectedWalletAccount1.GetKey()!; + var expectedAccountKey2 = expectedWalletAccount2.GetKey()!; + var expectedAccountKey3 = expectedWalletAccount3.GetKey()!; + + var actualMultiSigAccount1 = expectedWallet.CreateMultiSigAccount([expectedAccountKey1.PublicKey]); + var actualMultiSigAccount2 = expectedWallet.CreateMultiSigAccount([expectedAccountKey1.PublicKey, expectedAccountKey2.PublicKey, expectedAccountKey3.PublicKey]); + + Assert.IsNotNull(actualMultiSigAccount1); + Assert.AreNotEqual(expectedWalletAccount1.ScriptHash, actualMultiSigAccount1.ScriptHash); + Assert.AreEqual(expectedAccountKey1.PublicKey, actualMultiSigAccount1.GetKey()!.PublicKey); + Assert.IsTrue(Helper.IsMultiSigContract(actualMultiSigAccount1.Contract!.Script)); + Assert.IsTrue(expectedWallet.GetMultiSigAccounts().Contains(actualMultiSigAccount1)); + + var notExpectedAccountKeys = new ECPoint[1025]; + Assert.ThrowsExactly(() => expectedWallet.CreateMultiSigAccount()); + Assert.ThrowsExactly(() => expectedWallet.CreateMultiSigAccount(2, [expectedAccountKey1.PublicKey])); + Assert.ThrowsExactly(() => expectedWallet.CreateMultiSigAccount(0, [expectedAccountKey1.PublicKey])); + Assert.ThrowsExactly(() => expectedWallet.CreateMultiSigAccount(1025, notExpectedAccountKeys)); + + Assert.IsNotNull(actualMultiSigAccount2); + Assert.AreNotEqual(expectedWalletAccount2.ScriptHash, actualMultiSigAccount2.ScriptHash); + Assert.Contains(actualMultiSigAccount2.GetKey()!.PublicKey, [expectedAccountKey1.PublicKey, expectedAccountKey2.PublicKey, expectedAccountKey3.PublicKey]); + Assert.IsTrue(Helper.IsMultiSigContract(actualMultiSigAccount2.Contract!.Script)); + Assert.IsTrue(expectedWallet.GetMultiSigAccounts().Contains(actualMultiSigAccount2)); + } +} diff --git a/tests/Neo.UnitTests/Wallets/UT_WalletAccount.cs b/tests/Neo.UnitTests/Wallets/UT_WalletAccount.cs new file mode 100644 index 0000000000..12fcdc76bb --- /dev/null +++ b/tests/Neo.UnitTests/Wallets/UT_WalletAccount.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_WalletAccount.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.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.Wallets; + +namespace Neo.UnitTests.Wallets; + +public class MyWalletAccount : WalletAccount +{ + private KeyPair? key = null; + public override bool HasKey => key != null; + + public MyWalletAccount(UInt160 scriptHash) + : base(scriptHash, TestProtocolSettings.Default) + { + } + + public override KeyPair? GetKey() + { + return key; + } + + public void SetKey(KeyPair inputKey) + { + key = inputKey; + } +} + +[TestClass] +public class UT_WalletAccount +{ + [TestMethod] + public void TestGetAddress() + { + var walletAccount = new MyWalletAccount(UInt160.Zero); + Assert.AreEqual("NKuyBkoGdZZSLyPbJEetheRhMjeznFZszf", walletAccount.Address); + } + + [TestMethod] + public void TestGetWatchOnly() + { + var walletAccount = new MyWalletAccount(UInt160.Zero); + Assert.IsTrue(walletAccount.WatchOnly); + walletAccount.Contract = new Contract(); + Assert.IsFalse(walletAccount.WatchOnly); + } +} diff --git a/tests/Neo.UnitTests/Wallets/UT_Wallets_Helper.cs b/tests/Neo.UnitTests/Wallets/UT_Wallets_Helper.cs new file mode 100644 index 0000000000..f96c2e4131 --- /dev/null +++ b/tests/Neo.UnitTests/Wallets/UT_Wallets_Helper.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_Wallets_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.Cryptography; +using Neo.Extensions.IO; +using Neo.Wallets; + +namespace Neo.UnitTests.Wallets; + +[TestClass] +public class UT_Wallets_Helper +{ + [TestMethod] + public void TestToScriptHash() + { + byte[] array = { 0x01 }; + UInt160 scriptHash = new(Crypto.Hash160(array)); + Assert.AreEqual(scriptHash, "NdtB8RXRmJ7Nhw1FPTm7E6HoDZGnDw37nf".ToScriptHash(TestProtocolSettings.Default.AddressVersion)); + + Action action = () => "3vQB7B6MrGQZaxCuFg4oh".ToScriptHash(TestProtocolSettings.Default.AddressVersion); + Assert.ThrowsExactly(action); + + var address = scriptHash.ToAddress(ProtocolSettings.Default.AddressVersion); + Span data = stackalloc byte[21]; + // NEO version is 0x17 + data[0] = 0x01; + scriptHash.ToArray().CopyTo(data[1..]); + address = Base58.Base58CheckEncode(data); + action = () => address.ToScriptHash(ProtocolSettings.Default.AddressVersion); + Assert.ThrowsExactly(action); + } +} diff --git a/tests/Neo.UnitTests/test.config.json b/tests/Neo.UnitTests/test.config.json new file mode 100644 index 0000000000..0d2f885da6 --- /dev/null +++ b/tests/Neo.UnitTests/test.config.json @@ -0,0 +1,66 @@ +{ + "ApplicationConfiguration": { + "Logger": { + "Path": "Logs", + "ConsoleOutput": false, + "Active": false + }, + "Storage": { + "Engine": "LevelDBStore", + "Path": "Data_LevelDB_{0}" + }, + "P2P": { + "Port": 10333, + "WsPort": 10334, + "MinDesiredConnections": 10, + "MaxConnections": 40, + "MaxConnectionsPerAddress": 3 + }, + "UnlockWallet": { + "Path": "", + "Password": "", + "IsActive": false + } + }, + "ProtocolConfiguration": { + "Network": 860833102, + "AddressVersion": 53, + "MillisecondsPerBlock": 15000, + "MaxTransactionsPerBlock": 512, + "MemoryPoolMaxTransactions": 50000, + "MaxTraceableBlocks": 2102400, + "Hardforks": {}, + "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" + ] + } +}