├── .circleci ├── config.yml └── merge_pr.sh ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── pull_request_template.md ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .project-template ├── fill_template_vars.py ├── refill_template_vars.py └── template_vars.txt ├── .readthedocs.yaml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── newsfragments ├── README.md └── validate_files.py ├── pyproject.toml ├── scripts └── release │ └── test_package.py ├── setup.py ├── tests └── core │ ├── __init__.py │ ├── conftest.py │ ├── sample_proof_key_does_not_exist.py │ ├── sample_proof_key_exists.py │ ├── speed.py │ ├── test_bin_trie.py │ ├── test_binaries_utils.py │ ├── test_branches_utils.py │ ├── test_constants.py │ ├── test_exceptions.py │ ├── test_fog.py │ ├── test_hexary_trie.py │ ├── test_hexary_trie_walk.py │ ├── test_import_and_version.py │ ├── test_iter.py │ ├── test_nibbles_utils.py │ ├── test_nodes_utils.py │ ├── test_proof.py │ ├── test_smt.py │ └── test_typing.py ├── tox.ini └── trie ├── __init__.py ├── binary.py ├── branches.py ├── constants.py ├── exceptions.py ├── fog.py ├── hexary.py ├── iter.py ├── smt.py ├── tools ├── builder.py └── strategies.py ├── typing.py ├── utils ├── __init__.py ├── binaries.py ├── db.py ├── nibbles.py └── nodes.py └── validation.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # heavily inspired by https://raw.githubusercontent.com/pinax/pinax-wiki/6bd2a99ab6f702e300d708532a6d1d9aa638b9f8/.circleci/config.yml 4 | 5 | common: &common 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | - run: 10 | name: checkout fixtures submodule 11 | command: git submodule update --init --recursive 12 | - run: 13 | name: merge pull request base 14 | command: ./.circleci/merge_pr.sh 15 | - run: 16 | name: merge pull request base (2nd try) 17 | command: ./.circleci/merge_pr.sh 18 | when: on_fail 19 | - run: 20 | name: merge pull request base (3rd try) 21 | command: ./.circleci/merge_pr.sh 22 | when: on_fail 23 | - restore_cache: 24 | keys: 25 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 26 | - run: 27 | name: install dependencies 28 | command: | 29 | python -m pip install --upgrade pip 30 | python -m pip install tox 31 | - run: 32 | name: run tox 33 | command: python -m tox run -r 34 | - save_cache: 35 | paths: 36 | - .hypothesis 37 | - .tox 38 | - ~/.cache/pip 39 | - ~/.local 40 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 41 | 42 | orbs: 43 | win: circleci/windows@5.0.0 44 | 45 | windows-wheel-steps: 46 | windows-wheel-setup: &windows-wheel-setup 47 | executor: 48 | name: win/default 49 | shell: bash.exe 50 | working_directory: C:\Users\circleci\project\py-trie 51 | environment: 52 | TOXENV: windows-wheel 53 | restore-cache-step: &restore-cache-step 54 | restore_cache: 55 | keys: 56 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 57 | install-pyenv-step: &install-pyenv-step 58 | run: 59 | name: install pyenv 60 | command: | 61 | pip install pyenv-win --target $HOME/.pyenv 62 | echo 'export PYENV="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 63 | echo 'export PYENV_ROOT="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 64 | echo 'export PYENV_USERPROFILE="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 65 | echo 'export PATH="$PATH:$HOME/.pyenv/pyenv-win/bin"' >> $BASH_ENV 66 | echo 'export PATH="$PATH:$HOME/.pyenv/pyenv-win/shims"' >> $BASH_ENV 67 | source $BASH_ENV 68 | pyenv update 69 | install-latest-python-step: &install-latest-python-step 70 | run: 71 | name: install latest python version and tox 72 | command: | 73 | LATEST_VERSION=$(pyenv install --list | grep -E "${MINOR_VERSION}\.[0-9]+$" | tail -1) 74 | echo "installing python version $LATEST_VERSION" 75 | pyenv install $LATEST_VERSION 76 | pyenv global $LATEST_VERSION 77 | python3 -m pip install --upgrade pip 78 | python3 -m pip install tox 79 | run-tox-step: &run-tox-step 80 | run: 81 | name: run tox 82 | command: | 83 | echo 'running tox with' $(python3 --version) 84 | python3 -m tox run -r 85 | save-cache-step: &save-cache-step 86 | save_cache: 87 | paths: 88 | - .tox 89 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 90 | 91 | docs: &docs 92 | working_directory: ~/repo 93 | steps: 94 | - checkout 95 | - restore_cache: 96 | keys: 97 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 98 | - run: 99 | name: install dependencies 100 | command: | 101 | python -m pip install --upgrade pip 102 | python -m pip install tox 103 | - run: 104 | name: run tox 105 | command: python -m tox run -r 106 | - store_artifacts: 107 | path: /home/circleci/repo/docs/_build 108 | - save_cache: 109 | paths: 110 | - .tox 111 | - ~/.cache/pip 112 | - ~/.local 113 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 114 | resource_class: xlarge 115 | 116 | jobs: 117 | docs: 118 | <<: *docs 119 | docker: 120 | - image: cimg/python:3.10 121 | environment: 122 | TOXENV: docs 123 | 124 | py38-core: 125 | <<: *common 126 | docker: 127 | - image: cimg/python:3.8 128 | environment: 129 | TOXENV: py38-core 130 | py39-core: 131 | <<: *common 132 | docker: 133 | - image: cimg/python:3.9 134 | environment: 135 | TOXENV: py39-core 136 | py310-core: 137 | <<: *common 138 | docker: 139 | - image: cimg/python:3.10 140 | environment: 141 | TOXENV: py310-core 142 | py311-core: 143 | <<: *common 144 | docker: 145 | - image: cimg/python:3.11 146 | environment: 147 | TOXENV: py311-core 148 | py312-core: 149 | <<: *common 150 | docker: 151 | - image: cimg/python:3.12 152 | environment: 153 | TOXENV: py312-core 154 | py313-core: 155 | <<: *common 156 | docker: 157 | - image: cimg/python:3.13 158 | environment: 159 | TOXENV: py313-core 160 | 161 | py38-lint: 162 | <<: *common 163 | docker: 164 | - image: cimg/python:3.8 165 | environment: 166 | TOXENV: py38-lint 167 | py39-lint: 168 | <<: *common 169 | docker: 170 | - image: cimg/python:3.9 171 | environment: 172 | TOXENV: py39-lint 173 | py310-lint: 174 | <<: *common 175 | docker: 176 | - image: cimg/python:3.10 177 | environment: 178 | TOXENV: py310-lint 179 | py311-lint: 180 | <<: *common 181 | docker: 182 | - image: cimg/python:3.11 183 | environment: 184 | TOXENV: py311-lint 185 | py312-lint: 186 | <<: *common 187 | docker: 188 | - image: cimg/python:3.12 189 | environment: 190 | TOXENV: py312-lint 191 | py313-lint: 192 | <<: *common 193 | docker: 194 | - image: cimg/python:3.13 195 | environment: 196 | TOXENV: py313-lint 197 | 198 | py38-wheel: 199 | <<: *common 200 | docker: 201 | - image: cimg/python:3.8 202 | environment: 203 | TOXENV: py38-wheel 204 | py39-wheel: 205 | <<: *common 206 | docker: 207 | - image: cimg/python:3.9 208 | environment: 209 | TOXENV: py39-wheel 210 | py310-wheel: 211 | <<: *common 212 | docker: 213 | - image: cimg/python:3.10 214 | environment: 215 | TOXENV: py310-wheel 216 | py311-wheel: 217 | <<: *common 218 | docker: 219 | - image: cimg/python:3.11 220 | environment: 221 | TOXENV: py311-wheel 222 | py312-wheel: 223 | <<: *common 224 | docker: 225 | - image: cimg/python:3.12 226 | environment: 227 | TOXENV: py312-wheel 228 | py313-wheel: 229 | <<: *common 230 | docker: 231 | - image: cimg/python:3.13 232 | environment: 233 | TOXENV: py313-wheel 234 | 235 | py311-windows-wheel: 236 | <<: *windows-wheel-setup 237 | steps: 238 | - checkout 239 | - <<: *restore-cache-step 240 | - <<: *install-pyenv-step 241 | - run: 242 | name: set minor version 243 | command: echo "export MINOR_VERSION='3.11'" >> $BASH_ENV 244 | - <<: *install-latest-python-step 245 | - <<: *run-tox-step 246 | - <<: *save-cache-step 247 | 248 | py312-windows-wheel: 249 | <<: *windows-wheel-setup 250 | steps: 251 | - checkout 252 | - <<: *restore-cache-step 253 | - <<: *install-pyenv-step 254 | - run: 255 | name: set minor version 256 | command: echo "export MINOR_VERSION='3.12'" >> $BASH_ENV 257 | - <<: *install-latest-python-step 258 | - <<: *run-tox-step 259 | - <<: *save-cache-step 260 | 261 | py313-windows-wheel: 262 | <<: *windows-wheel-setup 263 | steps: 264 | - checkout 265 | - <<: *restore-cache-step 266 | - <<: *install-pyenv-step 267 | - run: 268 | name: set minor version 269 | command: echo "export MINOR_VERSION='3.13'" >> $BASH_ENV 270 | - <<: *install-latest-python-step 271 | - <<: *run-tox-step 272 | - <<: *save-cache-step 273 | 274 | define: &all_jobs 275 | - docs 276 | - py38-core 277 | - py39-core 278 | - py310-core 279 | - py311-core 280 | - py312-core 281 | - py313-core 282 | - py38-lint 283 | - py39-lint 284 | - py310-lint 285 | - py311-lint 286 | - py312-lint 287 | - py313-lint 288 | - py38-wheel 289 | - py39-wheel 290 | - py310-wheel 291 | - py311-wheel 292 | - py312-wheel 293 | - py313-wheel 294 | - py311-windows-wheel 295 | - py312-windows-wheel 296 | - py313-windows-wheel 297 | 298 | workflows: 299 | version: 2 300 | test: 301 | jobs: *all_jobs 302 | nightly: 303 | triggers: 304 | - schedule: 305 | # Weekdays 12:00p UTC 306 | cron: "0 12 * * 1,2,3,4,5" 307 | filters: 308 | branches: 309 | only: 310 | - main 311 | jobs: *all_jobs 312 | -------------------------------------------------------------------------------- /.circleci/merge_pr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then 4 | PR_INFO_URL=https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls/$CIRCLE_PR_NUMBER 5 | PR_BASE_BRANCH=$(curl -L "$PR_INFO_URL" | python -c 'import json, sys; obj = json.load(sys.stdin); sys.stdout.write(obj["base"]["ref"])') 6 | git fetch origin +"$PR_BASE_BRANCH":circleci/pr-base 7 | # We need these config values or git complains when creating the 8 | # merge commit 9 | git config --global user.name "Circle CI" 10 | git config --global user.email "circleci@example.com" 11 | git merge --no-edit circleci/pr-base 12 | fi 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "## What was wrong" 8 | - type: textarea 9 | id: what-happened 10 | attributes: 11 | label: What happened? 12 | description: Also tell us what you expected to happen 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: code-that-caused 17 | attributes: 18 | label: Code that produced the error 19 | description: Formats to Python, no backticks needed 20 | render: python 21 | validations: 22 | required: false 23 | - type: textarea 24 | id: error-output 25 | attributes: 26 | label: Full error output 27 | description: Formats to shell, no backticks needed 28 | render: shell 29 | validations: 30 | required: false 31 | - type: markdown 32 | attributes: 33 | value: "## Potential Solutions" 34 | - type: textarea 35 | id: how-to-fix 36 | attributes: 37 | label: Fill this section in if you know how this could or should be fixed 38 | description: Include any relevant examples or reference material 39 | validations: 40 | required: false 41 | - type: input 42 | id: lib-version 43 | attributes: 44 | label: py-trie Version 45 | description: Which version of py-trie are you using? 46 | placeholder: x.x.x 47 | validations: 48 | required: false 49 | - type: input 50 | id: py-version 51 | attributes: 52 | label: Python Version 53 | description: Which version of Python are you using? 54 | placeholder: x.x.x 55 | validations: 56 | required: false 57 | - type: input 58 | id: os 59 | attributes: 60 | label: Operating System 61 | description: Which operating system are you using? 62 | placeholder: osx/linux/win 63 | validations: 64 | required: false 65 | - type: textarea 66 | id: pip-freeze 67 | attributes: 68 | label: Output from `pip freeze` 69 | description: Run `python -m pip freeze` and paste the output below 70 | render: shell 71 | validations: 72 | required: false 73 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Questions about using py-trie? 4 | url: https://discord.gg/GHryRvPB84 5 | about: You can ask and answer usage questions on the Ethereum Python Community Discord 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature 3 | labels: ["feature_request"] 4 | body: 5 | - type: textarea 6 | id: feature-description 7 | attributes: 8 | label: What feature should we add? 9 | description: Include any relevant examples or reference material 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### What was wrong? 2 | 3 | Related to Issue # 4 | Closes # 5 | 6 | ### How was it fixed? 7 | 8 | ### Todo: 9 | 10 | - [ ] Clean up commit history 11 | - [ ] Add or update documentation related to these changes 12 | - [ ] Add entry to the [release notes](https://github.com/ethereum/py-trie/blob/main/newsfragments/README.md) 13 | 14 | #### Cute Animal Picture 15 | 16 | ![Put a link to a cute animal picture inside the parenthesis-->](<>) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | .build 12 | eggs 13 | .eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | pip-wheel-metadata 23 | venv* 24 | .venv* 25 | 26 | # Installer logs 27 | pip-log.txt 28 | 29 | # Unit test / coverage reports 30 | .coverage 31 | .tox 32 | nosetests.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | # Complexity 43 | output/*.html 44 | output/*/index.html 45 | 46 | # Sphinx 47 | docs/_build 48 | docs/modules.rst 49 | docs/*.internal.rst 50 | docs/*.utils.rst 51 | docs/*._utils.* 52 | 53 | # Blockchain 54 | chains 55 | 56 | # Hypothesis Property base testing 57 | .hypothesis 58 | 59 | # tox/pytest cache 60 | .cache 61 | .pytest_cache 62 | 63 | # pycache 64 | __pycache__/ 65 | 66 | # Test output logs 67 | logs 68 | 69 | # VIM temp files 70 | *.sw[op] 71 | 72 | # mypy 73 | .mypy_cache 74 | 75 | # macOS 76 | .DS_Store 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # vs-code 82 | .vscode 83 | 84 | # jupyter notebook files 85 | *.ipynb 86 | 87 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 88 | # For a more precise, explicit template, see: 89 | # https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 90 | 91 | ## General 92 | .idea/* 93 | .idea_modules/* 94 | 95 | ## File-based project format: 96 | *.iws 97 | 98 | ## IntelliJ 99 | out/ 100 | 101 | ## Plugin-specific files: 102 | 103 | ### JIRA plugin 104 | atlassian-ide-plugin.xml 105 | 106 | ### Crashlytics plugin (for Android Studio and IntelliJ) 107 | com_crashlytics_export_strings.xml 108 | crashlytics.properties 109 | crashlytics-build.properties 110 | fabric.properties 111 | 112 | # END JetBrains section 113 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "fixtures"] 2 | path = fixtures 3 | url = https://github.com/ethereum/tests.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: '.project-template|docs/conf.py' 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.5.0 5 | hooks: 6 | - id: check-yaml 7 | - id: check-toml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - repo: https://github.com/asottile/pyupgrade 11 | rev: v3.15.0 12 | hooks: 13 | - id: pyupgrade 14 | args: [--py38-plus] 15 | - repo: https://github.com/psf/black 16 | rev: 23.9.1 17 | hooks: 18 | - id: black 19 | - repo: https://github.com/PyCQA/flake8 20 | rev: 6.1.0 21 | hooks: 22 | - id: flake8 23 | additional_dependencies: 24 | - flake8-bugbear==23.9.16 25 | exclude: setup.py 26 | - repo: https://github.com/PyCQA/autoflake 27 | rev: v2.2.1 28 | hooks: 29 | - id: autoflake 30 | - repo: https://github.com/pycqa/isort 31 | rev: 5.12.0 32 | hooks: 33 | - id: isort 34 | - repo: https://github.com/pycqa/pydocstyle 35 | rev: 6.3.0 36 | hooks: 37 | - id: pydocstyle 38 | additional_dependencies: 39 | - tomli # required until >= python311 40 | - repo: https://github.com/executablebooks/mdformat 41 | rev: 0.7.17 42 | hooks: 43 | - id: mdformat 44 | additional_dependencies: 45 | - mdformat-gfm 46 | - repo: https://github.com/PrincetonUniversity/blocklint 47 | rev: v0.2.5 48 | hooks: 49 | - id: blocklint 50 | exclude: 'docs/Makefile|docs/release_notes.rst|tox.ini|CHANGELOG.rst' 51 | -------------------------------------------------------------------------------- /.project-template/fill_template_vars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import re 6 | from pathlib import Path 7 | 8 | 9 | def _find_files(project_root): 10 | path_exclude_pattern = r"\.git($|\/)|venv|_build" 11 | file_exclude_pattern = r"fill_template_vars\.py|\.swp$" 12 | filepaths = [] 13 | for dir_path, _dir_names, file_names in os.walk(project_root): 14 | if not re.search(path_exclude_pattern, dir_path): 15 | for file in file_names: 16 | if not re.search(file_exclude_pattern, file): 17 | filepaths.append(str(Path(dir_path, file))) 18 | 19 | return filepaths 20 | 21 | 22 | def _replace(pattern, replacement, project_root): 23 | print(f"Replacing values: {pattern}") 24 | for file in _find_files(project_root): 25 | try: 26 | with open(file) as f: 27 | content = f.read() 28 | content = re.sub(pattern, replacement, content) 29 | with open(file, "w") as f: 30 | f.write(content) 31 | except UnicodeDecodeError: 32 | pass 33 | 34 | 35 | def main(): 36 | project_root = Path(os.path.realpath(sys.argv[0])).parent.parent 37 | 38 | module_name = input("What is your python module name? ") 39 | 40 | pypi_input = input(f"What is your pypi package name? (default: {module_name}) ") 41 | pypi_name = pypi_input or module_name 42 | 43 | repo_input = input(f"What is your github project name? (default: {pypi_name}) ") 44 | repo_name = repo_input or pypi_name 45 | 46 | rtd_input = input( 47 | f"What is your readthedocs.org project name? (default: {pypi_name}) " 48 | ) 49 | rtd_name = rtd_input or pypi_name 50 | 51 | project_input = input( 52 | f"What is your project name (ex: at the top of the README)? (default: {repo_name}) " 53 | ) 54 | project_name = project_input or repo_name 55 | 56 | short_description = input("What is a one-liner describing the project? ") 57 | 58 | _replace("", module_name, project_root) 59 | _replace("", pypi_name, project_root) 60 | _replace("", repo_name, project_root) 61 | _replace("", rtd_name, project_root) 62 | _replace("", project_name, project_root) 63 | _replace("", short_description, project_root) 64 | 65 | os.makedirs(project_root / module_name, exist_ok=True) 66 | Path(project_root / module_name / "__init__.py").touch() 67 | Path(project_root / module_name / "py.typed").touch() 68 | 69 | 70 | if __name__ == "__main__": 71 | main() 72 | -------------------------------------------------------------------------------- /.project-template/refill_template_vars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | import subprocess 7 | 8 | 9 | def main(): 10 | template_dir = Path(os.path.dirname(sys.argv[0])) 11 | template_vars_file = template_dir / "template_vars.txt" 12 | fill_template_vars_script = template_dir / "fill_template_vars.py" 13 | 14 | with open(template_vars_file, "r") as input_file: 15 | content_lines = input_file.readlines() 16 | 17 | process = subprocess.Popen( 18 | [sys.executable, str(fill_template_vars_script)], 19 | stdin=subprocess.PIPE, 20 | stdout=subprocess.PIPE, 21 | stderr=subprocess.PIPE, 22 | text=True, 23 | ) 24 | 25 | for line in content_lines: 26 | process.stdin.write(line) 27 | process.stdin.flush() 28 | 29 | stdout, stderr = process.communicate() 30 | 31 | if process.returncode != 0: 32 | print(f"Error occurred: {stderr}") 33 | sys.exit(1) 34 | 35 | print(stdout) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /.project-template/template_vars.txt: -------------------------------------------------------------------------------- 1 | trie 2 | trie 3 | py-trie 4 | 5 | trie 6 | Python implementation of the Ethereum trie structure 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.10" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | fail_on_warning: true 11 | 12 | python: 13 | install: 14 | - method: pip 15 | path: . 16 | extra_requirements: 17 | - docs 18 | 19 | # Build all formats for RTD Downloads - htmlzip, pdf, epub 20 | formats: all 21 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | py-trie v3.1.0 (2025-01-29) 2 | --------------------------- 3 | 4 | Features 5 | ~~~~~~~~ 6 | 7 | - Merge template, adding py313 to CI and replacing ``bumpversion`` with ``bump-my-version``. (`#156 `__) 8 | 9 | 10 | py-trie v3.0.1 (2024-04-22) 11 | --------------------------- 12 | 13 | Internal Changes - for py-trie Contributors 14 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | - Open ``hexbytes`` dep to ``>=0.2.3`` (`#151 `__) 17 | - Merge template updates, notably adding python 3.12 support (`#154 `__) 18 | 19 | 20 | Miscellaneous Changes 21 | ~~~~~~~~~~~~~~~~~~~~~ 22 | 23 | - `#155 `__ 24 | 25 | 26 | py-trie v3.0.0 (2023-12-06) 27 | --------------------------- 28 | 29 | Breaking Changes 30 | ~~~~~~~~~~~~~~~~ 31 | 32 | - Drop support for python 3.7 (`#144 `__) 33 | 34 | 35 | Improved Documentation 36 | ~~~~~~~~~~~~~~~~~~~~~~ 37 | 38 | - Remove typo in README (`#139 `__) 39 | 40 | 41 | Internal Changes - for py-trie Contributors 42 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | 44 | - Add upper pin to ``hexbytes`` dependency to due incoming breaking change (`#141 `__) 45 | - Update `ethereum/tests` fixture to ``v12.4``. (`#143 `__) 46 | - Merge python project template updates, including move to pre-commit for linting (`#144 `__) 47 | - Import types ``Literal`` and ``Protocol`` directly from ``typing`` since now >py38 (`#146 `__) 48 | - Change the name of ``master`` branch to ``main`` (`#147 `__) 49 | 50 | 51 | py-trie v2.1.1 (2023-06-08) 52 | --------------------------- 53 | 54 | Internal Changes - for py-trie Contributors 55 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 56 | 57 | - merged updates from the ethereum python project template (`#137 `__) 58 | - convert old-style `format` strings to f-strings, additional cleanup (`#138 `__) 59 | 60 | 61 | v2.1.0 62 | ------ 63 | 64 | Features 65 | ~~~~~~~~ 66 | 67 | - Support Python 3.11 68 | 69 | Misc 70 | ~~~~ 71 | 72 | - Merged py-trie with the ethereum python project template 73 | 74 | v2.0.2 75 | ------ 76 | 77 | Misc 78 | ~~~~ 79 | 80 | - Remove upper pin on hexbytes dependency 81 | 82 | v2.0.1 83 | ------ 84 | 85 | Misc 86 | ~~~~ 87 | 88 | - Make typing_extensions an optional dependency 89 | 90 | v2.0.0 91 | ------ 92 | 93 | Breaking Changes 94 | ~~~~~~~~~~~~~~~~ 95 | 96 | - Drop python 3.6 support 97 | - Require rlp dependency to be >=3,<4 98 | - Require eth-utils dependency to be >=2,<3 99 | 100 | Features 101 | ~~~~~~~~ 102 | 103 | - Added node_type field to HexaryTrieNode so that users can easily inspect the type 104 | of a node. 105 | - Add support for python 3.9 and 3.10 106 | 107 | Misc 108 | ~~~~ 109 | 110 | - Upgrade typing_extensions dependency 111 | 112 | v2.0.0-alpha.4 113 | --------------- 114 | 115 | Released 2020-08-31 116 | 117 | Breaking Changes 118 | ~~~~~~~~~~~~~~~~ 119 | 120 | - Dropped pypy support, upgrade to faster py-rlp v2-alpha.1 121 | https://github.com/ethereum/py-trie/pull/118 122 | 123 | v2.0.0-alpha.3 124 | --------------- 125 | 126 | Released 2020-08-24 127 | 128 | Bugfixes 129 | ~~~~~~~~ 130 | 131 | - Relax the version constraint on typing-extensions, which was causing downstream conflicts. 132 | https://github.com/ethereum/py-trie/pull/117 133 | 134 | v2.0.0-alpha.2 135 | --------------- 136 | 137 | Released 2020-06-19 138 | 139 | Features 140 | ~~~~~~~~ 141 | 142 | - Added NodeIterator.keys(), .items(), .values() (mimicking the dict version of these), as well 143 | as NodeIterator.nodes(), which yields all of the annotated trie nodes. 144 | https://github.com/ethereum/py-trie/pull/112 145 | - Improved repr(HexaryTrie) 146 | https://github.com/ethereum/py-trie/pull/112 147 | - Can now use NodeIterator to navigate to the empty key b'', using NodeIterator.next(key=None) or 148 | simply NodeIterator.next(). 149 | https://github.com/ethereum/py-trie/pull/110 150 | - TraversedPartialPath has a new simulated_node attribute, which we can treat as a node that 151 | would have been at the traversed path if the traversal had succeeded. See the readme for more. 152 | https://github.com/ethereum/py-trie/pull/111 153 | 154 | Bugfixes 155 | ~~~~~~~~ 156 | 157 | - In certain cases, deleting key b'short' would actually delete the key at b'short-nope-long'! 158 | Changed key_starts_with() to fix it 159 | https://github.com/ethereum/py-trie/pull/109 160 | - HexaryTrie.set(key, b'') would sometimes try to create a leaf node with an 161 | empty value. Instead, it should act exactly the same as HexaryTrie.delete(key) 162 | https://github.com/ethereum/py-trie/pull/109 163 | - When a MissingTrieNode is raised during pruning (or using squash_changes()), a node body 164 | that was pruned before the exception was raised might stay pruned, even though the trie 165 | wasn't updated. 166 | https://github.com/ethereum/py-trie/pull/109 167 | - When using squash_changes() on a HexaryTrie with prune=True, doing a no-op change would 168 | cause the root node to get pruned (deleted even though it was still needed for the current 169 | root hash!). 170 | https://github.com/ethereum/py-trie/pull/113 171 | - Only raise a TraversedPartialPath when traversing into a matching leaf node. Instead, return 172 | an empty node when traversing into a divergent path. 173 | https://github.com/ethereum/py-trie/pull/114 174 | 175 | 176 | v2.0.0-alpha.1 177 | --------------- 178 | 179 | Released 2020-05-27 180 | 181 | Breaking Changes 182 | ~~~~~~~~~~~~~~~~ 183 | 184 | - Removed trie.Trie -- use trie.HexaryTrie instead 185 | https://github.com/ethereum/py-trie/pull/100 186 | - Removed trie.sync (classes: SyncRequest and HexaryTrieSync) 187 | New syncing helper tools are imminent. 188 | https://github.com/ethereum/py-trie/pull/100 189 | - MissingTrieNode is no longer a KeyError, paving the way for eventually raising a KeyError instead 190 | of returning b'' when a key is not present in the trie 191 | https://github.com/ethereum/py-trie/pull/98 192 | - If a trie body is missing when calling HexaryTrie.root_node, the exception will be 193 | MissingTraversalNode instead of MissingTrieNode 194 | https://github.com/ethereum/py-trie/pull/102 195 | - Remove support for setting the trie's raw root node directly, via 196 | HexaryTrie.root_node = new_raw_root_node 197 | https://github.com/ethereum/py-trie/pull/106 198 | - Return new annotated HexaryTrieNode from HexaryTrie.root_node property 199 | https://github.com/ethereum/py-trie/pull/106 200 | 201 | Features 202 | ~~~~~~~~ 203 | 204 | - MissingTrieNode now includes the prefix of the key leading to the node body that was missing 205 | from the database. This is important for other potential database layouts. The prefix may be None, 206 | if it cannot be determined. For now, it will not be determined when setting or deleting a key. 207 | https://github.com/ethereum/py-trie/pull/98 208 | - New HexaryTrie.traverse(tuple_of_nibbles) returns an annotated trie node found at the 209 | given path of nibbles, starting from the root. 210 | https://github.com/ethereum/py-trie/pull/102 211 | - New HexaryTrie.traverse_from(node, tuple_of_nibbles) returns an annotated trie node found 212 | when navigating from the given node_body down through the given path of nibbles. Useful for 213 | avoiding database reads when the parent node body is known. Otherwise, navigating down from 214 | the root would be required every time. 215 | https://github.com/ethereum/py-trie/pull/102 216 | - New MissingTraversalNode exception, analogous to MissingTrieNode, but when traversing 217 | (because key is not available, and root_hash not available during traverse_from()) 218 | https://github.com/ethereum/py-trie/pull/102 219 | - New TraversedPartialPath exception, raised when you try to navigate to a node, but end up 220 | part-way inside an extension node, or try to navigate into a leaf node. 221 | https://github.com/ethereum/py-trie/pull/102 222 | - New HexaryTrieFog to help track unexplored prefixes, when walking a trie. Serializeable to bytes. 223 | New exceptions PerfectVisibility or FullDirectionalVisibility when no prefixes are unexplored. 224 | New TrieFrontierCache to reduce duplicate database accesses on a full trie walk. 225 | https://github.com/ethereum/py-trie/pull/95 226 | 227 | Bugfixes 228 | ~~~~~~~~ 229 | 230 | - Pruning Bugfix: with duplicate values at multiple keys, pruning would sometimes incorrectly 231 | prune out a node that was still required. This is fixed for fresh databases, and unfixable 232 | for existing databases. (Prune is not designed for on-disk/existing DBs anyhow) 233 | https://github.com/ethereum/py-trie/pull/93 234 | - Avoid reading root node when unnecessary during squash_changes(). This can be important when 235 | building a witness, if the witness is supposed to be empty. (for example, in storage tries) 236 | https://github.com/ethereum/py-trie/pull/101 237 | 238 | Misc 239 | ~~~~ 240 | 241 | - Type annotation cleanups & upgrades flake8/eth-utils 242 | https://github.com/ethereum/py-trie/pull/95 243 | 244 | 1.4.0 245 | ---------- 246 | 247 | Released 2019-04-24 248 | 249 | - Python 3.7 support 250 | https://github.com/ethereum/py-trie/pull/73 251 | - Several proof (aka witness) updates 252 | - Added HexaryTrie.get_proof for proving a key exists https://github.com/ethereum/py-trie/pull/80 253 | - Prove a key is missing with get_proof https://github.com/ethereum/py-trie/pull/91 254 | - Bugfix getting a key from a proof with short nodes https://github.com/ethereum/py-trie/pull/82 255 | - Raise MissingTrieNode with extra info, when an expected trie node is missing from the database 256 | (includes update so that pruning old nodes waits until set/delete succeeds) 257 | https://github.com/ethereum/py-trie/pull/83 258 | https://github.com/ethereum/py-trie/pull/86 (minor cleanup of 83) 259 | https://github.com/ethereum/py-trie/pull/90 (squash_changes() support for missing nodes) 260 | - New `with trie.at_root(hash) as snapshot:` API, to read trie at a different root hash 261 | https://github.com/ethereum/py-trie/pull/84 262 | - EXPERIMENTAL Sparse Merkle Trie in trie.smt (unstable API: could change at minor version) 263 | https://github.com/ethereum/py-trie/pull/77 264 | - Dropped support for rlp v0.x 265 | https://github.com/ethereum/py-trie/pull/75 266 | - Doc updates 267 | - https://github.com/ethereum/py-trie/pull/62 268 | - https://github.com/ethereum/py-trie/pull/64 269 | - https://github.com/ethereum/py-trie/pull/72 (plus other maintenance) 270 | 271 | 1.3.8 272 | -------- 273 | 274 | * Speed optimization for `HexaryTrie._prune_node` (https://github.com/ethereum/py-trie/pull/60) 275 | 276 | 1.1.0 277 | -------- 278 | 279 | * Add trie syncing 280 | * Witness helper functions for binary trie 281 | 282 | 1.0.1 283 | -------- 284 | 285 | * Fix broken deprecated `Trie` class. 286 | 287 | 1.0.0 288 | -------- 289 | 290 | * Rename `Trie` to `HexaryTrie` 291 | * Add new `BinaryTrie` class 292 | 293 | 0.3.2 294 | -------- 295 | 296 | * Add `Trie.get_from_proof` for verification of trie proofs. 297 | 298 | 0.3.0 299 | -------- 300 | 301 | * Remove snapshot and revert API 302 | 303 | 0.1.0 304 | -------- 305 | 306 | * Initial Release 307 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2025 The Ethereum Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | 4 | recursive-include scripts * 5 | recursive-include tests * 6 | 7 | global-include *.pyi 8 | 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | prune .tox 12 | prune venv* 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRENT_SIGN_SETTING := $(shell git config commit.gpgSign) 2 | 3 | .PHONY: clean-pyc clean-build docs 4 | 5 | help: 6 | @echo "clean-build - remove build artifacts" 7 | @echo "clean-pyc - remove Python file artifacts" 8 | @echo "clean - run clean-build and clean-pyc" 9 | @echo "dist - build package and cat contents of the dist directory" 10 | @echo "lint - fix linting issues with pre-commit" 11 | @echo "test - run tests quickly with the default Python" 12 | @echo "docs - view draft of newsfragments to be added to CHANGELOG" 13 | @echo "autobuild-docs - live update docs when changes are saved" 14 | @echo "notes - consume towncrier newsfragments/ and update CHANGELOG - requires bump to be set" 15 | @echo "release - package and upload a release (does not run notes target) - requires bump to be set" 16 | 17 | clean-build: 18 | rm -fr build/ 19 | rm -fr dist/ 20 | rm -fr *.egg-info 21 | 22 | clean-pyc: 23 | find . -name '*.pyc' -exec rm -f {} + 24 | find . -name '*.pyo' -exec rm -f {} + 25 | find . -name '*~' -exec rm -f {} + 26 | find . -name '__pycache__' -exec rm -rf {} + 27 | 28 | clean: clean-build clean-pyc 29 | 30 | dist: clean 31 | python -m build 32 | ls -l dist 33 | 34 | lint: 35 | @pre-commit run --all-files --show-diff-on-failure || ( \ 36 | echo "\n\n\n * pre-commit should have fixed the errors above. Running again to make sure everything is good..." \ 37 | && pre-commit run --all-files --show-diff-on-failure \ 38 | ) 39 | 40 | test: 41 | python -m pytest tests 42 | 43 | # docs commands 44 | 45 | docs: 46 | python ./newsfragments/validate_files.py 47 | towncrier build --draft --version preview 48 | 49 | # release commands 50 | 51 | package-test: clean 52 | python -m build 53 | python scripts/release/test_package.py 54 | 55 | notes: check-bump 56 | # Let UPCOMING_VERSION be the version that is used for the current bump 57 | $(eval UPCOMING_VERSION=$(shell bump-my-version bump --dry-run $(bump) -v | awk -F"'" '/New version will be / {print $$2}')) 58 | # Now generate the release notes to have them included in the release commit 59 | towncrier build --yes --version $(UPCOMING_VERSION) 60 | # Before we bump the version, make sure that the towncrier-generated docs will build 61 | make docs 62 | git commit -m "Compile release notes for v$(UPCOMING_VERSION)" 63 | 64 | release: check-bump check-git clean 65 | # verify that notes command ran correctly 66 | ./newsfragments/validate_files.py is-empty 67 | CURRENT_SIGN_SETTING=$(git config commit.gpgSign) 68 | git config commit.gpgSign true 69 | bump-my-version bump $(bump) 70 | python -m build 71 | git config commit.gpgSign "$(CURRENT_SIGN_SETTING)" 72 | git push upstream && git push upstream --tags 73 | twine upload dist/* 74 | 75 | # release helpers 76 | 77 | check-bump: 78 | ifndef bump 79 | $(error bump must be set, typically: major, minor, patch, or devnum) 80 | endif 81 | 82 | check-git: 83 | # require that upstream is configured for ethereum/py-trie 84 | @if ! git remote -v | grep "upstream[[:space:]]git@github.com:ethereum/py-trie.git (push)\|upstream[[:space:]]https://github.com/ethereum/py-trie (push)"; then \ 85 | echo "Error: You must have a remote named 'upstream' that points to 'py-trie'"; \ 86 | exit 1; \ 87 | fi 88 | -------------------------------------------------------------------------------- /newsfragments/README.md: -------------------------------------------------------------------------------- 1 | This directory collects "newsfragments": short files that each contain 2 | a snippet of ReST-formatted text that will be added to the next 3 | release notes. This should be a description of aspects of the change 4 | (if any) that are relevant to users. (This contrasts with the 5 | commit message and PR description, which are a description of the change as 6 | relevant to people working on the code itself.) 7 | 8 | Each file should be named like `..rst`, where 9 | `` is an issue number, and `` is one of: 10 | 11 | - `breaking` 12 | - `bugfix` 13 | - `deprecation` 14 | - `docs` 15 | - `feature` 16 | - `internal` 17 | - `misc` 18 | - `performance` 19 | - `removal` 20 | 21 | So for example: `123.feature.rst`, `456.bugfix.rst` 22 | 23 | If the PR fixes an issue, use that number here. If there is no issue, 24 | then open up the PR first and use the PR number for the newsfragment. 25 | 26 | Note that the `towncrier` tool will automatically 27 | reflow your text, so don't try to do any fancy formatting. Run 28 | `towncrier build --draft` to get a preview of what the release notes entry 29 | will look like in the final release notes. 30 | -------------------------------------------------------------------------------- /newsfragments/validate_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Towncrier silently ignores files that do not match the expected ending. 4 | # We use this script to ensure we catch these as errors in CI. 5 | 6 | import pathlib 7 | import sys 8 | 9 | ALLOWED_EXTENSIONS = { 10 | ".breaking.rst", 11 | ".bugfix.rst", 12 | ".deprecation.rst", 13 | ".docs.rst", 14 | ".feature.rst", 15 | ".internal.rst", 16 | ".misc.rst", 17 | ".performance.rst", 18 | ".removal.rst", 19 | } 20 | 21 | ALLOWED_FILES = { 22 | "validate_files.py", 23 | "README.md", 24 | } 25 | 26 | THIS_DIR = pathlib.Path(__file__).parent 27 | 28 | num_args = len(sys.argv) - 1 29 | assert num_args in {0, 1} 30 | if num_args == 1: 31 | assert sys.argv[1] in ("is-empty",) 32 | 33 | for fragment_file in THIS_DIR.iterdir(): 34 | if fragment_file.name in ALLOWED_FILES: 35 | continue 36 | elif num_args == 0: 37 | full_extension = "".join(fragment_file.suffixes) 38 | if full_extension not in ALLOWED_EXTENSIONS: 39 | raise Exception(f"Unexpected file: {fragment_file}") 40 | elif sys.argv[1] == "is-empty": 41 | raise Exception(f"Unexpected file: {fragment_file}") 42 | else: 43 | raise RuntimeError( 44 | f"Strange: arguments {sys.argv} were validated, but not found" 45 | ) 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.autoflake] 2 | exclude = "__init__.py" 3 | remove_all_unused_imports = true 4 | 5 | [tool.isort] 6 | combine_as_imports = true 7 | extra_standard_library = "pytest" 8 | force_grid_wrap = 1 9 | force_sort_within_sections = true 10 | force_to_top = "pytest" 11 | honor_noqa = true 12 | known_first_party = "trie" 13 | known_third_party = "hypothesis" 14 | multi_line_output = 3 15 | profile = "black" 16 | use_parentheses = true 17 | 18 | [tool.mypy] 19 | check_untyped_defs = true 20 | disallow_any_generics = true 21 | disallow_incomplete_defs = true 22 | disallow_subclassing_any = true 23 | disallow_untyped_calls = true 24 | disallow_untyped_decorators = true 25 | disallow_untyped_defs = true 26 | ignore_missing_imports = true 27 | strict_equality = true 28 | strict_optional = true 29 | warn_redundant_casts = true 30 | warn_return_any = true 31 | warn_unused_configs = true 32 | warn_unused_ignores = true 33 | 34 | 35 | [tool.pydocstyle] 36 | # All error codes found here: 37 | # http://www.pydocstyle.org/en/3.0.0/error_codes.html 38 | # 39 | # Ignored: 40 | # D1 - Missing docstring error codes 41 | # 42 | # Selected: 43 | # D2 - Whitespace error codes 44 | # D3 - Quote error codes 45 | # D4 - Content related error codes 46 | select = "D2,D3,D4" 47 | 48 | # Extra ignores: 49 | # D200 - One-line docstring should fit on one line with quotes 50 | # D203 - 1 blank line required before class docstring 51 | # D204 - 1 blank line required after class docstring 52 | # D205 - 1 blank line required between summary line and description 53 | # D212 - Multi-line docstring summary should start at the first line 54 | # D302 - Use u""" for Unicode docstrings 55 | # D400 - First line should end with a period 56 | # D401 - First line should be in imperative mood 57 | # D412 - No blank lines allowed between a section header and its content 58 | # D415 - First line should end with a period, question mark, or exclamation point 59 | add-ignore = "D200,D203,D204,D205,D212,D302,D400,D401,D412,D415" 60 | 61 | # Explanation: 62 | # D400 - Enabling this error code seems to make it a requirement that the first 63 | # sentence in a docstring is not split across two lines. It also makes it a 64 | # requirement that no docstring can have a multi-sentence description without a 65 | # summary line. Neither one of those requirements seem appropriate. 66 | 67 | [tool.pytest.ini_options] 68 | addopts = "-v --showlocals --durations 10" 69 | log_date_format = "%m-%d %H:%M:%S" 70 | log_format = "%(levelname)8s %(asctime)s %(filename)20s %(message)s" 71 | xfail_strict = true 72 | 73 | [tool.towncrier] 74 | # Read https://github.com/ethereum/py-trie/blob/main/newsfragments/README.md for instructions 75 | directory = "newsfragments" 76 | filename = "CHANGELOG.rst" 77 | issue_format = "`#{issue} `__" 78 | package = "trie" 79 | title_format = "py-trie v{version} ({project_date})" 80 | underlines = ["-", "~", "^"] 81 | 82 | [[tool.towncrier.type]] 83 | directory = "breaking" 84 | name = "Breaking Changes" 85 | showcontent = true 86 | 87 | [[tool.towncrier.type]] 88 | directory = "bugfix" 89 | name = "Bugfixes" 90 | showcontent = true 91 | 92 | [[tool.towncrier.type]] 93 | directory = "deprecation" 94 | name = "Deprecations" 95 | showcontent = true 96 | 97 | [[tool.towncrier.type]] 98 | directory = "docs" 99 | name = "Improved Documentation" 100 | showcontent = true 101 | 102 | [[tool.towncrier.type]] 103 | directory = "feature" 104 | name = "Features" 105 | showcontent = true 106 | 107 | [[tool.towncrier.type]] 108 | directory = "internal" 109 | name = "Internal Changes - for py-trie Contributors" 110 | showcontent = true 111 | 112 | [[tool.towncrier.type]] 113 | directory = "misc" 114 | name = "Miscellaneous Changes" 115 | showcontent = false 116 | 117 | [[tool.towncrier.type]] 118 | directory = "performance" 119 | name = "Performance Improvements" 120 | showcontent = true 121 | 122 | [[tool.towncrier.type]] 123 | directory = "removal" 124 | name = "Removals" 125 | showcontent = true 126 | 127 | [tool.bumpversion] 128 | current_version = "3.1.0" 129 | parse = """ 130 | (?P\\d+) 131 | \\.(?P\\d+) 132 | \\.(?P\\d+) 133 | (- 134 | (?P[^.]*) 135 | \\.(?P\\d+) 136 | )? 137 | """ 138 | serialize = [ 139 | "{major}.{minor}.{patch}-{stage}.{devnum}", 140 | "{major}.{minor}.{patch}", 141 | ] 142 | search = "{current_version}" 143 | replace = "{new_version}" 144 | regex = false 145 | ignore_missing_version = false 146 | tag = true 147 | sign_tags = true 148 | tag_name = "v{new_version}" 149 | tag_message = "Bump version: {current_version} → {new_version}" 150 | allow_dirty = false 151 | commit = true 152 | message = "Bump version: {current_version} → {new_version}" 153 | 154 | [tool.bumpversion.parts.stage] 155 | optional_value = "stable" 156 | first_value = "stable" 157 | values = [ 158 | "alpha", 159 | "beta", 160 | "stable", 161 | ] 162 | 163 | [tool.bumpversion.part.devnum] 164 | 165 | [[tool.bumpversion.files]] 166 | filename = "setup.py" 167 | search = "version=\"{current_version}\"" 168 | replace = "version=\"{new_version}\"" 169 | -------------------------------------------------------------------------------- /scripts/release/test_package.py: -------------------------------------------------------------------------------- 1 | from pathlib import ( 2 | Path, 3 | ) 4 | import subprocess 5 | from tempfile import ( 6 | TemporaryDirectory, 7 | ) 8 | import venv 9 | 10 | 11 | def create_venv(parent_path: Path) -> Path: 12 | venv_path = parent_path / "package-smoke-test" 13 | venv.create(venv_path, with_pip=True) 14 | subprocess.run( 15 | [venv_path / "bin" / "pip", "install", "-U", "pip", "setuptools"], check=True 16 | ) 17 | return venv_path 18 | 19 | 20 | def find_wheel(project_path: Path) -> Path: 21 | wheels = list(project_path.glob("dist/*.whl")) 22 | 23 | if len(wheels) != 1: 24 | raise Exception( 25 | f"Expected one wheel. Instead found: {wheels} " 26 | f"in project {project_path.absolute()}" 27 | ) 28 | 29 | return wheels[0] 30 | 31 | 32 | def install_wheel(venv_path: Path, wheel_path: Path) -> None: 33 | subprocess.run( 34 | [venv_path / "bin" / "pip", "install", f"{wheel_path}"], 35 | check=True, 36 | ) 37 | 38 | 39 | def test_install_local_wheel() -> None: 40 | with TemporaryDirectory() as tmpdir: 41 | venv_path = create_venv(Path(tmpdir)) 42 | wheel_path = find_wheel(Path(".")) 43 | install_wheel(venv_path, wheel_path) 44 | print("Installed", wheel_path.absolute(), "to", venv_path) 45 | print(f"Activate with `source {venv_path}/bin/activate`") 46 | input("Press enter when the test has completed. The directory will be deleted.") 47 | 48 | 49 | if __name__ == "__main__": 50 | test_install_local_wheel() 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import ( 3 | find_packages, 4 | setup, 5 | ) 6 | 7 | extras_require = { 8 | "dev": [ 9 | "build>=0.9.0", 10 | "bump_my_version>=0.19.0", 11 | "eth-hash>=0.1.0,<1.0.0", 12 | "ipython", 13 | "pre-commit>=3.4.0", 14 | "tox>=4.0.0", 15 | "twine", 16 | "wheel", 17 | ], 18 | "docs": [ 19 | "towncrier>=24,<25", 20 | ], 21 | "test": [ 22 | "hypothesis>=6.56.4,<7", 23 | "pycryptodome", 24 | "pytest>=7.0.0", 25 | "pytest-xdist>=2.4.0", 26 | ], 27 | } 28 | 29 | extras_require["dev"] = ( 30 | extras_require["dev"] + extras_require["docs"] + extras_require["test"] 31 | ) 32 | 33 | with open("README.md") as readme_file: 34 | long_description = readme_file.read() 35 | 36 | setup( 37 | name="trie", 38 | # *IMPORTANT*: Don't manually change the version here. Use the 'bump-my-version' utility. 39 | version="3.1.0", 40 | description="""Python implementation of the Ethereum Trie structure""", 41 | long_description=long_description, 42 | long_description_content_type="text/markdown", 43 | author="The Ethereum Foundation", 44 | author_email="snakecharmers@ethereum.org", 45 | url="https://github.com/ethereum/py-trie", 46 | include_package_data=True, 47 | install_requires=[ 48 | "eth-hash>=0.1.0", 49 | "eth-utils>=2.0.0", 50 | "hexbytes>=0.2.3", 51 | "rlp>=3", 52 | "sortedcontainers>=2.1.0", 53 | ], 54 | python_requires=">=3.8, <4", 55 | extras_require=extras_require, 56 | py_modules=["trie"], 57 | license="MIT", 58 | zip_safe=False, 59 | keywords="ethereum blockchain evm trie merkle", 60 | packages=find_packages(exclude=["scripts", "scripts.*", "tests", "tests.*"]), 61 | package_data={"trie": ["py.typed"]}, 62 | classifiers=[ 63 | "Development Status :: 5 - Production/Stable", 64 | "Intended Audience :: Developers", 65 | "License :: OSI Approved :: MIT License", 66 | "Natural Language :: English", 67 | "Programming Language :: Python :: 3", 68 | "Programming Language :: Python :: 3.8", 69 | "Programming Language :: Python :: 3.9", 70 | "Programming Language :: Python :: 3.10", 71 | "Programming Language :: Python :: 3.11", 72 | "Programming Language :: Python :: 3.12", 73 | "Programming Language :: Python :: 3.13", 74 | ], 75 | ) 76 | -------------------------------------------------------------------------------- /tests/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/py-trie/d92445c201c1552906c4a1551c1ca703883aef2e/tests/core/__init__.py -------------------------------------------------------------------------------- /tests/core/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import warnings 3 | 4 | 5 | @pytest.fixture(autouse=True) 6 | def show_all_warnings(): 7 | warnings.simplefilter("always") 8 | -------------------------------------------------------------------------------- /tests/core/sample_proof_key_does_not_exist.py: -------------------------------------------------------------------------------- 1 | # flake8: NOQA: E501 2 | # This proves that the key below does not exist on the trie rooted at this state 3 | # root. It was obtained by querying geth via the LES protocol 4 | state_root = b"Gu\xd8\x85\xf5/\x83:e\xf5\x9e0\x0b\xce\x86J\xcc\xe4.0\xc8#\xdaW\xb3\xbd\xd0).\x91\x17\xe8" 5 | key = b"A\xb1\xa0d\x97R\xaf\x1b(\xb3\xdc)\xa1Un\xeex\x1eJL:\x1f\x7fS\xf9\x0f\xa84\xde\t\x8cM" 6 | proof = ( 7 | [ 8 | b"\x01\xb2\xcf/\xa7&\xef{\xec9c%\xed\xeb\x9b)\xe9n\xb5\xd5\x0e\x8c\xa9A\xc1:-{<2$)", 9 | b"\xa2\xbab\xe5J\x88\xa1\x8b\x90y\xa5yW\xd7G\x13\x16\xec\xb3\xb6\x87S9okV\xa3\rlC\xbfU", 10 | b"\xd6\x06\x92\x9e\x0b\xd310|\xbeV\x9d\xb4r\xdf0\xa5Q\xfb\xec\xb9I\x8c\x96r\x81\xeb\xefX7_l", 11 | b"\xa8\x88\xed@\x04\x7f\xa6\xbe&\x89&\x89T\t+\xac\xb8w\x8a\xebn\x16\x0c\xe1n\xb4?\xad\x14\xfdF\xff", 12 | b"\xc9\t\xd0\xaa\xb0:P\xdc\xea\xedX%\x04\x9a\xbe\x1f\x16\x0cf\xbc\x04P#@\xfd\xd60\xad\xecK\x8b\x08", 13 | b"x\xff\xb2\x9ajO\xbc\x1bjR\x80$I\xe6\x95\xf6Tow\x82\xf9\x01\xa8V\xa9\xaa4\xa6`\x88\xf9\x10", 14 | b"I\x1cQc\x8a\xeda\xf8\xd1D\x01GT)\xc9\x02O\xef\x8d\xcc\\\xf9\xe6}\x8a~\xcc\x98~\xd5\xd6\xb6", 15 | b"U'\xa2\xa0 \xe4\xb1\xb6\xc3\xcd4C_\x9c]\xb3P\xa8w\xef\x8c\xde\xc2\x02^v\xcd\x12\xed%\x89\xa5", 16 | b"(\xa6x\xfa\xbe\xc3\x9a\xae\xaa\xe9\xbcv#u\\\xdfo\x14\x9a3\xbc\x89c\xc1\xfe\xdf[{|\x02P\x03", 17 | b"\xcf5\x07\x8f3\xa9\x1f\x19Q\xbb\x11\x8a\xb0\x97\xbe\x93\xb2\xd5~\xe2\xe06\x07\xc37\x08vg\x80 BD", 18 | b"U\x8e/\x95&\n\xc5\xf1\xd4\xc3\xb9\xa84Rd\xaa\x80\xfe8\xf1\xcf G\xcc\xe3\x99\x01\x07\xceH\x9a`", 19 | b"W\x1f\xb5\x1c\xec\xf7\x0b\x86\x15\r\xf9\xf9\x94\xcd|\xe6B\x9f\xa8l\x8d]D\xf7\xba\xee:\xc0\\\x11\xb8\x08", 20 | b"\xf5i\xee)\xc4\xd24\xfc\x8f\xba\xc0vS\x1dU>\xccz\xd18\n\xa2+\n\xcf\xe2i*\xee\x18\xe8\xc1", 21 | b"\x9dmSX\x1e\xee\xf7`\x1d\x0cO\xfcF\xe4\xbd\x0cE2\x10H6\xf0\x93|\xd5z\xe7=\xebbJ\xd6", 22 | b"u\x08\x92\x08\xa5Nl\x938\x03\xa3\xe2O\xe8\xfe\xb1\xc4\x87\x8c\xb8q\x9eb\x89b\x96\x98\xd7\xf22\xb9\xa2", 23 | b"\xa6V\xb5?\xcc\xd2\xc8*ME\xe7\xcf\xf8\xad\xf8\xdb\xe7\xf8\xf6D\xd5<\x1c\x95F\x13\x0e\x06rz\xe5m", 24 | b"", 25 | ], 26 | [ 27 | b"\x8d\x0c\xca\x062\xc1Q\x99\xf4\x1bIL\xa0\x13Ec\xbbZ\xa1w\xfd\xab\x99\xc8\x9bu_\x03\xaee7\xd7", 28 | b"\xf6\x8e\xd5f\x18\x04t\xf5\x8a\xcf@\xdaU\x1d\x0f\xd5\xf0cH\xe3\xb7\xd3\xed2K\xdc\x84\xafxW\xb5\x19", 29 | b"\xc6j\x95SAN\x14]Q\x031pJ\x95^\xa7\xf0h\x05\x04\xd7\xc7\xf4\xab0fk\xeceL\xc1\x11", 30 | b"\xff\xf1&z\xb5\xa7+)\xfe\x95\x13F\xd9G\xae\rF2\x08[.\xdf\xca\x11\xcb\xab\x1a!\xc5\xabI0", 31 | b't\x9a8\x89\x98\xcd\xc2qu\x98>d\xa1x3\xfa\x839l\nT\xfe\xc8r"\x1cQ\x8b\x9bc %', 32 | b",JY\xbe\xf5\xe7*]\xc2\xe7\xfa\x02E6QV\xaa\x86\xe4\xd5\n!M%9\xbct\xee\xf93\x89#", 33 | b"0\xe70\x08^\xd4\td\x15\xf2Kz\xc1\x14\xa9G/\xc8$R]\xbb\xad\x82\x8d\xb2\n$\x8c\xf1#N", 34 | b"6\xec@\x8b\xa4$>\x9d\xfcO;l\xa32I\x0b\xff\x99\xc5F\xd5\r\xaf\xeb\xf7\xe9\xd1\xf1>\xde\\\xec", 35 | b"\x1b,:\x1c\xa96\x14L!\xf3K\xa8\xa9q[\x00di\x92\x14(H\xaeUG\xfeR\xfc\x1fE-\xea", 36 | b"\xdc\xc45\x9c\xb6\x7f\xa5\xb0a`\xd5\xe5@\xb1\x98J\xabI", 50 | b"\x93\xde\xde\x01\xd7+@\xda\xb4\xa9\xa9\x06[\xd5![3eT!.\x06\xe8\xac\xd7v\xe3\xa9\x01\xdc\x12\xa5", 51 | b"\x14\x1dx\xa7\x9b\x85\xfeB\xe05\xc9f\x8f\x9b'\xf9%\x06\xb4\x8f$D\xda\x96\xba\xd4\x06nv\xfd\xa9h", 52 | b"p8\xad\xb7\x18G\xc4\xf2/\x16\x7fFf\xa6\xf633;\x15f\x17\x80\x16\xdf@\x82(\xb6\x1c\xa8a\xa8", 53 | b"\xee\xdb\x9d\x96gQ+\xa2\xcf8\x87\x19\xa0\xc2\x8e\xed\xad\xdf\xdd\x02\xd6\xd9\xc1L\xee\x00\xc8\t!\x1c\x8c\xbc", 54 | b"/`!\t\x0b\x86h\xb2X\xb0\xa8\xa9\x90\xd7\xb8O\x14\xfew\xf4?&\xa1\x04\xc5\xaf+%W\xb7p`", 55 | b"\\\x05\x0e\xfay\x0c\xb5\xd5\x88\x82'\xc2O#\xcc\x93\x1a\xe4\x05a\xe7C\xad\x18*\xf6[\xba\x1f\x8du\xf3", 56 | b"\x8b\xf9\xb9\x05\x82o6\xcfq\xa4\xb9\xd1\xf2\x88\xb7u\xb3\xaf\xf9\xc5!\x91\xf8\x96\x00\t\xec\xf5\xdc\xf5>\xbd", 57 | b"\x85u{\x9a-{\xd249\x08T\x95\xd9\xbe\xcd'\xc9\x0f0\r\x91\xac\x13\xb1\x8fmo\xdf7BO\x91", 58 | b"\xa8\x9b\x94E3J\xc0\x849\xd7\xbc\xbe\xda`f4^\x0e$?E\x06\xf4\xcd\xda\xae\xdax5\x81\xf4\x1c", 59 | b"Uf\xcf\x806\x1c\xf2\xb9}T\x8b}\x91\xbe\xd2\x0e\x1e\xb25\xf7C(\xf3\xef\x9e\x80\xa2\xa3I\xa3j\x08", 60 | b"i\xaatX\xd6V\xd8\xf6\x9dV\x8b\xb0V\xd0\xb4_D\x0fO\x99J\xcf\xb8\xe42X#\xcbw\x96=5", 61 | b"w\xb0\xfa\xb8\xd7-\xd6\x16\xa7\xe1\x9f\x92\xe3\x96[\xd6\x18\x83\xb7\xf9\x177\xe36\x01\xeeR\xac\xe4\xb3\xd5\xba", 62 | b"", 63 | ], 64 | [ 65 | b'\xd5\x12\x033\xdd\xba\x05f\xc4\x10?\xea"\xd5\x93\x15\x9f\x16\x83\xa0\xfd\x84p\x80\x9dd\x07\xd2\n[o\xaa', 66 | b"\x8c\x98B\xedD6\x8cz\xd5\x8d\xd4\x9a|us\xb6lG\x98T\xcc\x0b\x9c(\xfa\x1e\xed\xb7\x1e\x86\xa2A", 67 | b"\x90\xbew-\xfa\xb6GH'\xdf#\x13\x8f\x06H\x05<\r\xdbf\xd0\x99M\xd2E1\x8d\xb3].\x05\x88", 68 | b"H\x19\xcei\xd8\xe2/\xe1G\xd9p[3\xad\xc8\xf9\x8c\xa7mg\xd5\xc1\xd1\x1a\xa6\x94|+\xc7\x12\x10\xba\x1e\xc4w', 76 | b"\xb1\xe0\x11^z\xc5\xecy\x82`k\xc5UxrdDi\xde\x07y\xc9h&\x95A$\xd3\x15bN\xcd", 77 | b"\x8bZrK\xef\x8a\x1c\x8aI\xaf\xfa\x1e\x9c\xcc\x1cq\x01Yh\x9d\x04;\x14\xda\x91\xba\x95\xcb\x14\n\xe5\xd3", 78 | b'=\xfc\xeb\xc5\x8e\xf3!"\xc3=,\xcf.:>DY#\\\xab\xe48\x0e\xd6\x17\x1fP\xe8yxAy', 79 | b"o\xb1\xbd\xc4x\xdd\x9av`\x12\x95\xfb\x7f\xbd\x88m\xeb\x7f\x9a\xa0\xe0\xda\xf0y\xc6\xa9S\x87\xe1\xe4\xda0", 80 | b"\xefp\xc6\xe4q\xe5!\xe0l\xbb\xc3\xf1\xed\xe6Y\xc8\x8a-\xcc\xbe\xd6x\x0e\xc7\xf1o\xc5\xd91\x96\xd4\xd3", 81 | b"", 82 | ], 83 | [ 84 | b"\xc4|$\xed\xaf\xde\xdc6R4\x02*\xe1OX\x15\xea\x05\xae=p\x82\xd7\xc5%Lx\xe9\xcdBG\x11", 85 | b"5\xd4x\x9c\x04\x92\xb1}\xa7\xdd>\xb6\x80r\x9a\xf5\x8f\xcfWG\t\x905\xe6\xa2\xf99l\xca\xd3\xec\xf8", 86 | b"u\xb6d\x1a\x82\x9e\xf8\tNq+\x03\xdaS\x1c\x1ahi\xd8\x85:\x8b4\xfc\xe8\xe4\xae\x12\xa2\xe2\x9c\xd2", 87 | b"a\xe6\xdf\xf7\xcd\x8e\xc8\xb6\xd76=H\xac\xf1J\x1f\xe5a\xe9-\xb23B\xc7(y\x1aF\x8c,x\xfd", 88 | b"\xe2rd\xcb_\x08\x8e\xfd\x1es\xb9\x05\x01\x82H\xd2\xdcyJF\xa71G\xa5\xa4q\x94dw\xadv\xf7", 89 | b"|\xe4\xed\n\x06E\xd28\xa9\x04A\xe1\x91\x1fb\x1b9\x82\xfa\x15xB\x9d\x14QUix~\x1a\x89\x80", 90 | b"\r\xf9k1\xeeN\xabE\x94}n\xe6\xb1\xafJ\xc9\xfe\xa1\xf0(\xffa\xe5\x8em4?1\x1a{q\x10", 91 | b"\xd3p7\x17j\xf4\\\xa1\xf3\x01p\x9c`i\xfb\xc1\xb9Hf\xa2ZTl\xe4\x95\x1a:\x12k8\xb0N", 92 | b"\xf4$\xf5\xb9\xbd4\xb5\x97\xd3\xf5(\x13\xfe\xfd\x7f\xd1\xa6\xf2v\xa5H\xcb\xfb\x19i:\x9b\xe7^\xb8\x07J", 93 | b"\x88\xae9\x87\xcd\xcc\x93'&Xm\x02\x1bdN\xcfP}\xf4\xd1\x0fy\xf02,\x8d|=\xbb\x0eLQ", 94 | b"\xb9\x9d\x18hQ\xe4\xa5\x13T\xc2\xd5\xf6\xd8\xb9v9\x0b:\xedv\xedu\xa5!\xf79\x0f\x89\xc8\xce\xb3", 113 | b"", 114 | b"\xcd\x89K\xab\x92\xa6ct\xc4\xc0\xeaN1A\\\x93\xee>9\x07\x0bI\x85i\xfbX\xbd\xe5\xc8\xa83\x85", 115 | b"\x9aIL;\x8b&G\xff}\x92T\xd6\xcc\x95v\xa0i\xe2\xcc\xd4p\x06\xb4(}\x0f\x89\xff\xa5\xec \x88", 116 | b"", 117 | b"", 118 | b"", 119 | b"", 120 | ], 121 | [ 122 | b" \x17\x18\x08\xd8\xa4%\xd26\xd0%T\x8aX\xbf\x8b\x8c\x95Kw\xaf\xa5M\xb8\x18Q\x81t\x9eK", 123 | b"\xf8D\x01\x80\xa0V\xe8\x1f\x17\x1b\xccU\xa6\xff\x83E\xe6\x92\xc0\xf8n[H\xe0\x1b\x99l\xad\xc0\x01b/\xb5\xe3c\xb4!\xa0\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", 124 | ], 125 | ) 126 | -------------------------------------------------------------------------------- /tests/core/sample_proof_key_exists.py: -------------------------------------------------------------------------------- 1 | # flake8: NOQA: E501 2 | # This proves that the given key (which is an account) exists on the trie rooted at this state 3 | # root. It was obtained by querying geth via the LES protocol 4 | state_root = b"Gu\xd8\x85\xf5/\x83:e\xf5\x9e0\x0b\xce\x86J\xcc\xe4.0\xc8#\xdaW\xb3\xbd\xd0).\x91\x17\xe8" 5 | key = b"\x9b\xbf\xc3\x08Z\xd0\xd47\x84\xe6\xe4S4ndG|\xac\xa3\x0f^7\xd5nv\x14\x9e\x98\x84\xe7\xc2\x97" 6 | proof = ( 7 | [ 8 | b"\x01\xb2\xcf/\xa7&\xef{\xec9c%\xed\xeb\x9b)\xe9n\xb5\xd5\x0e\x8c\xa9A\xc1:-{<2$)", 9 | b"\xa2\xbab\xe5J\x88\xa1\x8b\x90y\xa5yW\xd7G\x13\x16\xec\xb3\xb6\x87S9okV\xa3\rlC\xbfU", 10 | b"\xd6\x06\x92\x9e\x0b\xd310|\xbeV\x9d\xb4r\xdf0\xa5Q\xfb\xec\xb9I\x8c\x96r\x81\xeb\xefX7_l", 11 | b"\xa8\x88\xed@\x04\x7f\xa6\xbe&\x89&\x89T\t+\xac\xb8w\x8a\xebn\x16\x0c\xe1n\xb4?\xad\x14\xfdF\xff", 12 | b"\xc9\t\xd0\xaa\xb0:P\xdc\xea\xedX%\x04\x9a\xbe\x1f\x16\x0cf\xbc\x04P#@\xfd\xd60\xad\xecK\x8b\x08", 13 | b"x\xff\xb2\x9ajO\xbc\x1bjR\x80$I\xe6\x95\xf6Tow\x82\xf9\x01\xa8V\xa9\xaa4\xa6`\x88\xf9\x10", 14 | b"I\x1cQc\x8a\xeda\xf8\xd1D\x01GT)\xc9\x02O\xef\x8d\xcc\\\xf9\xe6}\x8a~\xcc\x98~\xd5\xd6\xb6", 15 | b"U'\xa2\xa0 \xe4\xb1\xb6\xc3\xcd4C_\x9c]\xb3P\xa8w\xef\x8c\xde\xc2\x02^v\xcd\x12\xed%\x89\xa5", 16 | b"(\xa6x\xfa\xbe\xc3\x9a\xae\xaa\xe9\xbcv#u\\\xdfo\x14\x9a3\xbc\x89c\xc1\xfe\xdf[{|\x02P\x03", 17 | b"\xcf5\x07\x8f3\xa9\x1f\x19Q\xbb\x11\x8a\xb0\x97\xbe\x93\xb2\xd5~\xe2\xe06\x07\xc37\x08vg\x80 BD", 18 | b"U\x8e/\x95&\n\xc5\xf1\xd4\xc3\xb9\xa84Rd\xaa\x80\xfe8\xf1\xcf G\xcc\xe3\x99\x01\x07\xceH\x9a`", 19 | b"W\x1f\xb5\x1c\xec\xf7\x0b\x86\x15\r\xf9\xf9\x94\xcd|\xe6B\x9f\xa8l\x8d]D\xf7\xba\xee:\xc0\\\x11\xb8\x08", 20 | b"\xf5i\xee)\xc4\xd24\xfc\x8f\xba\xc0vS\x1dU>\xccz\xd18\n\xa2+\n\xcf\xe2i*\xee\x18\xe8\xc1", 21 | b"\x9dmSX\x1e\xee\xf7`\x1d\x0cO\xfcF\xe4\xbd\x0cE2\x10H6\xf0\x93|\xd5z\xe7=\xebbJ\xd6", 22 | b"u\x08\x92\x08\xa5Nl\x938\x03\xa3\xe2O\xe8\xfe\xb1\xc4\x87\x8c\xb8q\x9eb\x89b\x96\x98\xd7\xf22\xb9\xa2", 23 | b"\xa6V\xb5?\xcc\xd2\xc8*ME\xe7\xcf\xf8\xad\xf8\xdb\xe7\xf8\xf6D\xd5<\x1c\x95F\x13\x0e\x06rz\xe5m", 24 | b"", 25 | ], 26 | [ 27 | b"\xb3\x03\xa9\xc11\x87mQ\xa1I2D4jg\xfe\xd0%k\xf2\r]\xb0\x0e\xeb'\x17\xedx\xc9Uj", 28 | b"L/\r$7-\xa5\xdf x\x9c\xbc\xc4\x99\x1e\xc5\xd8\xb5\xaf\xd1\xd1\xae\xe6L\xeco\xc4\xe2RUe\r", 29 | b"\xbeSp\xf5\xef\x02\xcd\x83\xb2\x0b\xa06\xfd\xca\xbb\xed_\xf2}\xf7\xea\xb3\x84\x17\xed\xcc\x19mF\x13(\xf3", 30 | b"\xfb$IYR\x9f\x04p\x01\x1d}\x88\x0b\xed'\x8e%\x9b\xc9\xeaN_\xab\xf9\xc9\x9d\xac\xa9\xb3\t\x1eq", 31 | b"\xaab\xeb\x14\xc2\xf6}%\xaa+0\xb5\xc1\x0f< \xc5ma\xb1c\xeb\xdd\xca\xc0\x90\xe2L\x8b\xe9\xfe/", 32 | b"\x91l\x9d\xa2\x84\xbf\xc1\x05\xe2S\x0e\xc9`\xc0^}Q!\xc4ml-\xec\xf4R$\xf6\x8a\xd3\xc6\xf1j", 33 | b"\xf3\x13\xde\xe0L\xdb\x96E`Q\xdf\xa1\x13\x01b5\xe4k\xde\xde\xbf\xb10\xaf\xe61Z\xdbZ\xd47\xf4", 34 | b"\t\x81\xb0\xea*\xec\xd0\xc3\x16\xee\xed~\xdc\x98e\x90\xf2~p\xbbSY\x19\xcfl\xc4)\x01\xc2\xd9\xc91", 35 | b"-\xda%\x8a\xc5jA-\xe5 lIp\xbe\xb3h\x98\x0f\x80q\xed\xab\x89KN\xdd\xa6\xcb;\x98\xb08", 36 | b"\x13\x97\x12f\xa31\xfa}\xf1\xfe\x19\xfa\x0b\xe6\x89\x9a\xcb\xf5\xed\xf3Q\x98O=\xa3\xb0e/\xd9\x9fy\x08", 37 | b"f\xba%\xfb\xbfE\x1d]\xb3\x05\xe4$\xa5\xd2G\xecc\xe5#\x0f,\x91\x8bN9a\x8a\xd1L\x16l\xa5", 38 | b"#p\x15\x8bU\x04\x88/K|4a\xfc\x0e.Zm^{\x15uk\x8d\xe4_\xfe\xee\xae\xb99\xd1\x8e", 39 | b"C \x9f\xb3y\xf3d.\x8b\t\x1cF\x9eL\x08\x07y\x08\xb9\xe1\xffM\x87\xfd\xd6\xfd\xdb\x8f\x94\x9e\x88\xc2", 40 | b"\x17X\x1f/\x8b\x82\xf5\xe4\x02\x84}\xbe\x9bz` \x94'\"_\x9c\xff\x06\t>\x8a\xd7oK\xf9\xf5w", 41 | b"6Q\x8db\xd8\\\x84_Rin\x18\x1f\x17\x89\x7f@\xd6\xbb%>\xafa'\x80A\xa7\xd8}d\x07\"", 42 | b"\xccgm\xf7\x05\xc8\xe4G\xf4\xb3\x18\xc7\\.\x0b\xa25]\xdc\x80w\xda\xc9;\xde\x9b\x03\xa0LS\xce\x8c", 43 | b"", 44 | ], 45 | [ 46 | b'\xe4\xd3\x15\xe0\xaa\x0f\xf9\xd0\xa6\xc2\xc8B_\xaf"0\x8c\xea;\x91\xe4E\x04\xec\x901yZ\xd6>\xadc', 47 | b"wM\xce\x16JS:\xe96\x98\x12|\xa0\xc9~G\xbb\xc7u8\xc8\x93\x9b\x05\x92yh\xaa\xda\x94NK", 48 | b"\x89\xc7\xa2\xbd\xe1\xda\x06$|\xde\x03\xd9RS\x90\x84\xe7\x05\x0cc\xdfy\xb0\xfb@\x065\xdb8\xa9\xef\x1f", 49 | b"@\x11>\xe8\xb8\x19\xb7\xc7@\x92m$\x93 \x08\xc5\x15\xbd\x97\xb0;\xf5\x05q;\xb5\xc69\xd3E\xc4\x0e", 50 | b"\xd5_ol\x05o\x8e\xf0V\xd2\xa0n\xe7CxR\xc9\x92HTQhkc\x10K\xad\xfdU\xe9\x97\x8f", 51 | b"v\x7f\xc5KB\xdaYS\xa1\xbf \xda\xe2\x99\x84\xef,\x92\xdd\xc9\xb8\x9eo\xfcv(\x95\xff\x94t\xbc5", 52 | b"\xcbQ\x962!$\x1f\xdc\xdb\xfe\xef'\xc8\xc8O\xec\xa2\xae\xd3P\x88\xbf\xbd!\xea\x0e\xb0\x89\xe9\xdd\xf3w", 53 | b"H\xb8\x1b\xc3&\x86|!o\x003/\xc7K\xc9+,K\xe1y\xf2\x86\xa9*H\x05W\xcd\xf8\x8b\xb5\n", 54 | b"\x06\xc5\xa1\x83\xe4\xb4\xdc\xbf\xc0\x8c4Q\x93\x14W\xaf\xbb\xe9f\x82\xa2\x8d\xa3m\xda\xed\xc0W\x88UA\xd9", 55 | b"\x9czV\x7f$\xa8\xb9\xf3\xc1W0\x19\xac\xc5\xaap\x03?*\xe6\xd6\xee<\x0b\xafr\xf6ji\xd9\x87\xed", 56 | b"\xc7\x1d\xca\x95\xab~\xd3|\xa6\x9f\xba\x9e\xd5KxI\x95Y\xadx\xb8\xda\xa7!\xba\x93\xbbB,\x97n\xe4", 57 | b'\xd7"\x13\xca=\xa9|e\x11\x8f%\xb2^\x1b\xa6\xff\x93Z\x8b(\xca\xab\x12\xed\x8b3\x0f\xe0\xa7U\xa9\xe1', 58 | b"\xc2\xb4\x98\xb7\x08\x18#i7\x81\x85\xfd\xc3\xc6k\x12\x86\x99\xa55\x0c8\xd3\xbc\x9d\xc8\xe0\xd3\xcd=\xc6x", 59 | b"\xad\xf0\xea&\xf4\x8f=5\xe1\xb5b\xc1}\xba\xa1\n \xa4\xb7J2\x1f\xd7\xc9\x1d\xa4\xc2\xaf\xb7O\xb2\x12", 60 | b"\xd5~\x94\x99~Vy,4\xedMJ\x1a\xda3\xe7\x90\x91\xd4\xafw\xba\xbf\x89`\x0e\x99s\x93E\xdf%", 61 | b"\x82\xd2O\x16\xca{\x15\x87\xef-\x8a\xea\xb9\xcd\xfc\x82\x84\x99\xdco\xc1\x1eg\xf3-\x07\xf8\xa3\xed\xffx\x85", 62 | b"", 63 | ], 64 | [ 65 | b"\xc5\xa5\xd38zu\xfc\xe9\xe2j\x97\xf0\x81T$\xee5\x94AC\xb1\x85\x0c\xef\x10\xcb`Z\xfcT'\xcb", 66 | b"ZU\xe4?lj\x05\xf8\xbc\xa7\xf1\xe4\xdb\x08M\x06\xad\xbf\xb3s\xfa\xcaS\xb9{U\xd2n\x981+|", 67 | b'l\x0cL\xfb\\(g\xb47\xc2<\xcb\x14\xf3\xa9l\x01#\xdb"|\xdc\xfd\xa0#\xa2\x89\xcfx\x97\xb4\x8e', 68 | b"\x0b\xe7$\x1d\xa2\x1c\\\xa5)t\xd6\x82\xec\xed\x02]\xdd\xefz\xa3C`\x1b\xda\x81\t\xb3\x14\xdf5\xbb\xcb", 69 | b"\xe7%b2\xd4\xc6\x98\x90\xd8:B\xa4\x9e\n\xc6\xa1\x01\xac\x94\xbdr\xca\xdd\x8a\xa8\xe8\xc6F\xed\x04\xe9\x14", 70 | b"\xa7\xac\xc0S\xcbo\x98\xebJ)\xb1\x8b{\xda,\x98\xf2M\xca,\xcd\xc4%\x94\xe4\xdc<\xf5o}\x90\x1d", 71 | b"[\xd9}F\xe2\n\x84\xbc\xa0\x81\x0f\xb9\x0b]\x0c\x10%\x9d\r\x00RZgbV*2b\xd1z\xb5\xd3", 72 | b"\xac\xcag\xdb\xc3y\x91\x82\xddu\xad\x85%g\x82\xa0\r\xf4\x99^=\x14h\xee\xac\x81/o\xe6\xe4\xec\x0c", 73 | b"8\xeb\xed\x80}2\xd9.\x0e\xeb\x92\xa7\xae\xeb\x8d\x9b>8<\x9d\xc4\x05\xf2W;F\xce!\t\x15\xb2\xe3", 74 | b"*\xed\xbfJ\x80\x9f7\xd1\xcd\xeft\x89.e\x02M\r\x85D-\x9bL\x8d\xac*3h\xf3\x9f\xde\xe0F", 75 | b"d\xf9\xdf\xfb\xfa`\x97:\x11\xc4\x89u_\xe9&\xd0LX;r\x12\x86\\,}\x7f:\xbc\xf9\x9a\xd2\xe9", 76 | b"\x94\x80\xd4\xb8\xe4\xa6\xd4\x9cS\xcc\xc7*xo]2y~\xd6\x18a\xfb\xafP\x19\x87\xe7:\xb1r\x96\xdc", 77 | b"\x1c\xdar\xc1\x18\x1f\x0b\xf3\xe2\xf0\xf1<\x05\x88\xa4\x01J,\xc2\xa1\xbd`L\x8b\x95\xa6\xbdze4&\xc1", 78 | b">0\x01SdF=\x8c\xa7\x1d4\x1elOt\xcd;,|\xf0l\xe9O\x83\xf3\xc0rm\xb6\x82\xaa\x08", 79 | b"\xd0\xef\x12\xc5<\\\x00\x82$\x98\x8d\xb6\xa7l\xd6w\xa3\x00\xb1\x80jz\xc3\x8a,5\xb8\xf8\xbf\xb4^\x880\x824A\xfa\xbf\x0e\x1f\x9b /\x02\xadhx", 81 | b"", 82 | ], 83 | [ 84 | b"\xc1\x17\xa1{\x135'>\xce\x8a\xe8;\x84V\x8c\xfer\xdaZS\xc7v\xd7\x18\xfb\xe3\xbf\xff\x92\x87@D", 85 | b'\x06\xb9c\xad\x8d2\xc0WU\xaf"w\xe5>\x1a\xfd\x02\xf1\xdd\x91$h/\x02)\xc6\xd3\xbc\x17\xc42\xe8', 86 | b"\xc4\xa2\xb3*k\xa8\xc8\x124\x86\xa0\x9b\xad\xfa\xb9$5?\xc6\x0c]\x98Kb\xd13\xdb:\x85\xed\xe1[", 87 | b"%\xa4>aM\x08\xbet\x1b\xc8\xb5\xf2c.9o!\x03G\x99_\n\xef\x93OA^\xabC\x91\xce\x97", 88 | b"\xc9T\xc1\xf6\xc8\xbe\xd8h\x86\xfey\x82Evg\xe1zP\x9ct\x98(\x01\xf5\xfc\xf8\xbe\xf6\x1d\xc0\x15\x8e", 89 | b'\xd3\xf1\xe6T\xd7"\xba\xdeipC\xe5\xe1\x04\x0e?o\x84\xcb\x1aE\x18\xd0\xa36\x0eC\xc7D>\x12 ', 90 | b"\xe0\x06\x0c\xaf\xec\xe3op*j\xcd\x84\xef\x9b\x82a{,\x1c\x98\xba-\x10\xf9\x7f+\xb6\x8a/q,\xeb", 91 | b"\x8a'\xeb\x1a\xe8i\x91S\xf3;\xa8[f-\xb02\x01?\xac\xe4Ds\xd8E\xa0\x87\x8a\xec]\x9b?\x9e", 92 | b"\xcf\x0cM\xbd\x92\xbbaS\x9d\xd0:\x7f\xfe\xd5\x08\xac\xe4\xb5\x81ga\xc2>\\\x89\x95\x08\xd6C\xf9\xe6\xb7", 93 | b"\x9bh\xd3\xb0x\xf0\xfa5\xa6vV\x96_\x16\x9dx\x95B2\xa9\xcem\xc8\xb9\xaf\xb9\xff\n\xae\xc7\x14\x13", 94 | b"H\x03\x82\xd6\xbd\x00Z\r\xa03YQ\xa4\xfa\xcdl\xea8g{L\x16\x18\xca\xdb\xb75~\xff\x1b]&", 95 | b"A?l1\xbf\x04\xc3Qs\x9b\x08c\xc3|\xf5D6\xa2\x82\xf8\xd3\xf4@\xab\xa0oDx\xc4\xffY*", 96 | b"\x0c\xd7U\x880\xa0\xd3\xad\xdd\xda\xdb\x01\xac\x99ya:\xeb\xab8K%\xaf\xc4\xf1G\xd3*\xb7\xae\x01*", 97 | b"\xb8s\xab\x0e\xf4\x90\xdb\xce\x0b)l\xb3\x7f\xf1p\xc6&\x0eh\xfb\xc8\xd7\x88`\xcd\xdc\x97-l\xb6L\x82", 98 | b"x\xf2\x15\x85\xe9\x01\xd8\xdc\xc5\xbc\xb7\xda\xcd$\xf0\xae\xc9\x01\xcdHZ\xb8)\x97\x11\xff\xcc7\xa5\x98\xb4\xb6", 99 | b"\xf3\xb6\xdd\xe9\xb1\x93\x08A\xda\xa39\xfe$\x8dO\n$ Mn\"-'\xa5$F5\xae\xcd>\xa2\x0c", 100 | b"", 101 | ], 102 | [ 103 | b"\x82\x8b\x9d\x85\x0b/\x83\xacmb\x07\x89h\xa5\x86R\x8e\xf4\xd9_\x00\t\xeb\xb3>\\@\x11\xecOp\x7f", 104 | b"", 105 | b"", 106 | b'"\xee\xd9\x89<\xc3_\xca\xe9\xed\xc2v\r,\x9e\x10\x1c\x07\xe8E\xbd\x10\x9a\x16_:hk\xb9Om\xf2', 107 | b"", 108 | b"", 109 | b"\x11]i\xb3t6\xabKF\xc0\xa9\x81z&\xdf\x02\xcaRQ\x82\x92\xac\xf1\xf9~\x94\x94tM9\xbe\x1a", 110 | b"\xd0dY\xbc\xbe\xe5\xa8\x93\xc8e\xbd\x15\xf8\xb6b\x9a+\xbeh\xeb\x9d\x85\x1f(\xee\xd5\xb2 \xf2\xea\xa1\xf2", 111 | b"", 112 | b"`\xa8\xcd0:I\xdd\xd7\xa1\xc9W\r\x00\xa6\x1b\x0cM\xbb8\xb0Z\x8b\xe2\x87\x16\x0f\x99U\xf7\xdf\xc4U", 113 | b"", 114 | b"\xbcR\x17x\x12Y\xf1r\xb9c\xf5\x17#\xcd\xdb\xd5\x1c0\xd2\xda~\x99a\x96\xd5k\xef\x94\x0f\xd0$\xcb", 115 | b"!\x16\xaee\xb5H7X\xd5\tA\xb5{\x98\x8f\x12\x0bX\x85K\x184\x04\xcf\x80\x17\xf81V\xbc\xed\x9c", 116 | b"\x00\x08C^\xb5\xcfb\xb3\x13\xf0\x95S\x8eyQ\xe8\xdf\x9bI\xfe\xa2\x9c\x91@_\x16\x9d\x82w,u\x86", 117 | b"6&\x99Z\xae\xe6r\xab\xec\xb3X\x87\\\x02\x99>\xfa\xebP:\xd5\xd2t\xe2p\xc7\xe2\xe0\x0e\x95\xf9D", 118 | b"\xcf\x7f\x99\x9a\x1c\x18\xa6\x9av\xe6\xa2\xd5\xb3E\x8aJ\x18\xa7\x8c\xc0\x07\xda\xe9\x0bi\r\t\x0f\x9b\x06\xf8S", 119 | b"", 120 | ], 121 | [ 122 | b'\x07\x83C\xd1X\xdf\xddJ\xd4\xf2\x7f3+\n\x95\xb2\x89\xd2"\x9d\xc5S\xfb\xfc\x9ed\x8d\xd2\xd2\xe5\x99B', 123 | b"", 124 | b"", 125 | b"", 126 | b"", 127 | b"-m2\x00\xef\x95\xcd\xfe\xf8\x9e\x0b\xbf\xae\xd8\xb4\xd2\xa1*\xfde\xaa\xb1\x8a\xdd\x1d\x07\x03\xc7,<\xe8\xe7", 128 | b"", 129 | b"", 130 | b"", 131 | b"", 132 | b"", 133 | b"", 134 | b"", 135 | b"", 136 | b"", 137 | b"", 138 | b"", 139 | ], 140 | [ 141 | b"8Z\xd0\xd47\x84\xe6\xe4S4ndG|\xac\xa3\x0f^7\xd5nv\x14\x9e\x98\x84\xe7\xc2\x97", 142 | b"\xf8D\x01\x80\xa0U\xbd\x1daQ\x97{bg,!\xc2uK\xbe\xeb;\x82x\xb2\xe0\xc3\x8e\xdc\xd9I\x84n\xe3b\x8b\xf1\xa0\x1e\x0b*\xd9p\xb3e\xa2\x17\xc4\x0b\xcf5\x82\xcb\xb4\xfc\xc1d-z]\xd7\xa8*\xe1\xe2x\xe0\x10\x12>", 143 | ], 144 | ) 145 | -------------------------------------------------------------------------------- /tests/core/speed.py: -------------------------------------------------------------------------------- 1 | import cProfile 2 | import itertools 3 | import pstats 4 | import random 5 | import time 6 | 7 | from trie import ( 8 | HexaryTrie, 9 | ) 10 | 11 | 12 | def mk_random_bytes(n): 13 | return bytes(bytearray([random.randint(0, 255) for _ in range(n)])) 14 | 15 | 16 | TEST_DATA = { 17 | mk_random_bytes(i): mk_random_bytes(j) 18 | for _ in range(128) 19 | for i, j in itertools.product(range(1, 33, 4), range(1, 130, 8)) 20 | } 21 | 22 | 23 | def _insert_test(): 24 | trie = HexaryTrie(db={}) 25 | for k, v in sorted(TEST_DATA.items()): 26 | trie[k] = v 27 | return trie 28 | 29 | 30 | def _insert_squash_test(): 31 | trie = HexaryTrie(db={}) 32 | with trie.squash_changes() as memory_trie: 33 | for k, v in sorted(TEST_DATA.items()): 34 | memory_trie[k] = v 35 | return trie 36 | 37 | 38 | def main(profile=True): 39 | print("testing %s values" % len(TEST_DATA)) 40 | tests = [ 41 | ("insert", _insert_test), 42 | ("insert squash", _insert_squash_test), 43 | ] 44 | for name, func in tests: 45 | profiler = cProfile.Profile() 46 | if profile: 47 | profiler.enable() 48 | 49 | st = time.time() 50 | trie = func() 51 | elapsed = time.time() - st 52 | print("time to %s %d - %.2f" % (name, len(TEST_DATA), elapsed)) 53 | 54 | if profile: 55 | print("==== Profiling stats for %s test =========" % name) 56 | profiler.disable() 57 | stats = pstats.Stats(profiler) 58 | stats.strip_dirs().sort_stats("cumulative").print_stats(30) 59 | print("==========================================") 60 | 61 | st = time.time() 62 | for k in sorted(TEST_DATA.keys()): 63 | trie[k] 64 | elapsed = time.time() - st 65 | print("time to read %d - %.2f" % (len(TEST_DATA), elapsed)) 66 | 67 | 68 | if __name__ == "__main__": 69 | main() 70 | -------------------------------------------------------------------------------- /tests/core/test_bin_trie.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from hypothesis import ( 4 | given, 5 | settings, 6 | strategies as st, 7 | ) 8 | 9 | from trie.binary import ( 10 | BinaryTrie, 11 | ) 12 | from trie.constants import ( 13 | BLANK_HASH, 14 | ) 15 | from trie.exceptions import ( 16 | NodeOverrideError, 17 | ) 18 | 19 | 20 | @given( 21 | k=st.lists( 22 | st.binary(min_size=32, max_size=32), min_size=100, max_size=100, unique=True 23 | ), 24 | v=st.lists(st.binary(min_size=1), min_size=100, max_size=100), 25 | random=st.randoms(use_true_random=True), 26 | ) 27 | @settings(max_examples=10, deadline=1000) 28 | def test_bin_trie_different_order_insert(k, v, random): 29 | kv_pairs = list(zip(k, v)) 30 | result = BLANK_HASH 31 | # Repeat 3 times 32 | for _ in range(3): 33 | trie = BinaryTrie(db={}) 34 | random.shuffle(kv_pairs) 35 | for _i, (k, v) in enumerate(kv_pairs): 36 | trie.set(k, v) 37 | assert trie.get(k) == v 38 | assert result is BLANK_HASH or trie.root_hash == result 39 | result = trie.root_hash 40 | # insert already exist key/value 41 | trie.set(kv_pairs[0][0], kv_pairs[0][1]) 42 | assert trie.root_hash == result 43 | # Delete all key/value 44 | random.shuffle(kv_pairs) 45 | for k, _v in kv_pairs: 46 | trie.delete(k) 47 | assert trie.root_hash == BLANK_HASH 48 | 49 | 50 | @pytest.mark.parametrize( 51 | "kv1,kv2,key_to_be_deleted,will_delete,will_rasie_error", 52 | ( 53 | ( 54 | (b"\x12\x34\x56\x78", b"78"), 55 | (b"\x12\x34\x56\x79", b"79"), 56 | b"\x12\x34\x56", 57 | True, 58 | False, 59 | ), 60 | ( 61 | (b"\x12\x34\x56\x78", b"78"), 62 | (b"\x12\x34\x56\xff", b"ff"), 63 | b"\x12\x34\x56", 64 | True, 65 | False, 66 | ), 67 | ( 68 | (b"\x12\x34\x56\x78", b"78"), 69 | (b"\x12\x34\x56\x79", b"79"), 70 | b"\x12\x34\x57", 71 | False, 72 | False, 73 | ), 74 | ( 75 | (b"\x12\x34\x56\x78", b"78"), 76 | (b"\x12\x34\x56\x79", b"79"), 77 | b"\x12\x34\x56\x78\x9a", 78 | False, 79 | True, 80 | ), 81 | ), 82 | ) 83 | def test_bin_trie_delete_subtrie( 84 | kv1, kv2, key_to_be_deleted, will_delete, will_rasie_error 85 | ): 86 | trie = BinaryTrie(db={}) 87 | # First test case, delete subtrie of a kv node 88 | trie.set(kv1[0], kv1[1]) 89 | trie.set(kv2[0], kv2[1]) 90 | assert trie.get(kv1[0]) == kv1[1] 91 | assert trie.get(kv2[0]) == kv2[1] 92 | 93 | if will_delete: 94 | trie.delete_subtrie(key_to_be_deleted) 95 | assert trie.get(kv1[0]) is None 96 | assert trie.get(kv2[0]) is None 97 | assert trie.root_hash == BLANK_HASH 98 | else: 99 | if will_rasie_error: 100 | with pytest.raises(NodeOverrideError): 101 | trie.delete_subtrie(key_to_be_deleted) 102 | else: 103 | root_hash_before_delete = trie.root_hash 104 | trie.delete_subtrie(key_to_be_deleted) 105 | assert trie.get(kv1[0]) == kv1[1] 106 | assert trie.get(kv2[0]) == kv2[1] 107 | assert trie.root_hash == root_hash_before_delete 108 | 109 | 110 | @pytest.mark.parametrize( 111 | "invalide_key,if_error", 112 | ( 113 | (b"\x12\x34\x56", False), 114 | (b"\x12\x34\x56\x77", False), 115 | (b"\x12\x34\x56\x78\x9a", True), 116 | (b"\x12\x34\x56\x79\xab", True), 117 | (b"\xab\xcd\xef", False), 118 | ), 119 | ) 120 | def test_bin_trie_invalid_key(invalide_key, if_error): 121 | trie = BinaryTrie(db={}) 122 | trie.set(b"\x12\x34\x56\x78", b"78") 123 | trie.set(b"\x12\x34\x56\x79", b"79") 124 | 125 | assert trie.get(invalide_key) is None 126 | if if_error: 127 | with pytest.raises(NodeOverrideError): 128 | trie.delete(invalide_key) 129 | else: 130 | previous_root_hash = trie.root_hash 131 | trie.delete(invalide_key) 132 | assert previous_root_hash == trie.root_hash 133 | 134 | 135 | @given( 136 | keys=st.lists( 137 | st.binary(min_size=32, max_size=32), min_size=100, max_size=100, unique=True 138 | ), 139 | chosen_numbers=st.lists( 140 | st.integers(min_value=0, max_value=99), min_size=50, max_size=50, unique=True 141 | ), 142 | ) 143 | @settings(max_examples=10) 144 | def test_bin_trie_update_value(keys, chosen_numbers): 145 | """ 146 | This is a basic test to see if updating value works as expected. 147 | """ 148 | trie = BinaryTrie(db={}) 149 | for key in keys: 150 | trie.set(key, b"old") 151 | 152 | current_root = trie.root_hash 153 | for i in chosen_numbers: 154 | trie.set(keys[i], b"old") 155 | assert current_root == trie.root_hash 156 | trie.set(keys[i], b"new") 157 | assert current_root != trie.root_hash 158 | assert trie.get(keys[i]) == b"new" 159 | current_root = trie.root_hash 160 | -------------------------------------------------------------------------------- /tests/core/test_binaries_utils.py: -------------------------------------------------------------------------------- 1 | from hypothesis import ( 2 | given, 3 | strategies as st, 4 | ) 5 | 6 | from trie.utils.binaries import ( 7 | decode_from_bin, 8 | decode_to_bin_keypath, 9 | encode_from_bin_keypath, 10 | encode_to_bin, 11 | ) 12 | 13 | 14 | @given(value=st.binary(min_size=0, max_size=1024)) 15 | def test_round_trip_bin_encoding(value): 16 | value_as_binaries = encode_to_bin(value) 17 | result = decode_from_bin(value_as_binaries) 18 | assert result == value 19 | 20 | 21 | @given(value=st.lists(elements=st.integers(0, 1), min_size=0, max_size=1024)) 22 | def test_round_trip_bin_keypath_encoding(value): 23 | value_as_bin_keypath = encode_from_bin_keypath(bytes(value)) 24 | result = decode_to_bin_keypath(value_as_bin_keypath) 25 | assert result == bytes(value) 26 | -------------------------------------------------------------------------------- /tests/core/test_branches_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from trie.binary import ( 4 | BinaryTrie, 5 | ) 6 | from trie.branches import ( 7 | check_if_branch_exist, 8 | get_branch, 9 | get_trie_nodes, 10 | get_witness_for_key_prefix, 11 | if_branch_valid, 12 | ) 13 | from trie.exceptions import ( 14 | InvalidKeyError, 15 | ) 16 | 17 | 18 | @pytest.fixture 19 | def test_trie(): 20 | trie = BinaryTrie(db={}) 21 | trie.set(b"\x12\x34\x56\x78\x9a", b"9a") 22 | trie.set(b"\x12\x34\x56\x78\x9b", b"9b") 23 | trie.set(b"\x12\x34\x56\xff", b"ff") 24 | 25 | return trie 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "key_prefix,if_exist", 30 | ( 31 | (b"\x12\x34", True), 32 | (b"\x12\x34\x56\x78\x9b", True), 33 | (b"\x12\x56", False), 34 | (b"\x12\x34\x56\xff\xff", False), 35 | (b"\x12\x34\x56", True), 36 | (b"\x12\x34\x56\x78", True), 37 | ), 38 | ) 39 | def test_branch_exist(test_trie, key_prefix, if_exist): 40 | assert ( 41 | check_if_branch_exist(test_trie.db, test_trie.root_hash, key_prefix) == if_exist 42 | ) 43 | 44 | 45 | @pytest.mark.parametrize( 46 | "key,key_valid", 47 | ( 48 | (b"\x12\x34", True), 49 | (b"\x12\x34\x56\xff", True), 50 | (b"\x12\x34\x56\x78\x9b", True), 51 | (b"\x12\x56", True), 52 | (b"\x12\x34\x56\xff\xff", False), 53 | (b"", False), 54 | ), 55 | ) 56 | def test_branch(test_trie, key, key_valid): 57 | if key_valid: 58 | branch = get_branch(test_trie.db, test_trie.root_hash, key) 59 | assert if_branch_valid(branch, test_trie.root_hash, key, test_trie.get(key)) 60 | else: 61 | with pytest.raises(InvalidKeyError): 62 | get_branch(test_trie.db, test_trie.root_hash, key) 63 | 64 | 65 | @pytest.mark.parametrize( 66 | "root,nodes", 67 | ( 68 | ( 69 | b"#\xf037,w\xb9()\x0e4\x92\xdf\x11\xca\xea\xa5\x13/\x10\x1bJ\xa7\x16\x07\x07G\xb1\x01_\x16\xca", # noqa: E501 70 | [b"\x029a"], 71 | ), 72 | ( 73 | b"\x84\x97\xc1\xf7S\xf5\xa2\xbb>\xbd\xe9\xc3t\x0f\xac/\xad\xa8\x01\xff\x9aE\t\xc1\xab\x9e\xa3|\xc7Z\xb0v", # noqa: E501 74 | [ 75 | b"\x01#\xf037,w\xb9()\x0e4\x92\xdf\x11\xca\xea\xa5\x13/\x10\x1bJ\xa7\x16\x07\x07G\xb1\x01_\x16\xcaG\xe9\xb6\xa1\xfa\xd5\x82\xf4k\x04\x9c\x8e\xc8\x17\xb4G\xe1c*n\xf4o\x02\x85\xf1\x19\xa8\x83`\xfb\xf8\xa2", # noqa: E501 76 | b"\x029a", 77 | b"\x029b", 78 | ], 79 | ), 80 | ( 81 | b'\x13\x07<\xa0w6\xd5O\x91\x93\xb1\xde,0}\xe7\xee\x82\xd7\xf6\xce\x1b^\xb7}"\n\xe4&\xe2\xd7v', # noqa: E501 82 | [ 83 | b"\x00\x82\xbd\xe9\xc3t\x0f\xac/\xad\xa8\x01\xff\x9aE\t\xc1\xab\x9e\xa3|\xc7Z\xb0v", # noqa: E501 84 | b"\x01#\xf037,w\xb9()\x0e4\x92\xdf\x11\xca\xea\xa5\x13/\x10\x1bJ\xa7\x16\x07\x07G\xb1\x01_\x16\xcaG\xe9\xb6\xa1\xfa\xd5\x82\xf4k\x04\x9c\x8e\xc8\x17\xb4G\xe1c*n\xf4o\x02\x85\xf1\x19\xa8\x83`\xfb\xf8\xa2", # noqa: E501 85 | b"\x029a", 86 | b"\x029b", 87 | ], 88 | ), 89 | ( 90 | b"X\x99\x8f\x13\xeb\x9bF\x08\xec|\x8b\xd8}\xca\xed\xda\xbb4\tl\xc8\x9bJ;J\xed\x11\x86\xc2\xd7+\xca", # noqa: E501 91 | [ 92 | b"\x00\x80\x124V\xde\xb5\x8f\xdb\x98\xc0\xe8\xed\x10\xde\x84\x89\xe1\xc3\x90\xbeoi7y$sJ\x07\xa1h\xf5t\x1c\xac\r+", # noqa: E501 93 | b'\x01\x13\x07<\xa0w6\xd5O\x91\x93\xb1\xde,0}\xe7\xee\x82\xd7\xf6\xce\x1b^\xb7}"\n\xe4&\xe2\xd7v7\x94\x07\x18\xc9\x96E\xf1\x9bS1sv\xa2\x8b\x9a\x88\xfd/>5\xcb3\x9e\x03\x08\r\xe2\xe1\xd5\xaaq', # noqa: E501 94 | b"\x00\x82\xbd\xe9\xc3t\x0f\xac/\xad\xa8\x01\xff\x9aE\t\xc1\xab\x9e\xa3|\xc7Z\xb0v", # noqa: E501 95 | b"\x00\x83\x7fR\xce\xe1\xe1 +\x96\xde\xae\xcdV\x13\x9a \x90.7H\xb6\x80\t\x10\xe1(\x03\x15\xde\x94\x17X\xee\xe1", # noqa: E501 96 | b"\x01#\xf037,w\xb9()\x0e4\x92\xdf\x11\xca\xea\xa5\x13/\x10\x1bJ\xa7\x16\x07\x07G\xb1\x01_\x16\xcaG\xe9\xb6\xa1\xfa\xd5\x82\xf4k\x04\x9c\x8e\xc8\x17\xb4G\xe1c*n\xf4o\x02\x85\xf1\x19\xa8\x83`\xfb\xf8\xa2", # noqa: E501 97 | b"\x02ff", 98 | b"\x029a", 99 | b"\x029b", 100 | ], 101 | ), 102 | ( 103 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 104 | [], 105 | ), 106 | (32 * b"\x00", []), 107 | ), 108 | ) 109 | def test_get_trie_nodes(test_trie, root, nodes): 110 | assert set(nodes) == set(get_trie_nodes(test_trie.db, root)) 111 | 112 | 113 | @pytest.mark.parametrize( 114 | "key,nodes", 115 | ( 116 | ( 117 | b"\x12\x34\x56\x78\x9b", 118 | [ 119 | b"\x00\x80\x124V\xde\xb5\x8f\xdb\x98\xc0\xe8\xed\x10\xde\x84\x89\xe1\xc3\x90\xbeoi7y$sJ\x07\xa1h\xf5t\x1c\xac\r+", # noqa: E501 120 | b'\x01\x13\x07<\xa0w6\xd5O\x91\x93\xb1\xde,0}\xe7\xee\x82\xd7\xf6\xce\x1b^\xb7}"\n\xe4&\xe2\xd7v7\x94\x07\x18\xc9\x96E\xf1\x9bS1sv\xa2\x8b\x9a\x88\xfd/>5\xcb3\x9e\x03\x08\r\xe2\xe1\xd5\xaaq', # noqa: E501 121 | b"\x00\x82\xbd\xe9\xc3t\x0f\xac/\xad\xa8\x01\xff\x9aE\t\xc1\xab\x9e\xa3|\xc7Z\xb0v", # noqa: E501 122 | b"\x01#\xf037,w\xb9()\x0e4\x92\xdf\x11\xca\xea\xa5\x13/\x10\x1bJ\xa7\x16\x07\x07G\xb1\x01_\x16\xcaG\xe9\xb6\xa1\xfa\xd5\x82\xf4k\x04\x9c\x8e\xc8\x17\xb4G\xe1c*n\xf4o\x02\x85\xf1\x19\xa8\x83`\xfb\xf8\xa2", # noqa: E501 123 | b"\x029b", 124 | ], 125 | ), 126 | ( 127 | b"\x12\x34\x56\x78", 128 | [ 129 | b"\x00\x80\x124V\xde\xb5\x8f\xdb\x98\xc0\xe8\xed\x10\xde\x84\x89\xe1\xc3\x90\xbeoi7y$sJ\x07\xa1h\xf5t\x1c\xac\r+", # noqa: E501 130 | b'\x01\x13\x07<\xa0w6\xd5O\x91\x93\xb1\xde,0}\xe7\xee\x82\xd7\xf6\xce\x1b^\xb7}"\n\xe4&\xe2\xd7v7\x94\x07\x18\xc9\x96E\xf1\x9bS1sv\xa2\x8b\x9a\x88\xfd/>5\xcb3\x9e\x03\x08\r\xe2\xe1\xd5\xaaq', # noqa: E501 131 | b"\x00\x82\xbd\xe9\xc3t\x0f\xac/\xad\xa8\x01\xff\x9aE\t\xc1\xab\x9e\xa3|\xc7Z\xb0v", # noqa: E501 132 | b"\x01#\xf037,w\xb9()\x0e4\x92\xdf\x11\xca\xea\xa5\x13/\x10\x1bJ\xa7\x16\x07\x07G\xb1\x01_\x16\xcaG\xe9\xb6\xa1\xfa\xd5\x82\xf4k\x04\x9c\x8e\xc8\x17\xb4G\xe1c*n\xf4o\x02\x85\xf1\x19\xa8\x83`\xfb\xf8\xa2", # noqa: E501 133 | b"\x029a", 134 | b"\x029b", 135 | ], 136 | ), 137 | ( 138 | b"\x12\x34\x56", 139 | [ 140 | b"\x00\x80\x124V\xde\xb5\x8f\xdb\x98\xc0\xe8\xed\x10\xde\x84\x89\xe1\xc3\x90\xbeoi7y$sJ\x07\xa1h\xf5t\x1c\xac\r+", # noqa: E501 141 | b'\x01\x13\x07<\xa0w6\xd5O\x91\x93\xb1\xde,0}\xe7\xee\x82\xd7\xf6\xce\x1b^\xb7}"\n\xe4&\xe2\xd7v7\x94\x07\x18\xc9\x96E\xf1\x9bS1sv\xa2\x8b\x9a\x88\xfd/>5\xcb3\x9e\x03\x08\r\xe2\xe1\xd5\xaaq', # noqa: E501 142 | b"\x00\x82\xbd\xe9\xc3t\x0f\xac/\xad\xa8\x01\xff\x9aE\t\xc1\xab\x9e\xa3|\xc7Z\xb0v", # noqa: E501 143 | b"\x00\x83\x7fR\xce\xe1\xe1 +\x96\xde\xae\xcdV\x13\x9a \x90.7H\xb6\x80\t\x10\xe1(\x03\x15\xde\x94\x17X\xee\xe1", # noqa: E501 144 | b"\x01#\xf037,w\xb9()\x0e4\x92\xdf\x11\xca\xea\xa5\x13/\x10\x1bJ\xa7\x16\x07\x07G\xb1\x01_\x16\xcaG\xe9\xb6\xa1\xfa\xd5\x82\xf4k\x04\x9c\x8e\xc8\x17\xb4G\xe1c*n\xf4o\x02\x85\xf1\x19\xa8\x83`\xfb\xf8\xa2", # noqa: E501 145 | b"\x02ff", 146 | b"\x029a", 147 | b"\x029b", 148 | ], 149 | ), 150 | ( 151 | b"\x12", 152 | [ 153 | b"\x00\x80\x124V\xde\xb5\x8f\xdb\x98\xc0\xe8\xed\x10\xde\x84\x89\xe1\xc3\x90\xbeoi7y$sJ\x07\xa1h\xf5t\x1c\xac\r+", # noqa: E501 154 | b'\x01\x13\x07<\xa0w6\xd5O\x91\x93\xb1\xde,0}\xe7\xee\x82\xd7\xf6\xce\x1b^\xb7}"\n\xe4&\xe2\xd7v7\x94\x07\x18\xc9\x96E\xf1\x9bS1sv\xa2\x8b\x9a\x88\xfd/>5\xcb3\x9e\x03\x08\r\xe2\xe1\xd5\xaaq', # noqa: E501 155 | b"\x00\x82\xbd\xe9\xc3t\x0f\xac/\xad\xa8\x01\xff\x9aE\t\xc1\xab\x9e\xa3|\xc7Z\xb0v", # noqa: E501 156 | b"\x00\x83\x7fR\xce\xe1\xe1 +\x96\xde\xae\xcdV\x13\x9a \x90.7H\xb6\x80\t\x10\xe1(\x03\x15\xde\x94\x17X\xee\xe1", # noqa: E501 157 | b"\x01#\xf037,w\xb9()\x0e4\x92\xdf\x11\xca\xea\xa5\x13/\x10\x1bJ\xa7\x16\x07\x07G\xb1\x01_\x16\xcaG\xe9\xb6\xa1\xfa\xd5\x82\xf4k\x04\x9c\x8e\xc8\x17\xb4G\xe1c*n\xf4o\x02\x85\xf1\x19\xa8\x83`\xfb\xf8\xa2", # noqa: E501 158 | b"\x02ff", 159 | b"\x029a", 160 | b"\x029b", 161 | ], 162 | ), 163 | ( 164 | 32 * b"\x00", 165 | [ 166 | b"\x00\x80\x124V\xde\xb5\x8f\xdb\x98\xc0\xe8\xed\x10\xde\x84\x89\xe1\xc3\x90\xbeoi7y$sJ\x07\xa1h\xf5t\x1c\xac\r+", # noqa: E501 167 | ], 168 | ), 169 | ), 170 | ) 171 | def test_get_witness_for_key_prefix(test_trie, key, nodes): 172 | if nodes: 173 | assert set(nodes) == set( 174 | get_witness_for_key_prefix(test_trie.db, test_trie.root_hash, key) 175 | ) 176 | else: 177 | with pytest.raises(InvalidKeyError): 178 | get_witness_for_key_prefix(test_trie.db, test_trie.root_hash, key) 179 | -------------------------------------------------------------------------------- /tests/core/test_constants.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from eth_utils import ( 4 | keccak, 5 | ) 6 | from rlp.codec import ( 7 | encode_raw, 8 | ) 9 | 10 | from trie.constants import ( 11 | BLANK_HASH, 12 | BLANK_NODE, 13 | BLANK_NODE_HASH, 14 | ) 15 | from trie.smt import ( 16 | SparseMerkleTree as SMT, 17 | ) 18 | 19 | 20 | def test_hash_constants(): 21 | assert BLANK_HASH == keccak(BLANK_NODE) 22 | assert BLANK_NODE_HASH == keccak(encode_raw(b"")) 23 | 24 | 25 | def test_smt256_empty_hashes(): 26 | DEPTH = 256 # Default depth is 32 bytes 27 | 28 | # Start at the bottom 29 | EMPTY_LEAF_NODE_HASH = BLANK_HASH 30 | EMPTY_NODE_HASHES = collections.deque([EMPTY_LEAF_NODE_HASH]) 31 | 32 | # More hashes the lower you go down the tree (to the root) 33 | # NOTE: Did this with different code as a sanity check 34 | for _ in range(DEPTH - 1): 35 | EMPTY_NODE_HASHES.appendleft( 36 | keccak(EMPTY_NODE_HASHES[0] + EMPTY_NODE_HASHES[0]) 37 | ) 38 | EMPTY_ROOT_HASH = keccak(EMPTY_NODE_HASHES[0] + EMPTY_NODE_HASHES[0]) 39 | 40 | smt = SMT() 41 | assert smt.root_hash == EMPTY_ROOT_HASH 42 | 43 | key = b"\x00" * 32 44 | # _get(key) returns value, branch tuple 45 | assert smt._get(key)[1] == tuple(EMPTY_NODE_HASHES) 46 | -------------------------------------------------------------------------------- /tests/core/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from trie.exceptions import ( 4 | MissingTraversalNode, 5 | MissingTrieNode, 6 | TraversedPartialPath, 7 | ValidationError, 8 | ) 9 | from trie.typing import ( 10 | Nibbles, 11 | ) 12 | from trie.utils.nodes import ( 13 | annotate_node, 14 | compute_extension_key, 15 | compute_leaf_key, 16 | ) 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "valid_prefix", 21 | ( 22 | None, 23 | (), 24 | (0, 0, 0), 25 | (0xF,) * 128, # no length limit on the prefix 26 | ), 27 | ) 28 | def test_valid_MissingTrieNode_prefix(valid_prefix): 29 | exception = MissingTrieNode(b"", b"", b"", valid_prefix) 30 | assert exception.prefix == valid_prefix 31 | if valid_prefix is not None: 32 | assert str(Nibbles(valid_prefix)) in repr(exception) 33 | 34 | 35 | @pytest.mark.parametrize( 36 | "invalid_prefix, exception", 37 | ( 38 | ((b"F",), ValueError), 39 | (b"F", TypeError), 40 | ((b"\x00",), ValueError), 41 | ((b"\x0F",), ValueError), 42 | (0, TypeError), 43 | (0xF, TypeError), 44 | ((0, 0x10), ValueError), 45 | ((0, -1), ValueError), 46 | ), 47 | ) 48 | def test_invalid_MissingTrieNode_prefix(invalid_prefix, exception): 49 | with pytest.raises(exception): 50 | MissingTrieNode(b"", b"", b"", invalid_prefix) 51 | 52 | 53 | @pytest.mark.parametrize( 54 | "valid_nibbles", 55 | ( 56 | (), 57 | (0, 0, 0), 58 | (0xF,) * 128, # no length limit on the nibbles 59 | ), 60 | ) 61 | def test_valid_MissingTraversalNode_nibbles(valid_nibbles): 62 | exception = MissingTraversalNode(b"", valid_nibbles) 63 | assert exception.nibbles_traversed == valid_nibbles 64 | assert str(Nibbles(valid_nibbles)) in repr(exception) 65 | 66 | 67 | @pytest.mark.parametrize( 68 | "invalid_nibbles, exception", 69 | ( 70 | (None, TypeError), 71 | ((b"F",), ValueError), 72 | (b"F", TypeError), 73 | ((b"\x00",), ValueError), 74 | ((b"\x0F",), ValueError), 75 | (0, TypeError), 76 | (0xF, TypeError), 77 | ((0, 0x10), ValueError), 78 | ((0, -1), ValueError), 79 | ), 80 | ) 81 | def test_invalid_MissingTraversalNode_nibbles(invalid_nibbles, exception): 82 | with pytest.raises(exception): 83 | MissingTraversalNode(b"", invalid_nibbles) 84 | 85 | 86 | @pytest.mark.parametrize( 87 | "valid_nibbles", 88 | ( 89 | (), 90 | (0, 0, 0), 91 | (0xF,) * 128, # no length limit on the nibbles 92 | ), 93 | ) 94 | @pytest.mark.parametrize("key_encoding", (compute_extension_key, compute_leaf_key)) 95 | def test_valid_TraversedPartialPath_traversed_nibbles(valid_nibbles, key_encoding): 96 | some_node_key = (1, 2) 97 | node = annotate_node([key_encoding(some_node_key), b"random-value"]) 98 | exception = TraversedPartialPath(valid_nibbles, node, some_node_key[:1]) 99 | assert exception.nibbles_traversed == valid_nibbles 100 | assert str(Nibbles(valid_nibbles)) in repr(exception) 101 | 102 | 103 | @pytest.mark.parametrize( 104 | "invalid_nibbles, exception", 105 | ( 106 | (None, TypeError), 107 | ((b"F",), ValueError), 108 | (b"F", TypeError), 109 | ((b"\x00",), ValueError), 110 | ((b"\x0F",), ValueError), 111 | (0, TypeError), 112 | (0xF, TypeError), 113 | ((0, 0x10), ValueError), 114 | ((0, -1), ValueError), 115 | ), 116 | ) 117 | def test_invalid_TraversedPartialPath_traversed_nibbles(invalid_nibbles, exception): 118 | with pytest.raises(exception): 119 | TraversedPartialPath(invalid_nibbles, annotate_node(b""), (1,)) 120 | 121 | 122 | @pytest.mark.parametrize( 123 | "valid_nibbles", 124 | ( 125 | (0, 0, 0), 126 | (0xF,) * 128, # no length limit on the nibbles 127 | ), 128 | ) 129 | @pytest.mark.parametrize("key_encoding", (compute_extension_key, compute_leaf_key)) 130 | def test_valid_TraversedPartialPath_untraversed_nibbles(valid_nibbles, key_encoding): 131 | # This exception means that the actual node key should have more than the 132 | # untraversed amount. So we simulate some longer key for the given node 133 | longer_key = valid_nibbles + (0,) 134 | node = annotate_node([key_encoding(longer_key), b"random-value"]) 135 | exception = TraversedPartialPath((), node, valid_nibbles) 136 | assert exception.untraversed_tail == valid_nibbles 137 | assert str(Nibbles(valid_nibbles)) in repr(exception) 138 | 139 | 140 | @pytest.mark.parametrize("key_encoding", (compute_extension_key, compute_leaf_key)) 141 | def test_TraversedPartialPath_keeps_node_value(key_encoding): 142 | node_key = (0, 0xF, 9) 143 | untraversed_tail = node_key[:1] 144 | remaining_key = node_key[1:] 145 | node_value = b"unicorns" 146 | node = annotate_node([key_encoding(node_key), node_value]) 147 | tpp = TraversedPartialPath(node_key, node, untraversed_tail) 148 | simulated_node = tpp.simulated_node 149 | assert simulated_node.raw[1] == node_value 150 | if key_encoding is compute_leaf_key: 151 | assert simulated_node.sub_segments == () 152 | assert simulated_node.suffix == remaining_key 153 | assert simulated_node.raw[0] == compute_leaf_key(remaining_key) 154 | assert simulated_node.value == node_value 155 | elif key_encoding is compute_extension_key: 156 | assert simulated_node.sub_segments == (remaining_key,) 157 | assert simulated_node.suffix == () 158 | assert simulated_node.raw[0] == compute_extension_key(remaining_key) 159 | else: 160 | raise Exception("Unsupported way to encode keys: {key_encoding}") 161 | 162 | 163 | @pytest.mark.parametrize( 164 | "invalid_nibbles, node_key, exception", 165 | ( 166 | ((), (), ValueError), 167 | (None, (), TypeError), 168 | ((b"F",), (), ValueError), 169 | (b"F", (), TypeError), 170 | ((b"\x00",), (), ValueError), 171 | ((b"\x0F",), (), ValueError), 172 | (0, (), TypeError), 173 | (0xF, (), TypeError), 174 | ((0, 0x10), (), ValueError), 175 | ((0, -1), (), ValueError), 176 | # There must be some kind of tail 177 | ((), (1,), ValueError), 178 | # The untraversed tail must be a prefix of the node key 179 | ((0,), (1,), ValidationError), 180 | # The untraversed tail must not be the full length of the node key 181 | ((1,), (1,), ValidationError), 182 | ), 183 | ) 184 | @pytest.mark.parametrize("key_encoding", (compute_extension_key, compute_leaf_key)) 185 | def test_invalid_TraversedPartialPath_untraversed_nibbles( 186 | invalid_nibbles, node_key, exception, key_encoding 187 | ): 188 | if node_key == (): 189 | node = annotate_node(b"") 190 | else: 191 | node = annotate_node([key_encoding(node_key), b"some-val"]) 192 | 193 | # Handle special case: leaf nodes are permitted to have the 194 | # untraversed tail equal the suffix 195 | if len(node.suffix) > 0 and node.suffix == invalid_nibbles: 196 | # So in this one case, make sure we don't raise an exception 197 | TraversedPartialPath((), node, invalid_nibbles) 198 | else: 199 | with pytest.raises(exception): 200 | TraversedPartialPath((), node, invalid_nibbles) 201 | -------------------------------------------------------------------------------- /tests/core/test_fog.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from eth_utils import ( 4 | ValidationError, 5 | ) 6 | from hypothesis import ( 7 | given, 8 | strategies as st, 9 | ) 10 | 11 | from trie.exceptions import ( 12 | FullDirectionalVisibility, 13 | PerfectVisibility, 14 | ) 15 | from trie.fog import ( 16 | HexaryTrieFog, 17 | ) 18 | 19 | 20 | def test_trie_fog_completion(): 21 | fog = HexaryTrieFog() 22 | 23 | # fog should start with *nothing* verified 24 | assert not fog.is_complete 25 | 26 | # completing the empty prefix should immediately mark it as complete 27 | empty_prefix = () 28 | completed_fog = fog.explore(empty_prefix, ()) 29 | assert completed_fog.is_complete 30 | 31 | # original fog should be untouched 32 | assert not fog.is_complete 33 | 34 | 35 | def test_trie_fog_expand_before_complete(): 36 | fog = HexaryTrieFog() 37 | 38 | empty_prefix = () 39 | branched = fog.explore(empty_prefix, ((1,), (5,))) 40 | assert not branched.is_complete 41 | 42 | # complete only one prefix 43 | single_prefix = branched.explore((1,), ()) 44 | assert not single_prefix.is_complete 45 | 46 | completed = single_prefix.explore((5,), ()) 47 | assert completed.is_complete 48 | 49 | 50 | def test_trie_fog_expand_before_mark_all_complete(): 51 | fog = HexaryTrieFog() 52 | 53 | empty_prefix = () 54 | branched = fog.explore(empty_prefix, ((1,), (5,))) 55 | assert not branched.is_complete 56 | 57 | # complete all sub-segments at once 58 | completed = branched.mark_all_complete(((1,), (5,))) 59 | assert completed.is_complete 60 | 61 | 62 | def test_trie_fog_composition_equality(): 63 | fog = HexaryTrieFog() 64 | 65 | empty_prefix = () 66 | single_exploration = fog.explore(empty_prefix, ((9, 9, 9),)) 67 | 68 | half_explore = fog.explore(empty_prefix, ((9,),)) 69 | full_explore = half_explore.explore((9,), ((9, 9),)) 70 | 71 | assert single_exploration == full_explore 72 | 73 | 74 | def test_trie_fog_immutability(): 75 | fog = HexaryTrieFog() 76 | 77 | fog1 = fog.explore((), ((1,), (2,))) 78 | 79 | fog2 = fog1.explore((1,), ((3,),)) 80 | 81 | assert fog.nearest_unknown(()) == () 82 | assert fog1.nearest_unknown(()) == (1,) 83 | assert fog2.nearest_unknown(()) == (1, 3) 84 | 85 | assert fog != fog1 86 | assert fog1 != fog2 87 | assert fog != fog2 88 | 89 | 90 | @pytest.mark.parametrize( 91 | "sub_segments", 92 | ( 93 | [(1, 2), (1, 2, 3, 4)], 94 | [(1, 2), (1, 2)], 95 | ), 96 | ) 97 | def test_trie_fog_explore_invalid(sub_segments): 98 | """ 99 | Cannot explore with a sub_segment that is a child of another sub_segment, 100 | or a duplicate 101 | """ 102 | fog = HexaryTrieFog() 103 | with pytest.raises(ValidationError): 104 | fog.explore((), sub_segments) 105 | 106 | 107 | def test_trie_fog_nearest_unknown(): 108 | fog = HexaryTrieFog() 109 | 110 | empty_prefix = () 111 | assert fog.nearest_unknown((1, 2, 3)) == empty_prefix 112 | 113 | branched = fog.explore(empty_prefix, ((1, 1), (5, 5))) 114 | 115 | # Test shallower 116 | assert branched.nearest_unknown((0,)) == (1, 1) 117 | assert branched.nearest_unknown((1,)) == (1, 1) 118 | assert branched.nearest_unknown((2,)) == (1, 1) 119 | assert branched.nearest_unknown((4,)) == (5, 5) 120 | assert branched.nearest_unknown((5,)) == (5, 5) 121 | assert branched.nearest_unknown((6,)) == (5, 5) 122 | 123 | # Test same level 124 | assert branched.nearest_unknown((0, 9)) == (1, 1) 125 | assert branched.nearest_unknown((1, 1)) == (1, 1) 126 | assert branched.nearest_unknown((2, 1)) == (1, 1) 127 | assert branched.nearest_unknown((3, 2)) == (1, 1) 128 | assert branched.nearest_unknown((3, 3)) == (5, 5) 129 | assert branched.nearest_unknown((4, 9)) == (5, 5) 130 | assert branched.nearest_unknown((5, 5)) == (5, 5) 131 | assert branched.nearest_unknown((6, 1)) == (5, 5) 132 | 133 | # Test deeper 134 | assert branched.nearest_unknown((0, 9, 9)) == (1, 1) 135 | assert branched.nearest_unknown((1, 1, 0)) == (1, 1) 136 | assert branched.nearest_unknown((2, 1, 1)) == (1, 1) 137 | assert branched.nearest_unknown((4, 9, 9)) == (5, 5) 138 | assert branched.nearest_unknown((5, 5, 0)) == (5, 5) 139 | assert branched.nearest_unknown((6, 1, 1)) == (5, 5) 140 | 141 | 142 | def test_trie_fog_nearest_unknown_fully_explored(): 143 | fog = HexaryTrieFog() 144 | empty_prefix = () 145 | fully_explored = fog.explore(empty_prefix, ()) 146 | 147 | with pytest.raises(PerfectVisibility): 148 | fully_explored.nearest_unknown(()) 149 | 150 | with pytest.raises(PerfectVisibility): 151 | fully_explored.nearest_unknown((0,)) 152 | 153 | 154 | def test_trie_fog_nearest_right(): 155 | fog = HexaryTrieFog() 156 | 157 | empty_prefix = () 158 | assert fog.nearest_right((1, 2, 3)) == empty_prefix 159 | 160 | branched = fog.explore(empty_prefix, ((1, 1), (5, 5))) 161 | 162 | # Test shallower 163 | assert branched.nearest_right((0,)) == (1, 1) 164 | assert branched.nearest_right((1,)) == (1, 1) 165 | assert branched.nearest_right((2,)) == (5, 5) 166 | assert branched.nearest_right((4,)) == (5, 5) 167 | assert branched.nearest_right((5,)) == (5, 5) 168 | with pytest.raises(FullDirectionalVisibility): 169 | assert branched.nearest_right((6,)) 170 | 171 | # Test same level 172 | assert branched.nearest_right((0, 9)) == (1, 1) 173 | assert branched.nearest_right((1, 1)) == (1, 1) 174 | assert branched.nearest_right((2, 1)) == (5, 5) 175 | assert branched.nearest_right((3, 2)) == (5, 5) 176 | assert branched.nearest_right((3, 3)) == (5, 5) 177 | assert branched.nearest_right((4, 9)) == (5, 5) 178 | assert branched.nearest_right((5, 5)) == (5, 5) 179 | with pytest.raises(FullDirectionalVisibility): 180 | assert branched.nearest_right((5, 6)) 181 | with pytest.raises(FullDirectionalVisibility): 182 | assert branched.nearest_right((6, 1)) 183 | 184 | # Test deeper 185 | assert branched.nearest_right((0, 9, 9)) == (1, 1) 186 | assert branched.nearest_right((1, 1, 0)) == (1, 1) 187 | assert branched.nearest_right((2, 1, 1)) == (5, 5) 188 | assert branched.nearest_right((4, 9, 9)) == (5, 5) 189 | assert branched.nearest_right((5, 5, 0)) == (5, 5) 190 | assert branched.nearest_right((5, 5, 15)) == (5, 5) 191 | with pytest.raises(FullDirectionalVisibility): 192 | assert branched.nearest_right((6, 0, 0)) 193 | 194 | 195 | def test_trie_fog_nearest_right_empty(): 196 | fog = HexaryTrieFog() 197 | empty_prefix = () 198 | fully_explored = fog.explore(empty_prefix, ()) 199 | 200 | with pytest.raises(PerfectVisibility): 201 | fully_explored.nearest_right(()) 202 | 203 | with pytest.raises(PerfectVisibility): 204 | fully_explored.nearest_right((0,)) 205 | 206 | 207 | @given( 208 | st.lists( 209 | st.tuples( 210 | # next index to use to search for a prefix to expand 211 | st.lists( 212 | st.integers(min_value=0, max_value=0xF), 213 | max_size=4 214 | * 2, # one byte (two nibbles) deeper than the longest key above 215 | ), 216 | # sub_segments to use to lift the fog 217 | st.one_of( 218 | # branch node (or leaf node if size == 0) 219 | st.lists( 220 | st.tuples( 221 | st.integers(min_value=0, max_value=0xF), 222 | ), 223 | max_size=16, 224 | unique=True, 225 | ), 226 | # or extension node 227 | st.tuples( 228 | st.lists( 229 | st.integers(min_value=0, max_value=0xF), 230 | min_size=2, 231 | ), 232 | ), 233 | ), 234 | ), 235 | ), 236 | ) 237 | def test_trie_fog_serialize(expand_points): 238 | """ 239 | Build a bunch of random trie fogs, serialize them to a bytes representation, 240 | then deserialize them back. 241 | 242 | Validate that all deserialized tries are equal to their starting tries and 243 | respond to nearest_unknown the same as the original. 244 | """ 245 | starting_fog = HexaryTrieFog() 246 | for next_index, children in expand_points: 247 | try: 248 | next_unknown = starting_fog.nearest_unknown(next_index) 249 | except PerfectVisibility: 250 | # Have already completely explored the trie 251 | break 252 | 253 | starting_fog = starting_fog.explore(next_unknown, children) 254 | 255 | if expand_points: 256 | assert starting_fog != HexaryTrieFog() 257 | else: 258 | assert starting_fog == HexaryTrieFog() 259 | 260 | resumed_fog = HexaryTrieFog.deserialize(starting_fog.serialize()) 261 | assert resumed_fog == starting_fog 262 | 263 | if starting_fog.is_complete: 264 | assert resumed_fog.is_complete 265 | else: 266 | for search_index, _ in expand_points: 267 | nearest_unknown_original = starting_fog.nearest_unknown(search_index) 268 | nearest_unknown_deserialized = resumed_fog.nearest_unknown(search_index) 269 | assert nearest_unknown_deserialized == nearest_unknown_original 270 | -------------------------------------------------------------------------------- /tests/core/test_import_and_version.py: -------------------------------------------------------------------------------- 1 | def test_import_and_version(): 2 | import trie 3 | 4 | assert isinstance(trie.__version__, str) 5 | -------------------------------------------------------------------------------- /tests/core/test_iter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | import os 4 | 5 | from hypothesis import ( 6 | example, 7 | given, 8 | strategies as st, 9 | ) 10 | import rlp 11 | 12 | from trie import ( 13 | HexaryTrie, 14 | ) 15 | from trie.exceptions import ( 16 | MissingTraversalNode, 17 | ) 18 | from trie.iter import ( 19 | NodeIterator, 20 | ) 21 | from trie.tools.strategies import ( 22 | random_trie_strategy, 23 | trie_from_keys, 24 | trie_keys_with_extensions, 25 | ) 26 | from trie.utils.nibbles import ( 27 | nibbles_to_bytes, 28 | ) 29 | from trie.utils.nodes import ( 30 | is_extension_node, 31 | ) 32 | 33 | TESTS_DIR = os.path.dirname(os.path.dirname(__file__)) 34 | ROOT_PROJECT_DIR = os.path.dirname(TESTS_DIR) 35 | NEXT_PREV_FIXTURE_PATH = os.path.join( 36 | ROOT_PROJECT_DIR, "fixtures", "TrieTests", "trietestnextprev.json" 37 | ) 38 | RAW_NEXT_PREV_FIXTURES = [ 39 | (os.path.basename(NEXT_PREV_FIXTURE_PATH), json.load(open(NEXT_PREV_FIXTURE_PATH))) 40 | ] 41 | NEXT_PREV_FIXTURES = [ 42 | (f"{fixture_filename}:{key}", fixtures[key]) 43 | for fixture_filename, fixtures in RAW_NEXT_PREV_FIXTURES 44 | for key in sorted(fixtures.keys()) 45 | ] 46 | 47 | 48 | @pytest.mark.parametrize( 49 | "fixture_name,fixture", 50 | NEXT_PREV_FIXTURES, 51 | ) 52 | def test_trie_next_prev_using_fixtures(fixture_name, fixture): 53 | trie = HexaryTrie(db={}) 54 | for k in fixture["in"]: 55 | k = k.encode("utf-8") 56 | trie[k] = k 57 | 58 | iterator = NodeIterator(trie) 59 | for point, _, nxt in fixture["tests"]: 60 | point = point.encode("utf-8") 61 | nxt = nxt.encode("utf-8") 62 | if nxt == b"": 63 | nxt = None 64 | assert nxt == iterator.next(point) 65 | 66 | 67 | @given(random_trie_strategy()) 68 | def test_iter_next(random_trie): 69 | trie, contents = random_trie 70 | iterator = NodeIterator(trie) 71 | 72 | key = iterator.next() 73 | 74 | if len(contents) == 0: 75 | assert key is None 76 | else: 77 | assert key is not None 78 | 79 | visited = [] 80 | while key is not None: 81 | visited.append(key) 82 | key = iterator.next(key) 83 | assert visited == sorted(contents.keys()) 84 | 85 | 86 | @given(trie_keys_with_extensions(), st.integers(min_value=1, max_value=33)) 87 | def test_iter_keys(trie_keys, min_value_length): 88 | trie, contents = trie_from_keys(trie_keys, min_value_length) 89 | node_iterator = NodeIterator(trie) 90 | visited = [] 91 | for key in node_iterator.keys(): 92 | visited.append(key) 93 | assert visited == sorted(contents.keys()) 94 | 95 | 96 | @given(trie_keys_with_extensions(), st.integers(min_value=1, max_value=33)) 97 | @example( 98 | # Test when the values are in reversed order (so that a larger value appears 99 | # earlier in a trie). Test that values sorted in key order, not value order. 100 | trie_keys=(b"\x01\x00", b"\x01\x00\x00"), 101 | min_value_length=6, 102 | ) 103 | def test_iter_values(trie_keys, min_value_length): 104 | trie, contents = trie_from_keys(trie_keys, min_value_length) 105 | node_iterator = NodeIterator(trie) 106 | visited = [] 107 | for value in node_iterator.values(): 108 | visited.append(value) 109 | values_sorted_by_key = [ 110 | val for _, val in sorted(contents.items()) # only look at value but sort by key 111 | ] 112 | assert visited == values_sorted_by_key 113 | 114 | 115 | @given(trie_keys_with_extensions(), st.integers(min_value=1, max_value=33)) 116 | def test_iter_items(trie_keys, min_value_length): 117 | trie, contents = trie_from_keys(trie_keys, min_value_length) 118 | node_iterator = NodeIterator(trie) 119 | visited = [] 120 | for item in node_iterator.items(): 121 | visited.append(item) 122 | assert visited == sorted(contents.items()) 123 | 124 | 125 | @given(trie_keys_with_extensions(), st.integers(min_value=1, max_value=33)) 126 | def test_iter_nodes(trie_keys, min_value_length): 127 | trie, contents = trie_from_keys(trie_keys, min_value_length) 128 | visited = set() 129 | for prefix, node in NodeIterator(trie).nodes(): 130 | # Save a copy of the encoded node to check against the database 131 | visited.add(rlp.encode(node.raw)) 132 | # Verify that navigating to the node directly 133 | # returns the same node as this iterator 134 | assert node == trie.traverse(prefix) 135 | # Double-check that if the node stores a value, then the implied key matches 136 | if node.value: 137 | iterated_key = nibbles_to_bytes(prefix + node.suffix) 138 | assert node.value == contents[iterated_key] 139 | 140 | # All nodes should be visited 141 | # Note that because of node embedding, the node iterator will return more nodes 142 | # than actually exist in the underlying DB (it returns embedded nodes as if they 143 | # were not embedded). So we can't simply test that trie.db.values() 144 | # equals visited here. 145 | assert set(trie.db.values()) - visited == set() 146 | 147 | 148 | def test_iter_error(): 149 | trie = HexaryTrie({}) 150 | trie[b"cat"] = b"cat" 151 | trie[b"dog"] = b"dog" 152 | trie[b"bird"] = b"bird" 153 | raw_root_node = trie.root_node.raw 154 | assert is_extension_node(raw_root_node) 155 | node_to_remove = raw_root_node[1] 156 | trie.db.pop(node_to_remove) 157 | iterator = NodeIterator(trie) 158 | key = b"" 159 | with pytest.raises(MissingTraversalNode): 160 | while key is not None: 161 | key = iterator.next(key) 162 | -------------------------------------------------------------------------------- /tests/core/test_nibbles_utils.py: -------------------------------------------------------------------------------- 1 | from hypothesis import ( 2 | given, 3 | strategies as st, 4 | ) 5 | 6 | from trie.utils.nibbles import ( 7 | bytes_to_nibbles, 8 | nibbles_to_bytes, 9 | ) 10 | 11 | 12 | @given(value=st.binary(min_size=0, max_size=1024)) 13 | def test_round_trip_nibbling(value): 14 | value_as_nibbles = bytes_to_nibbles(value) 15 | result = nibbles_to_bytes(value_as_nibbles) 16 | assert result == value 17 | -------------------------------------------------------------------------------- /tests/core/test_nodes_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from trie.exceptions import ( 4 | InvalidNode, 5 | ValidationError, 6 | ) 7 | from trie.utils.nodes import ( 8 | consume_common_prefix, 9 | encode_branch_node, 10 | encode_kv_node, 11 | encode_leaf_node, 12 | get_common_prefix_length, 13 | parse_node, 14 | ) 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "left,right,expected", 19 | ( 20 | ([], [], 0), 21 | ([], [1], 0), 22 | ([1], [1], 1), 23 | ([1], [1, 1], 1), 24 | ([1, 2], [1, 1], 1), 25 | ([1, 2, 3, 4, 5, 6], [1, 2, 3, 5, 6], 3), 26 | ), 27 | ) 28 | def test_get_common_prefix_length(left, right, expected): 29 | actual_a = get_common_prefix_length(left, right) 30 | actual_b = get_common_prefix_length(right, left) 31 | assert actual_a == actual_b == expected 32 | 33 | 34 | @pytest.mark.parametrize( 35 | "left,right,expected", 36 | ( 37 | ([], [], ([], [], [])), 38 | ([], [1], ([], [], [1])), 39 | ([1], [1], ([1], [], [])), 40 | ([1], [1, 1], ([1], [], [1])), 41 | ([1, 2], [1, 1], ([1], [2], [1])), 42 | ([1, 2, 3, 4, 5, 6], [1, 2, 3, 5, 6], ([1, 2, 3], [4, 5, 6], [5, 6])), 43 | ), 44 | ) 45 | def test_consume_common_prefix(left, right, expected): 46 | actual_a = consume_common_prefix(left, right) 47 | actual_b = consume_common_prefix(right, left) 48 | expected_b = (expected[0], expected[2], expected[1]) 49 | assert actual_a == expected 50 | assert actual_b == expected_b 51 | 52 | 53 | @pytest.mark.parametrize( 54 | "node,expected_output", 55 | ( 56 | ( 57 | b"\x00\x03\x04\x05\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 58 | ( 59 | 0, 60 | b"\x00\x00\x01\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01", # noqa: E501 61 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 62 | ), 63 | ), 64 | ( 65 | b"\x01\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 66 | ( 67 | 1, 68 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 69 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 70 | ), 71 | ), 72 | (b"\x02value", (2, None, b"value")), 73 | (b"", None), 74 | (None, None), 75 | ( 76 | b"\x00\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 77 | None, 78 | ), 79 | ( 80 | b"\x01\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 81 | None, 82 | ), 83 | ( 84 | b"\x01\x02\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 85 | None, 86 | ), 87 | (b"\x02", None), 88 | ), 89 | ) 90 | def test_binary_trie_node_parsing(node, expected_output): 91 | if expected_output: 92 | assert expected_output == parse_node(node) 93 | else: 94 | with pytest.raises(InvalidNode): 95 | parse_node(node) 96 | 97 | 98 | @pytest.mark.parametrize( 99 | "keypath,node,expected_output", 100 | ( 101 | ( 102 | b"\x00", 103 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 104 | b"\x00\x10\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 105 | ), 106 | ( 107 | b"", 108 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 109 | None, 110 | ), 111 | ( 112 | b"\x00", 113 | b"\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 114 | None, 115 | ), 116 | (b"\x00", 12345, None), 117 | (b"\x00", range(32), None), 118 | ( 119 | b"\x01", 120 | b"\x00\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 121 | None, 122 | ), 123 | (b"\x02", b"", None), 124 | ), 125 | ) 126 | def test_encode_binary_trie_kv_node(keypath, node, expected_output): 127 | if expected_output: 128 | assert expected_output == encode_kv_node(keypath, node) 129 | else: 130 | with pytest.raises(ValidationError): 131 | encode_kv_node(keypath, node) 132 | 133 | 134 | @pytest.mark.parametrize( 135 | "left_child_node_hash,right_child_node_hash,expected_output", 136 | ( 137 | ( 138 | b"\xc8\x9e\xfd\xaaT\xc0\xf2\x0cz\xdfa(\x82\xdf\tP\xf5\xa9Qc~\x03\x07\xcd\xcbLg/)\x8b\x8b\xc6", # noqa: E501 139 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 140 | ( 141 | b"\x01\xc8\x9e\xfd\xaaT\xc0\xf2\x0cz\xdfa(\x82\xdf\tP\xf5\xa9Qc~\x03\x07\xcd\xcbLg/)\x8b\x8b\xc6\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p" # noqa: E501 142 | ), 143 | ), 144 | ( 145 | b"", 146 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 147 | None, 148 | ), 149 | ( 150 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 151 | b"\x01", 152 | None, 153 | ), 154 | ( 155 | b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 156 | 12345, 157 | None, 158 | ), 159 | ([3] * 32, [4] * 32, None), 160 | (b"\x01" * 33, b"\x01" * 32, None), 161 | ), 162 | ) 163 | def test_encode_binary_trie_branch_node( 164 | left_child_node_hash, right_child_node_hash, expected_output 165 | ): 166 | if expected_output: 167 | assert expected_output == encode_branch_node( 168 | left_child_node_hash, right_child_node_hash 169 | ) 170 | else: 171 | with pytest.raises(ValidationError): 172 | encode_branch_node(left_child_node_hash, right_child_node_hash) 173 | 174 | 175 | @pytest.mark.parametrize( 176 | "value,expected_output", 177 | ( 178 | (b"\x03\x04\x05", b"\x02\x03\x04\x05"), 179 | (b"", None), 180 | (12345, None), 181 | (range(5), None), 182 | ), 183 | ) 184 | def test_encode_binary_trie_leaf_node(value, expected_output): 185 | if expected_output: 186 | assert expected_output == encode_leaf_node(value) 187 | else: 188 | with pytest.raises(ValidationError): 189 | encode_leaf_node(value) 190 | -------------------------------------------------------------------------------- /tests/core/test_proof.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from eth_hash.auto import ( 4 | keccak, 5 | ) 6 | 7 | from trie.exceptions import ( 8 | BadTrieProof, 9 | ) 10 | from trie.hexary import ( 11 | HexaryTrie, 12 | ) 13 | 14 | 15 | def test_get_from_proof_key_exists(): 16 | from .sample_proof_key_exists import ( 17 | key, 18 | proof, 19 | state_root, 20 | ) 21 | 22 | assert HexaryTrie.get_from_proof(state_root, key, proof) != b"" 23 | 24 | 25 | def test_get_from_proof_key_does_not_exist(): 26 | from .sample_proof_key_does_not_exist import ( 27 | key, 28 | proof, 29 | state_root, 30 | ) 31 | 32 | assert HexaryTrie.get_from_proof(state_root, key, proof) == b"" 33 | 34 | 35 | def test_get_proof_key_does_not_exist(): 36 | trie = HexaryTrie({}) 37 | trie[b"hello"] = b"world" 38 | trie[b"hi"] = b"there" 39 | proof = trie.get_proof(b"hey") 40 | 41 | assert len(proof) > 0 42 | assert HexaryTrie.get_from_proof(trie.root_hash, b"hey", proof) == b"" 43 | 44 | 45 | def test_get_from_proof_invalid(): 46 | from .sample_proof_key_exists import ( 47 | key, 48 | proof, 49 | state_root, 50 | ) 51 | 52 | proof[5][3] = b"" 53 | with pytest.raises(BadTrieProof): 54 | HexaryTrie.get_from_proof(state_root, key, proof) 55 | 56 | 57 | def test_get_from_proof_empty(): 58 | state_root = keccak(b"state root") 59 | key = keccak(b"some key") 60 | proof = [] 61 | with pytest.raises(BadTrieProof): 62 | HexaryTrie.get_from_proof(state_root, key, proof) 63 | -------------------------------------------------------------------------------- /tests/core/test_smt.py: -------------------------------------------------------------------------------- 1 | from hypothesis import ( 2 | given, 3 | strategies as st, 4 | ) 5 | 6 | from trie.smt import ( 7 | BLANK_NODE, 8 | SparseMerkleProof, 9 | SparseMerkleTree, 10 | calc_root, 11 | ) 12 | 13 | 14 | @given( 15 | k=st.binary(min_size=1, max_size=32), 16 | v=st.binary(min_size=1, max_size=32), 17 | ) 18 | def test_simple_kv(k, v): 19 | smt = SparseMerkleTree(key_size=len(k)) 20 | empty_root = smt.root_hash 21 | 22 | # Nothing has been added yet 23 | assert not smt.exists(k) 24 | 25 | # Now that something is added, it should be consistent 26 | smt.set(k, v) 27 | assert smt.get(k) == v 28 | assert smt.root_hash != empty_root 29 | assert smt.root_hash == calc_root(k, v, smt.branch(k)) 30 | 31 | # If you delete it, it goes away 32 | smt.delete(k) 33 | assert not smt.exists(k) 34 | assert smt.root_hash == empty_root 35 | 36 | 37 | @given( 38 | key_size=st.shared(st.integers(min_value=1, max_value=32), key="key_size"), 39 | # Do this so that the size of the keys (in bytes) matches the key_size for the test 40 | keys=st.shared(st.integers(), key="key_size").flatmap( 41 | lambda key_size: st.lists( 42 | elements=st.binary(min_size=key_size, max_size=key_size), 43 | min_size=3, 44 | max_size=3, 45 | unique=True, 46 | ) 47 | ), 48 | vals=st.lists( 49 | elements=st.binary(min_size=1, max_size=32), 50 | min_size=3, 51 | max_size=3, 52 | ), 53 | ) 54 | def test_branch_updates(key_size, keys, vals): 55 | # Empty tree 56 | smt = SparseMerkleTree(key_size=key_size) 57 | 58 | # NOTE: smt._get internal method is used for testing only 59 | # because it doesn't do null checks on the empty default 60 | EMPTY_NODE_HASHES = list(smt._get(keys[0])[1]) 61 | 62 | # Objects to track proof data 63 | proofs = {k: SparseMerkleProof(k, BLANK_NODE, EMPTY_NODE_HASHES) for k in keys} 64 | 65 | # Track the big list of all updates 66 | proof_updates = [] 67 | for k, p in proofs.items(): 68 | # Update the key in the smt a bunch of times 69 | for v in vals: 70 | proof_updates.append((k, v, smt.set(k, v))) 71 | 72 | # Merge all of the updates into the tracked proof entries 73 | for u in proof_updates: 74 | p.update(*u) 75 | 76 | # All updates should be consistent with the latest smt root 77 | assert p.root_hash == smt.root_hash 78 | -------------------------------------------------------------------------------- /tests/core/test_typing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from hypothesis import ( 4 | example, 5 | given, 6 | strategies as st, 7 | ) 8 | 9 | from trie.typing import ( 10 | Nibbles, 11 | ) 12 | 13 | 14 | @pytest.mark.parametrize( 15 | "valid_nibbles", 16 | ( 17 | (), 18 | (0, 0, 0), 19 | (0xF,) * 128, # no length limit on the nibbles 20 | [ 21 | 0 22 | ], # list is an acceptable input to nibbles, though will be converted to tuple 23 | ), 24 | ) 25 | def test_valid_nibbles(valid_nibbles): 26 | typed_nibbles = Nibbles(valid_nibbles) 27 | assert typed_nibbles == tuple(valid_nibbles) 28 | 29 | 30 | @pytest.mark.parametrize( 31 | "invalid_nibbles, exception", 32 | ( 33 | (None, TypeError), 34 | ({0}, TypeError), # unordered set is not valid input 35 | ((b"F",), ValueError), 36 | (b"F", TypeError), 37 | ((b"\x00",), ValueError), 38 | ((b"\x0F",), ValueError), 39 | (0, TypeError), 40 | (0xF, TypeError), 41 | ((0, 0x10), ValueError), 42 | ((0, -1), ValueError), 43 | ), 44 | ) 45 | def test_invalid_nibbles(invalid_nibbles, exception): 46 | with pytest.raises(exception): 47 | Nibbles(invalid_nibbles) 48 | 49 | 50 | @given(st.lists(st.integers(min_value=0, max_value=0xF)), st.booleans()) 51 | @example([0], True) 52 | def test_nibbles_repr(nibbles_input, as_ipython): 53 | nibbles = Nibbles(nibbles_input) 54 | 55 | if as_ipython: 56 | 57 | class FakePrinter: 58 | str_buffer = "" 59 | 60 | def text(self, new_text): 61 | self.str_buffer += new_text 62 | 63 | p = FakePrinter() 64 | nibbles._repr_pretty_(p, cycle=False) 65 | repr_string = p.str_buffer 66 | else: 67 | repr_string = repr(nibbles) 68 | 69 | evaluated_repr = eval(repr_string) 70 | assert evaluated_repr == tuple(nibbles_input) 71 | 72 | re_cast = Nibbles(evaluated_repr) 73 | assert re_cast == nibbles 74 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{38,39,310,311,312,313}-core 4 | py{38,39,310,311,312,313}-lint 5 | py{38,39,310,311,312,313}-wheel 6 | windows-wheel 7 | docs 8 | 9 | [flake8] 10 | exclude=venv*,.tox,docs,build 11 | extend-ignore=E203 12 | max-line-length=88 13 | per-file-ignores=__init__.py:F401 14 | 15 | [blocklint] 16 | max_issue_threshold=1 17 | 18 | [testenv] 19 | usedevelop=True 20 | commands= 21 | core: pytest {posargs:tests/core} 22 | docs: make docs 23 | basepython= 24 | docs: python 25 | windows-wheel: python 26 | py38: python3.8 27 | py39: python3.9 28 | py310: python3.10 29 | py311: python3.11 30 | py312: python3.12 31 | py313: python3.13 32 | extras= 33 | test 34 | docs 35 | allowlist_externals=make,pre-commit 36 | 37 | [testenv:py{38,39,310,311,312,313}-lint] 38 | deps=pre-commit 39 | commands= 40 | pre-commit install 41 | pre-commit run --all-files --show-diff-on-failure 42 | 43 | [testenv:py{38,39,310,311,312,313}-wheel] 44 | deps= 45 | wheel 46 | build[virtualenv] 47 | allowlist_externals= 48 | /bin/rm 49 | /bin/bash 50 | commands= 51 | python -m pip install --upgrade pip 52 | /bin/rm -rf build dist 53 | python -m build 54 | /bin/bash -c 'python -m pip install --upgrade "$(ls dist/trie-*-py3-none-any.whl)" --progress-bar off' 55 | python -c "import trie" 56 | skip_install=true 57 | 58 | [testenv:windows-wheel] 59 | deps= 60 | wheel 61 | build[virtualenv] 62 | allowlist_externals= 63 | bash.exe 64 | commands= 65 | python --version 66 | python -m pip install --upgrade pip 67 | bash.exe -c "rm -rf build dist" 68 | python -m build 69 | bash.exe -c 'python -m pip install --upgrade "$(ls dist/trie-*-py3-none-any.whl)" --progress-bar off' 70 | python -c "import trie" 71 | skip_install=true 72 | -------------------------------------------------------------------------------- /trie/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import ( 2 | version as __version, 3 | ) 4 | 5 | from .binary import ( 6 | BinaryTrie, 7 | ) 8 | from .hexary import ( 9 | HexaryTrie, 10 | ) 11 | 12 | __version__ = __version("trie") 13 | -------------------------------------------------------------------------------- /trie/binary.py: -------------------------------------------------------------------------------- 1 | from eth_hash.auto import ( 2 | keccak, 3 | ) 4 | 5 | from trie.constants import ( 6 | BLANK_HASH, 7 | BRANCH_TYPE, 8 | BYTE_0, 9 | BYTE_1, 10 | KV_TYPE, 11 | LEAF_TYPE, 12 | ) 13 | from trie.exceptions import ( 14 | NodeOverrideError, 15 | ) 16 | from trie.utils.binaries import ( 17 | encode_to_bin, 18 | ) 19 | from trie.utils.nodes import ( 20 | encode_branch_node, 21 | encode_kv_node, 22 | encode_leaf_node, 23 | get_common_prefix_length, 24 | parse_node, 25 | ) 26 | from trie.validation import ( 27 | validate_is_bin_node, 28 | validate_is_bytes, 29 | ) 30 | 31 | 32 | class BinaryTrie: 33 | def __init__(self, db, root_hash=BLANK_HASH): 34 | self.db = db 35 | validate_is_bytes(root_hash) 36 | self.root_hash = root_hash 37 | 38 | def get(self, key): 39 | """ 40 | Fetches the value with a given keypath from the given node. 41 | 42 | Key will be encoded into binary array format first. 43 | """ 44 | validate_is_bytes(key) 45 | 46 | return self._get(self.root_hash, encode_to_bin(key)) 47 | 48 | def _get(self, node_hash, keypath): 49 | """ 50 | Note: keypath should be in binary array format, i.e., encoded by encode_to_bin() 51 | """ 52 | # Empty trie 53 | if node_hash == BLANK_HASH: 54 | return None 55 | nodetype, left_child, right_child = parse_node(self.db[node_hash]) 56 | # Key-value node descend 57 | if nodetype == LEAF_TYPE: 58 | if keypath: 59 | return None 60 | return right_child 61 | elif nodetype == KV_TYPE: 62 | # Keypath too short 63 | if not keypath: 64 | return None 65 | if keypath[: len(left_child)] == left_child: 66 | return self._get(right_child, keypath[len(left_child) :]) 67 | else: 68 | return None 69 | # Branch node descend 70 | elif nodetype == BRANCH_TYPE: 71 | # Keypath too short 72 | if not keypath: 73 | return None 74 | if keypath[:1] == BYTE_0: 75 | return self._get(left_child, keypath[1:]) 76 | else: 77 | return self._get(right_child, keypath[1:]) 78 | 79 | def set(self, key, value): 80 | """ 81 | Sets the value at the given keypath from the given node 82 | 83 | Key will be encoded into binary array format first. 84 | """ 85 | validate_is_bytes(key) 86 | validate_is_bytes(value) 87 | 88 | self.root_hash = self._set(self.root_hash, encode_to_bin(key), value) 89 | 90 | def _set(self, node_hash, keypath, value, if_delete_subtrie=False): 91 | """ 92 | If if_delete_subtrie is set to True, what it will do is that it take in a 93 | keypath and traverse til the end of keypath, then delete the whole subtrie 94 | of that node. 95 | 96 | Note: keypath should be in binary array format, i.e., encoded by encode_to_bin() 97 | """ 98 | # Empty trie 99 | if node_hash == BLANK_HASH: 100 | if value: 101 | return self._hash_and_save( 102 | encode_kv_node( 103 | keypath, self._hash_and_save(encode_leaf_node(value)) 104 | ) 105 | ) 106 | else: 107 | return BLANK_HASH 108 | nodetype, left_child, right_child = parse_node(self.db[node_hash]) 109 | # Node is a leaf node 110 | if nodetype == LEAF_TYPE: 111 | # Keypath must match, there should be no remaining keypath 112 | if keypath: 113 | raise NodeOverrideError( 114 | "Fail to set the value because the prefix of it's key" 115 | " is the same as existing key" 116 | ) 117 | if if_delete_subtrie: 118 | return BLANK_HASH 119 | return self._hash_and_save(encode_leaf_node(value)) if value else BLANK_HASH 120 | # node is a key-value node 121 | elif nodetype == KV_TYPE: 122 | # Keypath too short 123 | if not keypath: 124 | if if_delete_subtrie: 125 | return BLANK_HASH 126 | else: 127 | raise NodeOverrideError( 128 | "Fail to set the value because it's key" 129 | " is the prefix of other existing key" 130 | ) 131 | return self._set_kv_node( 132 | keypath, 133 | node_hash, 134 | nodetype, 135 | left_child, 136 | right_child, 137 | value, 138 | if_delete_subtrie, 139 | ) 140 | # node is a branch node 141 | elif nodetype == BRANCH_TYPE: 142 | # Keypath too short 143 | if not keypath: 144 | if if_delete_subtrie: 145 | return BLANK_HASH 146 | else: 147 | raise NodeOverrideError( 148 | "Fail to set the value because it's key" 149 | " is the prefix of other existing key" 150 | ) 151 | return self._set_branch_node( 152 | keypath, nodetype, left_child, right_child, value, if_delete_subtrie 153 | ) 154 | raise Exception("Invariant: This shouldn't ever happen") 155 | 156 | def _set_kv_node( 157 | self, 158 | keypath, 159 | node_hash, 160 | node_type, 161 | left_child, 162 | right_child, 163 | value, 164 | if_delete_subtrie=False, 165 | ): 166 | # Keypath prefixes match 167 | if if_delete_subtrie: 168 | if len(keypath) < len(left_child) and keypath == left_child[: len(keypath)]: 169 | return BLANK_HASH 170 | if keypath[: len(left_child)] == left_child: 171 | # Recurse into child 172 | subnode_hash = self._set( 173 | right_child, 174 | keypath[len(left_child) :], 175 | value, 176 | if_delete_subtrie, 177 | ) 178 | # If child is empty 179 | if subnode_hash == BLANK_HASH: 180 | return BLANK_HASH 181 | subnodetype, sub_left_child, sub_right_child = parse_node( 182 | self.db[subnode_hash] 183 | ) 184 | # If the child is a key-value node, compress together the keypaths 185 | # into one node 186 | if subnodetype == KV_TYPE: 187 | return self._hash_and_save( 188 | encode_kv_node(left_child + sub_left_child, sub_right_child) 189 | ) 190 | else: 191 | return self._hash_and_save(encode_kv_node(left_child, subnode_hash)) 192 | # Keypath prefixes don't match. Here we will be converting a key-value node 193 | # of the form (k, CHILD) into a structure of one of the following forms: 194 | # 1. (k[:-1], (NEWCHILD, CHILD)) 195 | # 2. (k[:-1], ((k2, NEWCHILD), CHILD)) 196 | # 3. (k1, ((k2, CHILD), NEWCHILD)) 197 | # 4. (k1, ((k2, CHILD), (k2', NEWCHILD)) 198 | # 5. (CHILD, NEWCHILD) 199 | # 6. ((k[1:], CHILD), (k', NEWCHILD)) 200 | # 7. ((k[1:], CHILD), NEWCHILD) 201 | # 8. (CHILD, (k[1:], NEWCHILD)) 202 | else: 203 | common_prefix_len = get_common_prefix_length( 204 | left_child, keypath[: len(left_child)] 205 | ) 206 | # New key-value pair can not contain empty value 207 | # Or one can not delete non-exist subtrie 208 | if not value or if_delete_subtrie: 209 | return node_hash 210 | # valnode: the child node that has the new value we are adding 211 | # Case 1: keypath prefixes almost match, 212 | # so we are in case (1), (2), (5), (6) 213 | if len(keypath) == common_prefix_len + 1: 214 | valnode = self._hash_and_save(encode_leaf_node(value)) 215 | # Case 2: keypath prefixes mismatch in the middle, so we need to break 216 | # the keypath in half. We are in case (3), (4), (7), (8) 217 | else: 218 | if len(keypath) <= common_prefix_len: 219 | raise NodeOverrideError( 220 | "Fail to set the value because it's key" 221 | " is the prefix of other existing key" 222 | ) 223 | valnode = self._hash_and_save( 224 | encode_kv_node( 225 | keypath[common_prefix_len + 1 :], 226 | self._hash_and_save(encode_leaf_node(value)), 227 | ) 228 | ) 229 | # oldnode: the child node the has the old child value 230 | # Case 1: (1), (3), (5), (6) 231 | if len(left_child) == common_prefix_len + 1: 232 | oldnode = right_child 233 | # (2), (4), (6), (8) 234 | else: 235 | oldnode = self._hash_and_save( 236 | encode_kv_node(left_child[common_prefix_len + 1 :], right_child) 237 | ) 238 | # Create the new branch node (because the key paths diverge, there has to 239 | # be some "first bit" at which they diverge, so there must be a branch 240 | # node somewhere) 241 | if keypath[common_prefix_len : common_prefix_len + 1] == BYTE_1: 242 | newsub = self._hash_and_save(encode_branch_node(oldnode, valnode)) 243 | else: 244 | newsub = self._hash_and_save(encode_branch_node(valnode, oldnode)) 245 | # Case 1: keypath prefixes match in the first bit, so we still need 246 | # a kv node at the top 247 | # (1) (2) (3) (4) 248 | if common_prefix_len: 249 | return self._hash_and_save( 250 | encode_kv_node(left_child[:common_prefix_len], newsub) 251 | ) 252 | # Case 2: keypath prefixes diverge in the first bit, so we replace the 253 | # kv node with a branch node 254 | # (5) (6) (7) (8) 255 | else: 256 | return newsub 257 | 258 | def _set_branch_node( 259 | self, 260 | keypath, 261 | node_type, 262 | left_child, 263 | right_child, 264 | value, 265 | if_delete_subtrie=False, 266 | ): 267 | # Which child node to update? Depends on first bit in keypath 268 | if keypath[:1] == BYTE_0: 269 | new_left_child = self._set( 270 | left_child, keypath[1:], value, if_delete_subtrie 271 | ) 272 | new_right_child = right_child 273 | else: 274 | new_right_child = self._set( 275 | right_child, keypath[1:], value, if_delete_subtrie 276 | ) 277 | new_left_child = left_child 278 | # Compress branch node into kv node 279 | if new_left_child == BLANK_HASH or new_right_child == BLANK_HASH: 280 | subnodetype, sub_left_child, sub_right_child = parse_node( 281 | self.db[ 282 | new_left_child if new_left_child != BLANK_HASH else new_right_child 283 | ] 284 | ) 285 | first_bit = BYTE_1 if new_right_child != BLANK_HASH else BYTE_0 286 | # Compress (k1, (k2, NODE)) -> (k1 + k2, NODE) 287 | if subnodetype == KV_TYPE: 288 | return self._hash_and_save( 289 | encode_kv_node(first_bit + sub_left_child, sub_right_child) 290 | ) 291 | # kv node pointing to a branch node 292 | elif subnodetype in (BRANCH_TYPE, LEAF_TYPE): 293 | return self._hash_and_save( 294 | encode_kv_node( 295 | first_bit, 296 | new_left_child 297 | if new_left_child != BLANK_HASH 298 | else new_right_child, 299 | ) 300 | ) 301 | else: 302 | return self._hash_and_save( 303 | encode_branch_node(new_left_child, new_right_child) 304 | ) 305 | 306 | def exists(self, key): 307 | validate_is_bytes(key) 308 | 309 | return self.get(key) is not None 310 | 311 | def delete(self, key): 312 | """ 313 | Equals to setting the value to None 314 | """ 315 | validate_is_bytes(key) 316 | 317 | self.root_hash = self._set(self.root_hash, encode_to_bin(key), b"") 318 | 319 | def delete_subtrie(self, key): 320 | """ 321 | Given a key prefix, delete the whole subtrie that starts with the key prefix. 322 | 323 | Key will be encoded into binary array format first. 324 | 325 | It will call `_set` with `if_delete_subtrie` set to True. 326 | """ 327 | validate_is_bytes(key) 328 | 329 | self.root_hash = self._set( 330 | self.root_hash, 331 | encode_to_bin(key), 332 | value=b"", 333 | if_delete_subtrie=True, 334 | ) 335 | 336 | # 337 | # Convenience 338 | # 339 | @property 340 | def root_node(self): 341 | return self.db[self.root_hash] 342 | 343 | @root_node.setter 344 | def root_node(self, node): 345 | validate_is_bin_node(node) 346 | 347 | self.root_hash = self._hash_and_save(node) 348 | 349 | # 350 | # Utils 351 | # 352 | def _hash_and_save(self, node): 353 | """ 354 | Saves a node into the database and returns its hash 355 | """ 356 | validate_is_bin_node(node) 357 | 358 | node_hash = keccak(node) 359 | self.db[node_hash] = node 360 | return node_hash 361 | 362 | # 363 | # Dictionary API 364 | # 365 | def __getitem__(self, key): 366 | return self.get(key) 367 | 368 | def __setitem__(self, key, value): 369 | return self.set(key, value) 370 | 371 | def __delitem__(self, key): 372 | return self.delete(key) 373 | 374 | def __contains__(self, key): 375 | return self.exists(key) 376 | -------------------------------------------------------------------------------- /trie/branches.py: -------------------------------------------------------------------------------- 1 | from eth_hash.auto import ( 2 | keccak, 3 | ) 4 | 5 | from trie.binary import ( 6 | BinaryTrie, 7 | ) 8 | from trie.constants import ( 9 | BLANK_HASH, 10 | BRANCH_TYPE, 11 | BYTE_0, 12 | KV_TYPE, 13 | LEAF_TYPE, 14 | ) 15 | from trie.exceptions import ( 16 | InvalidKeyError, 17 | ) 18 | from trie.utils.binaries import ( 19 | encode_to_bin, 20 | ) 21 | from trie.utils.nodes import ( 22 | parse_node, 23 | ) 24 | from trie.validation import ( 25 | validate_is_bin_node, 26 | validate_is_bytes, 27 | ) 28 | 29 | 30 | def check_if_branch_exist(db, root_hash, key_prefix): 31 | """ 32 | Given a key prefix, return whether this prefix is 33 | the prefix of an existing key in the trie. 34 | """ 35 | validate_is_bytes(key_prefix) 36 | 37 | return _check_if_branch_exist(db, root_hash, encode_to_bin(key_prefix)) 38 | 39 | 40 | def _check_if_branch_exist(db, node_hash, key_prefix): 41 | # Empty trie 42 | if node_hash == BLANK_HASH: 43 | return False 44 | nodetype, left_child, right_child = parse_node(db[node_hash]) 45 | if nodetype == LEAF_TYPE: 46 | if key_prefix: 47 | return False 48 | return True 49 | elif nodetype == KV_TYPE: 50 | if not key_prefix: 51 | return True 52 | if len(key_prefix) < len(left_child): 53 | if key_prefix == left_child[: len(key_prefix)]: 54 | return True 55 | return False 56 | else: 57 | if key_prefix[: len(left_child)] == left_child: 58 | return _check_if_branch_exist( 59 | db, right_child, key_prefix[len(left_child) :] 60 | ) 61 | return False 62 | elif nodetype == BRANCH_TYPE: 63 | if not key_prefix: 64 | return True 65 | if key_prefix[:1] == BYTE_0: 66 | return _check_if_branch_exist(db, left_child, key_prefix[1:]) 67 | else: 68 | return _check_if_branch_exist(db, right_child, key_prefix[1:]) 69 | else: 70 | raise Exception("Invariant: unreachable code path") 71 | 72 | 73 | def get_branch(db, root_hash, key): 74 | """ 75 | Get a long-format Merkle branch 76 | """ 77 | validate_is_bytes(key) 78 | 79 | return tuple(_get_branch(db, root_hash, encode_to_bin(key))) 80 | 81 | 82 | def _get_branch(db, node_hash, keypath): 83 | if node_hash == BLANK_HASH: 84 | return 85 | node = db[node_hash] 86 | nodetype, left_child, right_child = parse_node(node) 87 | if nodetype == LEAF_TYPE: 88 | if not keypath: 89 | yield node 90 | else: 91 | raise InvalidKeyError("Key too long") 92 | elif nodetype == KV_TYPE: 93 | if not keypath: 94 | raise InvalidKeyError("Key too short") 95 | if keypath[: len(left_child)] == left_child: 96 | yield node 97 | yield from _get_branch(db, right_child, keypath[len(left_child) :]) 98 | else: 99 | yield node 100 | elif nodetype == BRANCH_TYPE: 101 | if not keypath: 102 | raise InvalidKeyError("Key too short") 103 | if keypath[:1] == BYTE_0: 104 | yield node 105 | yield from _get_branch(db, left_child, keypath[1:]) 106 | else: 107 | yield node 108 | yield from _get_branch(db, right_child, keypath[1:]) 109 | else: 110 | raise Exception("Invariant: unreachable code path") 111 | 112 | 113 | def if_branch_valid(branch, root_hash, key, value): 114 | # value being None means the key is not in the trie 115 | if value is not None: 116 | validate_is_bytes(key) 117 | # branch must not be empty 118 | assert branch 119 | for node in branch: 120 | validate_is_bin_node(node) 121 | 122 | db = {keccak(node): node for node in branch} 123 | assert BinaryTrie(db=db, root_hash=root_hash).get(key) == value 124 | return True 125 | 126 | 127 | def get_trie_nodes(db, node_hash): 128 | """ 129 | Get full trie of a given root node 130 | """ 131 | return tuple(_get_trie_nodes(db, node_hash)) 132 | 133 | 134 | def _get_trie_nodes(db, node_hash): 135 | if node_hash in db: 136 | node = db[node_hash] 137 | else: 138 | return 139 | nodetype, left_child, right_child = parse_node(node) 140 | if nodetype == KV_TYPE: 141 | yield node 142 | yield from get_trie_nodes(db, right_child) 143 | elif nodetype == BRANCH_TYPE: 144 | yield node 145 | yield from get_trie_nodes(db, left_child) 146 | yield from get_trie_nodes(db, right_child) 147 | elif nodetype == LEAF_TYPE: 148 | yield node 149 | else: 150 | raise Exception("Invariant: unreachable code path") 151 | 152 | 153 | def get_witness_for_key_prefix(db, node_hash, key): 154 | """ 155 | Get all witness given a keypath prefix. 156 | Include 157 | 158 | 1. witness along the keypath and 159 | 2. witness in the subtrie of the last node in keypath 160 | """ 161 | validate_is_bytes(key) 162 | 163 | return tuple(_get_witness_for_key_prefix(db, node_hash, encode_to_bin(key))) 164 | 165 | 166 | def _get_witness_for_key_prefix(db, node_hash, keypath): 167 | if not keypath: 168 | yield from get_trie_nodes(db, node_hash) 169 | if node_hash in db: 170 | node = db[node_hash] 171 | else: 172 | return 173 | nodetype, left_child, right_child = parse_node(node) 174 | if nodetype == LEAF_TYPE: 175 | if keypath: 176 | raise InvalidKeyError("Key too long") 177 | elif nodetype == KV_TYPE: 178 | if len(keypath) < len(left_child) and left_child[: len(keypath)] == keypath: 179 | yield node 180 | yield from get_trie_nodes(db, right_child) 181 | elif keypath[: len(left_child)] == left_child: 182 | yield node 183 | yield from _get_witness_for_key_prefix( 184 | db, right_child, keypath[len(left_child) :] 185 | ) 186 | else: 187 | yield node 188 | elif nodetype == BRANCH_TYPE: 189 | if keypath[:1] == BYTE_0: 190 | yield node 191 | yield from _get_witness_for_key_prefix(db, left_child, keypath[1:]) 192 | else: 193 | yield node 194 | yield from _get_witness_for_key_prefix(db, right_child, keypath[1:]) 195 | else: 196 | raise Exception("Invariant: unreachable code path") 197 | -------------------------------------------------------------------------------- /trie/constants.py: -------------------------------------------------------------------------------- 1 | BLANK_NODE = b"" 2 | # keccak(b'') 3 | BLANK_HASH = b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p" # noqa: E501 4 | # keccak(rlp.encode(b'')) 5 | BLANK_NODE_HASH = b"V\xe8\x1f\x17\x1b\xccU\xa6\xff\x83E\xe6\x92\xc0\xf8n\x5bH\xe0\x1b\x99l\xad\xc0\x01b/\xb5\xe3c\xb4!" # noqa: E501 6 | 7 | 8 | NIBBLE_TERMINATOR = 16 9 | 10 | HP_FLAG_2 = 2 11 | HP_FLAG_0 = 0 12 | 13 | 14 | NODE_TYPE_BLANK = 0 15 | NODE_TYPE_LEAF = 1 16 | NODE_TYPE_EXTENSION = 2 17 | NODE_TYPE_BRANCH = 3 18 | 19 | # Constants for Binary Trie 20 | EXP = tuple(reversed(tuple(2**i for i in range(8)))) 21 | 22 | TWO_BITS = [bytes([0, 0]), bytes([0, 1]), bytes([1, 0]), bytes([1, 1])] 23 | PREFIX_00 = bytes([0, 0]) 24 | PREFIX_100000 = bytes([1, 0, 0, 0, 0, 0]) 25 | 26 | KV_TYPE = 0 27 | BRANCH_TYPE = 1 28 | LEAF_TYPE = 2 29 | BINARY_TRIE_NODE_TYPES = (0, 1, 2) 30 | KV_TYPE_PREFIX = bytes([0]) 31 | BRANCH_TYPE_PREFIX = bytes([1]) 32 | LEAF_TYPE_PREFIX = bytes([2]) 33 | 34 | BYTE_1 = bytes([1]) 35 | BYTE_0 = bytes([0]) 36 | -------------------------------------------------------------------------------- /trie/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Optional, 3 | ) 4 | 5 | from eth_typing import ( 6 | Hash32, 7 | ) 8 | from hexbytes import ( 9 | HexBytes, 10 | ) 11 | 12 | from trie.constants import ( 13 | NODE_TYPE_EXTENSION, 14 | NODE_TYPE_LEAF, 15 | ) 16 | from trie.typing import ( 17 | HexaryTrieNode, 18 | Nibbles, 19 | NibblesInput, 20 | NodeType, 21 | ) 22 | 23 | 24 | class InvalidNibbles(Exception): 25 | pass 26 | 27 | 28 | class InvalidNode(Exception): 29 | pass 30 | 31 | 32 | class ValidationError(Exception): 33 | pass 34 | 35 | 36 | class BadTrieProof(Exception): 37 | pass 38 | 39 | 40 | class NodeOverrideError(Exception): 41 | pass 42 | 43 | 44 | class InvalidKeyError(Exception): 45 | pass 46 | 47 | 48 | class SyncRequestAlreadyProcessed(Exception): 49 | pass 50 | 51 | 52 | class MissingTrieNode(Exception): 53 | """ 54 | Raised when a node of the trie is not available in the database, 55 | in the current state root. 56 | 57 | This may happen when trying to read out the value of a key, or when simply 58 | traversing the trie. 59 | """ 60 | 61 | def __init__( 62 | self, 63 | missing_node_hash: Hash32, 64 | root_hash: Hash32, 65 | requested_key: bytes, 66 | prefix: Nibbles = None, 67 | *args, 68 | ): 69 | if not isinstance(missing_node_hash, bytes): 70 | raise TypeError( 71 | "Missing node hash must be bytes, was: %r" % missing_node_hash 72 | ) 73 | elif not isinstance(root_hash, bytes): 74 | raise TypeError("Root hash must be bytes, was: %r" % root_hash) 75 | elif not isinstance(requested_key, bytes): 76 | raise TypeError("Requested key must be bytes, was: %r" % requested_key) 77 | 78 | if prefix is not None: 79 | prefix_nibbles: Optional[Nibbles] = Nibbles(prefix) 80 | else: 81 | prefix_nibbles = None 82 | 83 | super().__init__( 84 | HexBytes(missing_node_hash), 85 | HexBytes(root_hash), 86 | HexBytes(requested_key), 87 | prefix_nibbles, 88 | *args, 89 | ) 90 | 91 | def __repr__(self) -> str: 92 | return ( 93 | f"MissingTrieNode({self.missing_node_hash!r}, {self.root_hash!r}, " 94 | f"{self.requested_key!r}, prefix={self.prefix!r})" 95 | ) 96 | 97 | def __str__(self) -> str: 98 | return ( 99 | f"Trie database is missing hash {self.missing_node_hash!r} needed to look " 100 | f"up node at prefix {self.prefix}, when searching for key " 101 | f"{self.requested_key!r} at root hash {self.root_hash!r}" 102 | ) 103 | 104 | @property 105 | def missing_node_hash(self) -> HexBytes: 106 | return self.args[0] 107 | 108 | @property 109 | def root_hash(self) -> HexBytes: 110 | return self.args[1] 111 | 112 | @property 113 | def requested_key(self) -> HexBytes: 114 | return self.args[2] 115 | 116 | @property 117 | def prefix(self) -> Optional[Nibbles]: 118 | """ 119 | The tuple of nibbles that navigate to the missing node. For example, a missing 120 | root would have a prefix of (), and a missing left-most child of the 121 | root would have a prefix of (0, ). 122 | """ 123 | return self.args[3] 124 | 125 | 126 | class MissingTraversalNode(Exception): 127 | """ 128 | Raised when a node of the trie is not available in the database, 129 | during HexaryTrie.traverse() or .traverse_from(). 130 | 131 | This is triggered in the same situation as MissingTrieNode, but with less 132 | information available, because traversal can start from the middle of a trie. 133 | - traverse_from() ignore's the trie's root, so the root hash is unknown 134 | - the requested_key and prefix are unavailable because only the suffix of 135 | the key is known 136 | """ 137 | 138 | def __init__( 139 | self, missing_node_hash: Hash32, nibbles_traversed: NibblesInput, *args 140 | ) -> None: 141 | if not isinstance(missing_node_hash, bytes): 142 | raise TypeError( 143 | "Missing node hash must be bytes, was: %r" % missing_node_hash 144 | ) 145 | 146 | super().__init__(HexBytes(missing_node_hash), Nibbles(nibbles_traversed), *args) 147 | 148 | def __repr__(self) -> str: 149 | return ( 150 | f"MissingTraversalNode({self.missing_node_hash!r}, " 151 | f"{self.nibbles_traversed!r})" 152 | ) 153 | 154 | def __str__(self) -> str: 155 | return ( 156 | f"Trie database is missing hash {self.missing_node_hash!r}, found when " 157 | f"traversing down {self.nibbles_traversed}." 158 | ) 159 | 160 | @property 161 | def missing_node_hash(self) -> HexBytes: 162 | return self.args[0] 163 | 164 | @property 165 | def nibbles_traversed(self) -> Nibbles: 166 | """ 167 | Nibbles traversed down from the starting node to the missing node. 168 | """ 169 | return self.args[1] 170 | 171 | 172 | class TraversedPartialPath(Exception): 173 | """ 174 | Raised when a traversal key ends in the middle of a partial path. It might be in 175 | an extension node or a leaf node. 176 | """ 177 | 178 | def __init__( 179 | self, 180 | nibbles_traversed: NibblesInput, 181 | node: HexaryTrieNode, 182 | untraversed_tail: NibblesInput, 183 | *args, 184 | ) -> None: 185 | super().__init__( 186 | Nibbles(nibbles_traversed), 187 | node, 188 | Nibbles(untraversed_tail), 189 | *args, 190 | ) 191 | self._simulated_node = self._make_simulated_node() 192 | 193 | def __repr__(self) -> str: 194 | return ( 195 | f"TraversedPartialPath({self.nibbles_traversed}, {self.node}," 196 | f" {self.untraversed_tail})" 197 | ) 198 | 199 | def __str__(self) -> str: 200 | return ( 201 | f"Could not traverse through {self.node} at {self.nibbles_traversed}, only " 202 | f"partially traversed with: {self.untraversed_tail}" 203 | ) 204 | 205 | @property 206 | def nibbles_traversed(self) -> Nibbles: 207 | """ 208 | The nibbles traversed until the attached node, which could not be traversed into 209 | """ 210 | return self.args[0] 211 | 212 | @property 213 | def node(self) -> HexaryTrieNode: 214 | """ 215 | The node which could not be traversed into. This is any leaf, or an extension 216 | node where traversal went part-way into the path. It must not be a branch node. 217 | """ 218 | return self.args[1] 219 | 220 | @property 221 | def untraversed_tail(self) -> Nibbles: 222 | """ 223 | The nibbles that only reached partially into the extension or leaf node. 224 | """ 225 | return self.args[2] 226 | 227 | @property 228 | def simulated_node(self) -> HexaryTrieNode: 229 | """ 230 | For the purposes of walking a trie, we might only be interested in the 231 | sub_segments, suffix, etc, of the node -- but assuming we actually had a node 232 | immediately at the requested prefix. This returns a node simulated as if that 233 | were true. 234 | 235 | See the trie walk tests for an example of how this is used. 236 | """ 237 | return self._simulated_node 238 | 239 | def _make_simulated_node(self) -> HexaryTrieNode: 240 | from trie.utils.nodes import ( 241 | compute_extension_key, 242 | compute_leaf_key, 243 | key_starts_with, 244 | ) 245 | 246 | actual_node = self.node 247 | key_tail = self.untraversed_tail 248 | actual_sub_segments = actual_node.sub_segments 249 | 250 | if len(key_tail) == 0: 251 | raise ValueError( 252 | "Can only raise a TraversedPartialPath when some series " 253 | "of nibbles was untraversed" 254 | ) 255 | 256 | if len(actual_sub_segments) == 0: 257 | if not key_starts_with(actual_node.suffix, key_tail): 258 | raise ValidationError( 259 | f"Internal traverse bug: {actual_node.suffix} " 260 | f"does not start with {key_tail}" 261 | ) 262 | else: 263 | trimmed_suffix = Nibbles(actual_node.suffix[len(key_tail) :]) 264 | 265 | return HexaryTrieNode( 266 | (), 267 | actual_node.value, 268 | trimmed_suffix, 269 | [compute_leaf_key(trimmed_suffix), actual_node.raw[1]], 270 | NodeType(NODE_TYPE_LEAF), 271 | ) 272 | elif len(actual_sub_segments) == 1: 273 | extension = actual_sub_segments[0] 274 | if not key_starts_with(extension, key_tail): 275 | raise ValidationError( 276 | f"Internal traverse bug: extension {extension} does not start " 277 | f"with {key_tail}" 278 | ) 279 | elif len(key_tail) == len(extension): 280 | raise ValidationError( 281 | f"Internal traverse bug: {key_tail} should not equal {extension}" 282 | ) 283 | else: 284 | trimmed_extension = Nibbles(extension[len(key_tail) :]) 285 | 286 | return HexaryTrieNode( 287 | (trimmed_extension,), 288 | actual_node.value, 289 | actual_node.suffix, 290 | [compute_extension_key(trimmed_extension), actual_node.raw[1]], 291 | NodeType(NODE_TYPE_EXTENSION), 292 | ) 293 | else: 294 | raise ValidationError( 295 | f"Can only partially traverse into leaf or extension, got {actual_node}" 296 | ) 297 | 298 | 299 | class PerfectVisibility(Exception): 300 | """ 301 | Raised when calling :class:`trie.fog.HexaryTrieFog` methods that look for unknown 302 | prefixes, like :meth:`~trie.fog.HexaryTrieFog.nearest_unknown`, and there are no 303 | unknown parts of the trie. (in other words the fog reports 304 | :meth:`~trie.fog.HexaryTrieFog.is_complete` as True. 305 | """ 306 | 307 | 308 | class FullDirectionalVisibility(Exception): 309 | """ 310 | Raised when calling :meth:`trie.fog.HexaryTrieFog.nearest_right`, and there are no 311 | unknown prefixes *in that direction* of the trie. (The fog may not report 312 | :meth:`~trie.fog.HexaryTrieFog.is_complete` as True, because more may be 313 | available to the left). 314 | """ 315 | -------------------------------------------------------------------------------- /trie/fog.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from itertools import ( 3 | zip_longest, 4 | ) 5 | from typing import ( 6 | Any, 7 | Dict, 8 | Iterable, 9 | Sequence, 10 | Tuple, 11 | ) 12 | 13 | from eth_utils import ( 14 | ValidationError, 15 | to_tuple, 16 | ) 17 | from sortedcontainers import ( 18 | SortedSet, 19 | ) 20 | 21 | from trie.exceptions import ( 22 | FullDirectionalVisibility, 23 | PerfectVisibility, 24 | ) 25 | from trie.typing import ( 26 | GenericSortedSet, 27 | HexaryTrieNode, 28 | Nibbles, 29 | NibblesInput, 30 | ) 31 | from trie.utils.nibbles import ( 32 | decode_nibbles, 33 | encode_nibbles, 34 | ) 35 | from trie.utils.nodes import ( 36 | key_starts_with, 37 | ) 38 | 39 | 40 | class HexaryTrieFog: 41 | """ 42 | Keeps track of which parts of a trie have been verified to exist. 43 | 44 | Named after "fog of war" popular in video games like... Red Alert? IDK, I'm old. 45 | 46 | Object is immutable. Any changes, like marking a key prefix as complete, will 47 | return a new HexaryTrieFog object. 48 | """ 49 | 50 | _unexplored_prefixes: GenericSortedSet[Nibbles] 51 | 52 | # INVARIANT: No unexplored prefix may start with another unexplored prefix 53 | # For example, _unexplored_prefixes may not be {(1, 2), (1, 2, 3)}. 54 | 55 | def __init__(self) -> None: 56 | # Always start without knowing anything about a trie. The only unexplored 57 | # prefix is the root prefix: (), which means the whole trie is unexplored. 58 | self._unexplored_prefixes = SortedSet({()}) 59 | 60 | def __repr__(self) -> str: 61 | return f"HexaryTrieFog<{self._unexplored_prefixes!r}>" 62 | 63 | @property 64 | def is_complete(self) -> bool: 65 | return len(self._unexplored_prefixes) == 0 66 | 67 | def explore( 68 | self, old_prefix_input: NibblesInput, foggy_sub_segments: Sequence[NibblesInput] 69 | ) -> "HexaryTrieFog": 70 | """ 71 | The fog lifts from the old prefix. This call returns a HexaryTrieFog that 72 | narrows down the unexplored key prefixes. from the old prefix to the indicated 73 | children. 74 | 75 | For example, if only the key prefix 0x12 is unexplored, then calling 76 | explore((1, 2), ((3,), (0xe, 0xf))) would mark large swaths of 0x12 explored, 77 | leaving only two prefixes as unknown: 0x123 and 0x12ef. To continue exploring 78 | those prefixes, navigate to them using traverse() or traverse_from(). 79 | 80 | The sub_segments_input may be empty, which means the old prefix has been fully 81 | explored. 82 | """ 83 | old_prefix = Nibbles(old_prefix_input) 84 | sub_segments = [Nibbles(segment) for segment in foggy_sub_segments] 85 | new_fog_prefixes = self._unexplored_prefixes.copy() 86 | 87 | try: 88 | new_fog_prefixes.remove(old_prefix) 89 | except KeyError: 90 | raise ValidationError( 91 | f"Old parent {old_prefix} not found in {new_fog_prefixes!r}" 92 | ) 93 | 94 | if len(set(sub_segments)) != len(sub_segments): 95 | raise ValidationError( 96 | f"Got duplicate sub_segments in {sub_segments} " 97 | f"to HexaryTrieFog.explore()" 98 | ) 99 | 100 | # Further validation that no segment is a prefix of another 101 | all_lengths = {len(segment) for segment in sub_segments} 102 | if len(all_lengths) > 1: 103 | # The known use case of exploring nodes one at a time will never arrive in 104 | # this validation check which might be slow. Leaf nodes have no sub 105 | # segments, extension nodes have exactly one, and branch nodes have all 106 | # sub_segments of length 1. If a new use case hits this verification, 107 | # and speed becomes an issue, 108 | # see https://github.com/ethereum/py-trie/issues/107 109 | for segment in sub_segments: 110 | shorter_lengths = [ 111 | length for length in all_lengths if length < len(segment) 112 | ] 113 | for check_length in shorter_lengths: 114 | trimmed_segment = segment[:check_length] 115 | if trimmed_segment in sub_segments: 116 | raise ValidationError( 117 | f"Cannot add {segment} which is a child " 118 | f"of segment {trimmed_segment}" 119 | ) 120 | 121 | new_fog_prefixes.update([old_prefix + segment for segment in sub_segments]) 122 | return self._new_trie_fog(new_fog_prefixes) 123 | 124 | def mark_all_complete( 125 | self, prefix_inputs: Sequence[NibblesInput] 126 | ) -> "HexaryTrieFog": 127 | """ 128 | These might be leaves, or prefixes with 0 unknown keys within the range. 129 | 130 | This is equivalent to the following, but with better performance: 131 | 132 | result_fog = old_fog 133 | for complete_prefix in prefixes: 134 | result_fog = result_fog.explore(complete_prefix, ()) 135 | """ 136 | new_unexplored_prefixes = self._unexplored_prefixes.copy() 137 | for prefix in map(Nibbles, prefix_inputs): 138 | if prefix not in new_unexplored_prefixes: 139 | raise ValidationError( 140 | f"When marking {prefix} complete, could not " 141 | f"find in {new_unexplored_prefixes!r}" 142 | ) 143 | 144 | new_unexplored_prefixes.remove(prefix) 145 | return self._new_trie_fog(new_unexplored_prefixes) 146 | 147 | def nearest_unknown(self, key_input: NibblesInput = ()) -> Nibbles: 148 | """ 149 | Find the foggy prefix that is nearest to the supplied key. 150 | 151 | If prefixes are exactly the same distance to the left and right, 152 | then return the prefix on the right. 153 | 154 | :raises PerfectVisibility: if there are no foggy prefixes remaining 155 | """ 156 | key = Nibbles(key_input) 157 | 158 | index = self._unexplored_prefixes.bisect(key) 159 | 160 | if index == 0: 161 | # If sorted set is empty, bisect will return 0 162 | # But it might also return 0 if the search value is lower than the lowest 163 | # existing 164 | try: 165 | return self._unexplored_prefixes[0] 166 | except IndexError as exc: 167 | raise PerfectVisibility( 168 | "There are no more unexplored prefixes" 169 | ) from exc 170 | elif index == len(self._unexplored_prefixes): 171 | return self._unexplored_prefixes[-1] 172 | else: 173 | nearest_left = self._unexplored_prefixes[index - 1] 174 | nearest_right = self._unexplored_prefixes[index] 175 | 176 | # is the left or right unknown prefix closer? 177 | left_distance = self._prefix_distance(nearest_left, key) 178 | right_distance = self._prefix_distance(key, nearest_right) 179 | if left_distance < right_distance: 180 | return nearest_left 181 | else: 182 | return nearest_right 183 | 184 | def nearest_right(self, key_input: NibblesInput) -> Nibbles: 185 | """ 186 | Find the foggy prefix that is nearest on the right to the supplied key. 187 | 188 | :raises PerfectVisibility: if there are no foggy prefixes to the right 189 | """ 190 | key = Nibbles(key_input) 191 | 192 | index = self._unexplored_prefixes.bisect(key) 193 | 194 | if index == 0: 195 | # If sorted set is empty, bisect will return 0 196 | # But it might also return 0 if the search value is lower than the lowest 197 | # existing 198 | try: 199 | return self._unexplored_prefixes[0] 200 | except IndexError as exc: 201 | raise PerfectVisibility( 202 | "There are no more unexplored prefixes" 203 | ) from exc 204 | else: 205 | nearest_left = self._unexplored_prefixes[index - 1] 206 | 207 | # always return nearest right, unless prefix of key is unexplored 208 | if key_starts_with(key, nearest_left): 209 | return nearest_left 210 | else: 211 | try: 212 | # This can raise a IndexError if index == len(unexplored prefixes) 213 | return self._unexplored_prefixes[index] 214 | except IndexError as exc: 215 | raise FullDirectionalVisibility( 216 | f"There are no unexplored prefixes to the right of {key}" 217 | ) from exc 218 | 219 | @staticmethod 220 | @to_tuple 221 | def _prefix_distance(low_key: Nibbles, high_key: Nibbles) -> Iterable[int]: 222 | """ 223 | How far are the two keys from each other, as a sequence of differences. 224 | The first non-zero distance must be positive, but the remaining distances may 225 | be negative. Distances are designed to be simply compared, 226 | like distance1 < distance2. 227 | 228 | The high_key must be higher than the low key, or the output distances are not 229 | guaranteed to be accurate. 230 | """ 231 | for low_nibble, high_nibble in zip_longest(low_key, high_key, fillvalue=None): 232 | if low_nibble is None: 233 | final_low_nibble = 15 234 | else: 235 | final_low_nibble = low_nibble 236 | 237 | if high_nibble is None: 238 | final_high_nibble = 0 239 | else: 240 | final_high_nibble = high_nibble 241 | 242 | # Note: this might return a negative value. It's fine, because only the 243 | # relative distance matters. For example (1, 2) and (2, 1) produce a 244 | # distance of (1, -1). If the other reference point is (3, 1), making 245 | # the distance to the middle (1, 0), then the "correct" thing happened. 246 | # The (1, 2) key is a tiny bit closer to the (2, 1) key, and a tuple 247 | # comparison of the distance will show it as a smaller distance. 248 | yield final_high_nibble - final_low_nibble 249 | 250 | @classmethod 251 | def _new_trie_fog(cls, unexplored_prefixes: SortedSet) -> "HexaryTrieFog": 252 | """ 253 | Convert a set of unexplored prefixes to a proper HexaryTrieFog object. 254 | """ 255 | copy = cls() 256 | copy._unexplored_prefixes = unexplored_prefixes 257 | return copy 258 | 259 | def serialize(self) -> bytes: 260 | # encode nibbles to a bytes value, to compress this down a bit 261 | prefixes = [encode_nibbles(nibbles) for nibbles in self._unexplored_prefixes] 262 | return f"HexaryTrieFog:{prefixes!r}".encode() 263 | 264 | @classmethod 265 | def deserialize(cls, encoded: bytes) -> "HexaryTrieFog": 266 | serial_prefix = b"HexaryTrieFog:" 267 | if not encoded.startswith(serial_prefix): 268 | raise ValueError( 269 | f"Cannot deserialize this into HexaryTrieFog object: {encoded!r}" 270 | ) 271 | else: 272 | encoded_list = encoded[len(serial_prefix) :] 273 | prefix_list = ast.literal_eval(encoded_list.decode()) 274 | deserialized_prefixes = SortedSet( 275 | # decode nibbles from compressed bytes value, 276 | # and validate each value in range(16) 277 | Nibbles(decode_nibbles(prefix)) 278 | for prefix in prefix_list 279 | ) 280 | return cls._new_trie_fog(deserialized_prefixes) 281 | 282 | def __eq__(self, other: Any) -> bool: 283 | if not isinstance(other, HexaryTrieFog): 284 | return False 285 | else: 286 | return self._unexplored_prefixes == other._unexplored_prefixes 287 | 288 | 289 | class TrieFrontierCache: 290 | """ 291 | Keep a cache of HexaryTrieNodes for use with traverse_from. This 292 | can be used neatly with HexaryTrieFog to only keep a cache of the frontier 293 | of unexplored nodes, so that every expansion into a new unexplored node requires 294 | only one database lookup instead of log(n). 295 | """ 296 | 297 | def __init__(self) -> None: 298 | self._cache: Dict[Nibbles, Tuple[HexaryTrieNode, Nibbles]] = {} 299 | 300 | def get(self, prefix: NibblesInput) -> Tuple[HexaryTrieNode, Nibbles]: 301 | """ 302 | Find the cached node body of the parent of the given prefix. 303 | 304 | :return: parent node body, and the path from parent to the given prefix 305 | 306 | :raises KeyError: if there is no cached value for the prefix 307 | """ 308 | return self._cache[Nibbles(prefix)] 309 | 310 | def add( 311 | self, 312 | node_prefix_input: NibblesInput, 313 | trie_node: HexaryTrieNode, 314 | sub_segments: Sequence[NibblesInput], 315 | ) -> None: 316 | """ 317 | Add a new cached node body for each of the sub segments supplied. Later cache 318 | lookups will be in the form of get(node_prefix + sub_segments[0]). 319 | 320 | :param node_prefix: the path from the root to the cached node 321 | :param trie_node: the body to cache 322 | :param sub_segments: all of the children of the parent which should be made 323 | indexable 324 | """ 325 | node_prefix = Nibbles(node_prefix_input) 326 | 327 | # remove the cache entry for looking up node_prefix as a child 328 | if node_prefix != (): 329 | # If the cache entry doesn't exist, we can just ignore its absence 330 | self._cache.pop(Nibbles(node_prefix), None) 331 | 332 | # add cache entry for each child 333 | for segment in sub_segments: 334 | new_prefix = node_prefix + Nibbles(segment) 335 | self._cache[new_prefix] = (trie_node, Nibbles(segment)) 336 | 337 | def delete(self, prefix: NibblesInput) -> None: 338 | """ 339 | Delete the cache of the parent node for the given prefix. This only deletes 340 | this prefix's reference to the parent node, not all references to the parent 341 | node. 342 | """ 343 | # If the cache entry doesn't exist, we can just ignore its absence 344 | self._cache.pop(Nibbles(prefix), None) 345 | -------------------------------------------------------------------------------- /trie/iter.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Iterable, 3 | Optional, 4 | Tuple, 5 | ) 6 | 7 | from trie.exceptions import ( 8 | PerfectVisibility, 9 | ) 10 | from trie.fog import ( 11 | HexaryTrieFog, 12 | TrieFrontierCache, 13 | ) 14 | from trie.hexary import ( 15 | HexaryTrie, 16 | ) 17 | from trie.typing import ( 18 | HexaryTrieNode, 19 | Nibbles, 20 | ) 21 | from trie.utils.nibbles import ( 22 | bytes_to_nibbles, 23 | nibbles_to_bytes, 24 | ) 25 | from trie.utils.nodes import ( 26 | consume_common_prefix, 27 | ) 28 | 29 | 30 | class NodeIterator: 31 | """Iterate over all nodes of a trie, ensuring its consistency.""" 32 | 33 | def __init__(self, trie: HexaryTrie) -> None: 34 | self._trie = trie 35 | 36 | def next(self, key_bytes: Optional[bytes] = None) -> Optional[bytes]: 37 | """ 38 | Find the next key to the right from the given key, or None if there is 39 | no key to the right. 40 | 41 | .. NOTE:: To iterate the full trie, consider using keys() instead, for 42 | performance 43 | 44 | :param key_bytes: the key to start your search from. If None, return 45 | the first possible key. 46 | 47 | :return: key in bytes to the right of key_bytes, or None 48 | """ 49 | root = self._trie.root_node 50 | none_traversed = Nibbles(()) 51 | 52 | if key_bytes is None: 53 | next_key = self._get_next_key(root, none_traversed) 54 | else: 55 | key = bytes_to_nibbles(key_bytes) 56 | next_key = self._get_key_after(root, key, none_traversed) 57 | 58 | if next_key is None: 59 | return None 60 | else: 61 | return nibbles_to_bytes(next_key) 62 | 63 | def _get_key_after( 64 | self, node: HexaryTrieNode, key: Nibbles, traversed: Nibbles 65 | ) -> Optional[Nibbles]: 66 | """ 67 | Find the next key in the trie after key 68 | 69 | :param node: the source node to search for the next key after `key` 70 | :param key: the starting key used to seek the nearest key on the right 71 | :param traversed: the nibbles already traversed to get down to `node` 72 | 73 | :return: the complete key that is immediately to the right of `key` or None, 74 | if no key is immediately to the right (under `node`) 75 | """ 76 | for next_segment in node.sub_segments: 77 | if key[: len(next_segment)] > next_segment: 78 | # This segment is to the left of the key, keep looking... 79 | continue 80 | else: 81 | # Either: found the exact match, or the next result to the right 82 | # Either way, we'll want to take a look 83 | next_node = self._trie.traverse_from(node, next_segment) 84 | 85 | common, key_remaining, segment_remaining = consume_common_prefix( 86 | key, next_segment 87 | ) 88 | if len(segment_remaining) == 0: 89 | # Found a perfect match! Keep looking for keys to the 90 | # right of the target 91 | next_key = self._get_key_after( 92 | next_node, 93 | key_remaining, 94 | traversed + next_segment, 95 | ) 96 | if next_key is None: 97 | # Could not find a key to the right in any sub-node. 98 | # In other words, *only* the target key is in this sub-trie 99 | # So keep looking to the right... 100 | continue 101 | else: 102 | # We successfully found a key to the right in a subtree, 103 | # return it up 104 | return next_key 105 | else: 106 | # Found no exact match, and are now looking for 107 | # the next possible key 108 | return self._get_next_key(next_node, traversed + next_segment) 109 | 110 | if node.suffix > key: 111 | # This leaf node is to the right of the target key 112 | return traversed + node.suffix 113 | else: 114 | # Nothing found in any sub-segments 115 | return None 116 | 117 | def _get_next_key( 118 | self, node: HexaryTrieNode, traversed: Nibbles 119 | ) -> Optional[Nibbles]: 120 | """ 121 | Find the next possible key within the given node 122 | 123 | :param node: the parent node to search (plus all of its children) 124 | :param traversed: the key used to traverse down to `node` 125 | 126 | :return: the complete key that is the furthest left within `node` 127 | """ 128 | if node.value: 129 | # This is either a leaf node, or a branch node with a value. 130 | # The value in a branch node comes before all the child values 131 | return traversed + node.suffix 132 | elif len(node.sub_segments) == 0: 133 | # Only leaves should have 0 sub-segments, and should have a value. 134 | # There shouldn't be any way to navigate to a blank node, as long as 135 | # the trie hasn't changed during iteration. If it has... I guess 136 | # return None here. 137 | return None 138 | else: 139 | # This is a branch node with no value, or an extension node. 140 | # Either way, take the left-most child and repeat the search within it 141 | next_segment = node.sub_segments[0] 142 | next_node = self._trie.traverse_from(node, next_segment) 143 | return self._get_next_key(next_node, traversed + next_segment) 144 | 145 | def keys(self) -> Iterable[bytes]: 146 | """ 147 | Iterate over all trie keys from left to right. Some performance benefit over 148 | using :meth:`next` repeatedly, by caching node accesses between yielded values. 149 | """ 150 | for key, _ in self.items(): 151 | yield key 152 | 153 | def items(self) -> Iterable[Tuple[bytes, bytes]]: 154 | """ 155 | Iterate over all (key, value) pairs from left to right. 156 | """ 157 | for prefix, node in self.nodes(): 158 | if node.value: 159 | full_key = prefix + node.suffix 160 | yield nibbles_to_bytes(full_key), node.value 161 | 162 | def values(self) -> Iterable[bytes]: 163 | """ 164 | Iterate over all stored values from left to right. 165 | """ 166 | for _, node in self.nodes(): 167 | if node.value: 168 | yield node.value 169 | 170 | def nodes(self) -> Iterable[Tuple[Nibbles, HexaryTrieNode]]: 171 | """ 172 | Iterate over all trie nodes, starting at the left-most available one (the root), 173 | then the left-most available one (its left-most child) and so on. 174 | """ 175 | next_fog = HexaryTrieFog() 176 | cache = TrieFrontierCache() 177 | 178 | while True: 179 | try: 180 | # Always get the furthest left unexplored value 181 | nearest_prefix = next_fog.nearest_right(()) 182 | except PerfectVisibility: 183 | # No more unexplored nodes 184 | return 185 | 186 | try: 187 | cached_node, uncached_key = cache.get(nearest_prefix) 188 | except KeyError: 189 | node = self._trie.traverse(nearest_prefix) 190 | else: 191 | node = self._trie.traverse_from(cached_node, uncached_key) 192 | 193 | next_fog = next_fog.explore(nearest_prefix, node.sub_segments) 194 | 195 | if node.sub_segments: 196 | cache.add(nearest_prefix, node, node.sub_segments) 197 | else: 198 | cache.delete(nearest_prefix) 199 | 200 | yield nearest_prefix, node 201 | -------------------------------------------------------------------------------- /trie/smt.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Dict, 3 | Sequence, 4 | Tuple, 5 | ) 6 | 7 | from eth_typing import ( 8 | Hash32, 9 | ) 10 | from eth_utils import ( 11 | keccak, 12 | to_int, 13 | ) 14 | 15 | from trie.constants import ( 16 | BLANK_NODE, 17 | ) 18 | from trie.exceptions import ( 19 | ValidationError, 20 | ) 21 | from trie.validation import ( 22 | validate_is_bytes, 23 | validate_length, 24 | ) 25 | 26 | 27 | def calc_root(key: bytes, value: bytes, branch: Sequence[Hash32]) -> Hash32: 28 | r""" 29 | Obtain the merkle root of a given key/value/branch set. 30 | Can be used to validate a merkle proof or compute it's value from data. 31 | 32 | :param key: the keypath to decide the ordering of the sibling nodes in the branch 33 | :param value: the value (or leaf) that starts the merkle proof computation 34 | :param branch: the sequence of sibling nodes used to recursively perform the 35 | computation 36 | 37 | :return: the root hash of the merkle proof computation 38 | 39 | .. doctest:: 40 | 41 | >>> key = b'\x02' # Keypath 42 | >>> value = b'' # Value (or leaf) 43 | >>> branch = tuple([b'\x00'] * 8) # Any list of hashes 44 | >>> calc_root(key, value, branch) 45 | b'.+4IKt[\xd2\x14\xe4).\xf5\xc6\n\x11=\x01\xe89\xa1Z\x07#\xfd~(;\xfb\xb8\x8a\x0e' # noqa: E501 46 | 47 | """ 48 | validate_is_bytes(key) 49 | validate_is_bytes(value) 50 | validate_length(branch, len(key) * 8) 51 | 52 | path = to_int(key) 53 | target_bit = 1 54 | # traverse the path in leaf->root order 55 | # branch is in root->leaf order (key is in MSB to LSB order) 56 | node_hash = keccak(value) 57 | for sibling_node in reversed(branch): 58 | if path & target_bit: 59 | node_hash = keccak(sibling_node + node_hash) 60 | else: 61 | node_hash = keccak(node_hash + sibling_node) 62 | target_bit <<= 1 63 | 64 | return node_hash 65 | 66 | 67 | class SparseMerkleProof: 68 | r""" 69 | Track the current value and merkle proof branch for a given key. 70 | This will enable the tracked proof data to stay up to date with changes 71 | that may be streamed to the end user over external protocols without having 72 | to interactively query the full SMT to obtain the most up-to-date branch. 73 | 74 | Attributes 75 | ---------- 76 | key: key we are tracking 77 | value: currently synchronized value 78 | branch: currently synchronized merkle proof branch 79 | root_hash: result of computing the merkle root for the tracked data 80 | 81 | .. doctest:: 82 | 83 | >>> # smt is located on another process or machine 84 | >>> smt = SparseMerkleTree(key_size=1) 85 | >>> our_key = b'\x03' 86 | >>> our_value = b'\x01' 87 | >>> smt.set(our_key, our_value) 88 | >>> # We need to track proof data for *some* reason 89 | >>> our_proof = SparseMerkleProof(our_key, our_value, smt.branch(our_key)) 90 | >>> their_key = b'\x05' 91 | >>> their_new_value = b'\x01' 92 | >>> their_node_updates = smt.set(their_key, their_new_value) 93 | >>> # tree updates can be communicated over any channel to proof obj 94 | >>> our_proof.update(their_key, their_new_value, their_node_updates) 95 | >>> # Note our branch data was never queried from smt to our proof obj 96 | >>> our_proof.branch == smt.branch(our_key) 97 | True 98 | >>> # Despite that, root hashes are kept consistent. Proof validates! 99 | >>> our_proof.root_hash == smt.root_hash 100 | True 101 | >>> # This works for multiple updates 102 | >>> our_proof.update(their_key, b'\x02', smt.set(their_key, b'\x02')) 103 | >>> our_proof.update(their_key, b'\x03', smt.set(their_key, b'\x03')) 104 | >>> our_proof.update(their_key, b'\x04', smt.set(their_key, b'\x04')) 105 | >>> our_proof.root_hash == smt.root_hash 106 | True 107 | >>> # This also works for updates to ourselves 108 | >>> our_proof.update(our_key, b'\x05', smt.set(our_key, b'\x05')) 109 | >>> our_proof.root_hash == smt.root_hash 110 | True 111 | >>> our_proof.value 112 | b'\x05' 113 | 114 | """ 115 | 116 | def __init__(self, key: bytes, value: bytes, branch: Sequence[Hash32]): 117 | validate_is_bytes(key) 118 | validate_is_bytes(value) 119 | validate_length(branch, len(key) * 8) 120 | 121 | self._key = key 122 | self._key_size = len(key) 123 | self._value = value 124 | self._branch = list(branch) # Avoid issues with mutable lists 125 | self._branch_size = len(branch) 126 | 127 | @property 128 | def key(self) -> bytes: 129 | return self._key 130 | 131 | @property 132 | def value(self) -> bytes: 133 | return self._value 134 | 135 | @property 136 | def branch(self) -> Tuple[Hash32]: 137 | return tuple(self._branch) 138 | 139 | @property 140 | def root_hash(self) -> Hash32: 141 | return calc_root(self.key, self.value, self.branch) 142 | 143 | def update(self, key: bytes, value: bytes, node_updates: Sequence[Hash32]): 144 | """ 145 | Merge an update for another key with the one we are tracking internally. 146 | 147 | :param key: keypath of the update we are processing 148 | :param value: value of the update we are processing 149 | :param node_updates: sequence of sibling nodes (in root->leaf order) 150 | must be at least as large as the first diverging 151 | key in the keypath 152 | 153 | """ 154 | validate_is_bytes(key) 155 | validate_length(key, self._key_size) 156 | 157 | # Path diff is the logical XOR of the updated key and this account 158 | path_diff = to_int(self.key) ^ to_int(key) 159 | 160 | # Same key (diff of 0), update the tracked value 161 | if path_diff == 0: 162 | self._value = value 163 | # No need to update branch 164 | else: 165 | # Find the first mismatched bit between keypaths. This is 166 | # where the branch point occurs, and we should update the 167 | # sibling node in the source branch at the branch point. 168 | # NOTE: Keys are in MSB->LSB (root->leaf) order. 169 | # Node lists are in root->leaf order. 170 | # Be sure to convert between them effectively. 171 | for bit in reversed(range(self._branch_size)): 172 | if path_diff & (1 << bit) > 0: 173 | branch_point = (self._branch_size - 1) - bit 174 | break 175 | 176 | # NOTE: node_updates only has to be as long as necessary 177 | # to obtain the update. This allows an optimization 178 | # of pruning updates to the maximum possible depth 179 | # that would be required to update, which may be 180 | # significantly smaller than the tree depth. 181 | if len(node_updates) <= branch_point: 182 | raise ValidationError("Updated node list is not deep enough") 183 | 184 | # Update sibling node in the branch where our key differs from the update 185 | self._branch[branch_point] = node_updates[branch_point] 186 | # No need to update value 187 | 188 | 189 | class SparseMerkleTree: 190 | def __init__(self, key_size: int = 32, default: bytes = BLANK_NODE): 191 | """ 192 | Maintain a a binary trie with a particular depth (defined by key size) 193 | All values are stored at that depth, and the tree has a default value that it is 194 | reset to when a key is cleared. If this default is anything other than a blank 195 | node, then all keys "exist" in the database, which mimics the behavior of 196 | Ethereum on-chain datastores. 197 | 198 | :param key_size: The size (in # of bytes) of the key. All keys must be this 199 | size. Note that the size should be between 1 and 32 bytes. 200 | For performance, it is not advisible to have a key larger than 201 | 32 bytes (and you should optimize to much less than that) 202 | but if the data structure you seek to use as a key is larger, 203 | the suggestion would be to hash that structure in a 204 | serialized format to obtain the key, or add a unique 205 | identifier to the structure. 206 | :param default: The default value used for the database. Initializes the root. 207 | """ 208 | # Ensure we can support the given depth 209 | if not 1 <= key_size <= 32: 210 | raise ValidationError("Keysize must be number of bytes in range [1, 32]") 211 | 212 | self._key_size = key_size # key's size (# of bytes) 213 | self.depth = key_size * 8 # depth is number of bits in the key 214 | 215 | self._default = default 216 | 217 | # Initialize an empty tree with one branch 218 | self.db = {} 219 | node = self._default # Default leaf node 220 | for _ in range(self.depth): 221 | node_hash = keccak(node) 222 | self.db[node_hash] = node 223 | node = node_hash + node_hash 224 | 225 | # Finally, write the root hash 226 | self.root_hash = keccak(node) 227 | self.db[self.root_hash] = node 228 | 229 | @classmethod 230 | def from_db( 231 | cls, 232 | db: Dict[bytes, bytes], 233 | root_hash: Hash32, 234 | key_size: int = 32, 235 | default: bytes = BLANK_NODE, 236 | ): 237 | smt = cls(key_size=key_size, default=default) 238 | 239 | # If db is provided, and is not consistent, 240 | # there may be a silent error. Can't solve that easily. 241 | smt.db = db 242 | 243 | # Set root_hash, so we know where to start 244 | validate_is_bytes(root_hash) 245 | validate_length(root_hash, 32) # Must be a bytes32 hash 246 | 247 | smt.root_hash = root_hash 248 | 249 | return smt 250 | 251 | def get(self, key: bytes) -> bytes: 252 | value, _ = self._get(key) 253 | 254 | # Ensure that it isn't blank! 255 | if value == BLANK_NODE: 256 | raise KeyError("Key does not exist") 257 | 258 | return value 259 | 260 | def branch(self, key: bytes) -> Tuple[Hash32]: 261 | value, branch = self._get(key) 262 | 263 | # Ensure that it isn't blank! 264 | if value == BLANK_NODE: 265 | raise KeyError("Key does not exist") 266 | 267 | return branch 268 | 269 | def _get(self, key: bytes) -> Tuple[bytes, Tuple[Hash32]]: 270 | """ 271 | Returns db value and branch in root->leaf order 272 | """ 273 | validate_is_bytes(key) 274 | validate_length(key, self._key_size) 275 | branch = [] 276 | 277 | target_bit = 1 << (self.depth - 1) 278 | path = to_int(key) 279 | node_hash = self.root_hash 280 | # Append the sibling node to the branch 281 | # Iterate on the parent 282 | for _ in range(self.depth): 283 | node = self.db[node_hash] 284 | left, right = node[:32], node[32:] 285 | if path & target_bit: 286 | branch.append(left) 287 | node_hash = right 288 | else: 289 | branch.append(right) 290 | node_hash = left 291 | target_bit >>= 1 292 | 293 | # Value is the last hash in the chain 294 | # NOTE: Didn't do exception here for testing purposes 295 | return self.db[node_hash], tuple(branch) 296 | 297 | def set(self, key: bytes, value: bytes) -> Tuple[Hash32]: 298 | """ 299 | Returns all updated hashes in root->leaf order 300 | """ 301 | validate_is_bytes(key) 302 | validate_length(key, self._key_size) 303 | validate_is_bytes(value) 304 | 305 | path = to_int(key) 306 | node = value 307 | _, branch = self._get(key) 308 | proof_update = [] # Keep track of proof updates 309 | 310 | target_bit = 1 311 | # branch is in root->leaf order, so flip 312 | for sibling_node in reversed(branch): 313 | # Set 314 | node_hash = keccak(node) 315 | proof_update.append(node_hash) 316 | self.db[node_hash] = node 317 | 318 | # Update 319 | if path & target_bit: 320 | node = sibling_node + node_hash 321 | else: 322 | node = node_hash + sibling_node 323 | 324 | target_bit <<= 1 325 | 326 | # Finally, update root hash 327 | self.root_hash = keccak(node) 328 | self.db[self.root_hash] = node 329 | 330 | # updates need to be in root->leaf order, so flip back 331 | return tuple(reversed(proof_update)) 332 | 333 | def exists(self, key: bytes) -> bool: 334 | validate_is_bytes(key) 335 | validate_length(key, self._key_size) 336 | 337 | try: 338 | self.get(key) 339 | return True 340 | except KeyError: 341 | return False 342 | 343 | def delete(self, key: bytes) -> Tuple[Hash32]: 344 | """ 345 | Equals to setting the value to None 346 | Returns all updated hashes in root->leaf order 347 | """ 348 | validate_is_bytes(key) 349 | validate_length(key, self._key_size) 350 | 351 | return self.set(key, self._default) 352 | 353 | # 354 | # Dictionary API 355 | # 356 | 357 | def __getitem__(self, key: bytes) -> bytes: 358 | return self.get(key) 359 | 360 | def __setitem__(self, key: bytes, value: bytes): 361 | self.set(key, value) 362 | 363 | def __delitem__(self, key: bytes): 364 | self.delete(key) 365 | 366 | def __contains__(self, key: bytes) -> bool: 367 | return self.exists(key) 368 | -------------------------------------------------------------------------------- /trie/tools/builder.py: -------------------------------------------------------------------------------- 1 | from trie import ( 2 | HexaryTrie, 3 | ) 4 | 5 | 6 | def trie_from_keys(keys, minimum_value_length=0, prune=False): 7 | """ 8 | Make a new HexaryTrie, insert all the given keys, with the value equal to the key. 9 | Return the raw database and the HexaryTrie. 10 | """ 11 | # Create trie 12 | node_db = {} 13 | trie = HexaryTrie(node_db, prune=prune) 14 | with trie.squash_changes() as trie_batch: 15 | for k in keys: 16 | # Flood 3's at the end of the value to make it longer. b'3' is 17 | # encoded to 0x33, so the bytes and HexBytes representation look 18 | # the same. Just a convenience. 19 | trie_batch[k] = k.ljust(minimum_value_length, b"3") 20 | 21 | return node_db, trie 22 | -------------------------------------------------------------------------------- /trie/tools/strategies.py: -------------------------------------------------------------------------------- 1 | from hypothesis import ( 2 | strategies as st, 3 | ) 4 | 5 | from trie import ( 6 | HexaryTrie, 7 | ) 8 | 9 | 10 | @st.composite 11 | def random_trie_strategy(draw): 12 | trie_items = draw( 13 | st.lists( 14 | st.tuples( 15 | # key 16 | st.binary(max_size=32), 17 | # value 18 | st.binary(min_size=1, max_size=64), 19 | ), 20 | unique=True, 21 | max_size=512, 22 | ) 23 | ) 24 | 25 | trie = HexaryTrie({}) 26 | for key, value in trie_items: 27 | trie[key] = value 28 | return trie, dict(trie_items) 29 | 30 | 31 | @st.composite 32 | def trie_keys_with_extensions(draw, allow_empty_trie=True): 33 | """ 34 | Build trie keys that tend to have lots of extension/branch/leaf nodes. 35 | Anecdotally, this was more likely to produce examples like the one 36 | in test_trie_walk_root_change_with_traverse() about TraversedPartialPath. 37 | """ 38 | # Simplest possible trie: an empty trie 39 | # Test it about once, on average, per run of 200 tests (the default example count) 40 | # Also, this will shrink down to the empty trie as you shrink these integers. 41 | if allow_empty_trie and draw(st.integers(min_value=0, max_value=200)) == 0: 42 | return () 43 | 44 | def build_up_from_children(children): 45 | # Branch out 46 | return st.tuples( 47 | st.binary(min_size=0, max_size=3), 48 | children, 49 | st.binary(min_size=0, max_size=3), 50 | children, 51 | ) 52 | 53 | # build tree 54 | tree = draw( 55 | st.recursive( 56 | # key suffix 57 | st.tuples( 58 | st.binary(min_size=0, max_size=3), 59 | ), 60 | # branches/extensions 61 | build_up_from_children, 62 | ) 63 | ) 64 | 65 | def unroll_keys(node): 66 | if len(node) == 1: 67 | # leaf 68 | yield node[0] 69 | elif len(node) == 4: 70 | # branch 71 | for subkey in unroll_keys(node[1]): 72 | yield node[0] + subkey 73 | for subkey in unroll_keys(node[3]): 74 | yield node[2] + subkey 75 | 76 | # Use a hashable type here, to make uniqueness checks faster/possible 77 | return tuple(set(unroll_keys(tree))) 78 | 79 | 80 | def trie_from_keys(keys, min_value_length=1): 81 | trie = HexaryTrie({}) 82 | contents = {} 83 | with trie.squash_changes() as batch: 84 | for key in keys: 85 | # flood 3's at the end of the value to make it longer. b'3' is 86 | # encoded to 0x33, so the bytes and HexBytes representation 87 | # look the same. Just a convenience. 88 | value = (b"v" + key).ljust(min_value_length, b"3") 89 | batch[key] = value 90 | contents[key] = value 91 | 92 | return trie, contents 93 | -------------------------------------------------------------------------------- /trie/typing.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from typing import ( 3 | Iterable, 4 | List, 5 | Literal, 6 | NamedTuple, 7 | Protocol, 8 | Sequence, 9 | Tuple, 10 | TypeVar, 11 | Union, 12 | ) 13 | 14 | from eth_utils import ( 15 | is_list_like, 16 | ) 17 | 18 | from trie.constants import ( 19 | NODE_TYPE_BLANK, 20 | NODE_TYPE_BRANCH, 21 | NODE_TYPE_EXTENSION, 22 | NODE_TYPE_LEAF, 23 | ) 24 | 25 | # The RLP-decoded node is either blank, or a list, full of bytes or recursive nodes 26 | # Recursive definitions don't seem supported at the moment, follow: 27 | # https://github.com/python/mypy/issues/731 28 | # Another option is to manually declare a few levels of the type. It should be possible 29 | # to determine the maximum number of embeds with single-nibble keys and single byte 30 | # values. 31 | RawHexaryNode = Union[ 32 | # Blank node 33 | Literal[b""], 34 | # Leaf or extension node are length 2 35 | # Branch node is length 17 36 | List[ 37 | Union[ 38 | # keys, hashes to next nodes, and values 39 | bytes, 40 | # embedded subnodes 41 | "RawHexaryNode", 42 | ] 43 | ], 44 | ] 45 | 46 | 47 | class Nibble(enum.IntEnum): 48 | Hex0 = 0 49 | Hex1 = 1 50 | Hex2 = 2 51 | Hex3 = 3 52 | Hex4 = 4 53 | Hex5 = 5 54 | Hex6 = 6 55 | Hex7 = 7 56 | Hex8 = 8 57 | Hex9 = 9 58 | HexA = 0xA 59 | HexB = 0xB 60 | HexC = 0xC 61 | HexD = 0xD 62 | HexE = 0xE 63 | HexF = 0xF 64 | 65 | def __repr__(self): 66 | return hex(self.value) 67 | 68 | 69 | # A user-input value, where each element will be validated as a Nibble instead of int 70 | NibblesInput = Sequence[int] 71 | 72 | 73 | class Nibbles(Tuple[Nibble, ...]): 74 | def __new__(cls, nibbles: NibblesInput) -> "Nibbles": 75 | if type(nibbles) is Nibbles: 76 | # instanceof thinks that a Tuple[Nibble, ...] *is* a Nibbles, so we use 77 | # a stricter type check here 78 | return nibbles # type: ignore # mypy doesn't recognize that this is now a Nibbles # noqa: E501 79 | elif not is_list_like(nibbles): 80 | raise TypeError(f"Must pass in a tuple of nibbles, but got {nibbles!r}") 81 | else: 82 | return tuple.__new__( 83 | cls, (Nibble(maybe_nibble) for maybe_nibble in nibbles) 84 | ) 85 | 86 | def __add__(self, other: Tuple[Nibble, ...]) -> "Nibbles": 87 | return Nibbles(super().__add__(other)) 88 | 89 | def _repr_pretty_(self, p, cycle: bool) -> None: 90 | # Weird, ipython seems to drop the trailing comma in the pretty repr 91 | # they do. Fixing... 92 | if cycle: 93 | p.text("(...)") 94 | else: 95 | p.text(super().__repr__()) 96 | 97 | 98 | class NodeType(enum.IntEnum): 99 | BLANK = NODE_TYPE_BLANK 100 | LEAF = NODE_TYPE_LEAF 101 | EXTENSION = NODE_TYPE_EXTENSION 102 | BRANCH = NODE_TYPE_BRANCH 103 | 104 | 105 | class HexaryTrieNode(NamedTuple): 106 | """ 107 | Public API for a node of a trie, it is pre-processed a bit for simplicity. 108 | """ 109 | 110 | sub_segments: Tuple[Nibbles, ...] 111 | """ 112 | Sub segments are the _complete_ list of possible subkeys. 113 | All sub segments *not* listed can be considered to not exist. 114 | Sub segments are sorted. 115 | 116 | Each sub segment does not include the trie node prefix. For example: 117 | - Branch nodes have length-1 tuples as sub_segments. 118 | - Leaf nodes have no sub_segments 119 | - Extension nodes have one sub_segment 120 | """ 121 | 122 | value: bytes 123 | """ 124 | This is the value associated with the key which navigates to this node in 125 | the trie. If empty, will be set to b''. 126 | """ 127 | 128 | suffix: Nibbles 129 | """ 130 | In a leaf node, there is a suffix of a key remaining before the value is reached. 131 | This is that series of nibbles. On a branch node with a value, 132 | the suffix will be (). 133 | """ 134 | 135 | raw: RawHexaryNode 136 | """ 137 | The node body, which is useful for calls to HexaryTrie.traverse_from(...), 138 | for faster access of sub-nodes. 139 | """ 140 | 141 | node_type: NodeType 142 | """ 143 | The node type (leaf, branch, extension, blank). Useful for understanding the 144 | structure of the trie, but should not be checked often in normal usage. 145 | """ 146 | 147 | 148 | T = TypeVar("T") 149 | 150 | 151 | class GenericSortedSet(Protocol[T]): 152 | """ 153 | A protocol definining the minimal subset of features used from 154 | sortedcontainers.SortedSet. Feel free to add more as needed. 155 | """ 156 | 157 | def __contains__(self, search_value: T) -> bool: 158 | ... 159 | 160 | def __getitem__(self, index: int) -> T: 161 | ... 162 | 163 | def __len__(self) -> int: 164 | ... 165 | 166 | def __iter__(self) -> "GenericSortedSet[T]": 167 | ... 168 | 169 | def __next__(self) -> T: 170 | ... 171 | 172 | def bisect(self, search_value: T) -> int: 173 | ... 174 | 175 | def copy(self) -> "GenericSortedSet[T]": 176 | ... 177 | 178 | def remove(self, to_remove: T) -> None: 179 | ... 180 | 181 | def update(self, new_values: Iterable[T]) -> None: 182 | ... 183 | -------------------------------------------------------------------------------- /trie/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/py-trie/d92445c201c1552906c4a1551c1ca703883aef2e/trie/utils/__init__.py -------------------------------------------------------------------------------- /trie/utils/binaries.py: -------------------------------------------------------------------------------- 1 | from eth_utils import ( 2 | apply_to_return_value, 3 | ) 4 | from eth_utils.toolz import ( 5 | partition_all, 6 | ) 7 | 8 | from trie.constants import ( 9 | EXP, 10 | PREFIX_00, 11 | PREFIX_100000, 12 | TWO_BITS, 13 | ) 14 | 15 | 16 | @apply_to_return_value(bytes) 17 | def decode_from_bin(input_bin): 18 | """ 19 | 0100000101010111010000110100100101001001 -> ASCII 20 | """ 21 | for chunk in partition_all(8, input_bin): 22 | yield sum(2**exp * bit for exp, bit in enumerate(reversed(chunk))) 23 | 24 | 25 | @apply_to_return_value(bytes) 26 | def encode_to_bin(value): 27 | """ 28 | ASCII -> 0100000101010111010000110100100101001001 29 | """ 30 | for char in value: 31 | for exp in EXP: 32 | if char & exp: 33 | yield True 34 | else: 35 | yield False 36 | 37 | 38 | def encode_from_bin_keypath(input_bin): 39 | """ 40 | Encodes a sequence of 0s and 1s into tightly packed bytes 41 | Used in encoding key path of a KV-NODE 42 | """ 43 | padded_bin = bytes((4 - len(input_bin)) % 4) + input_bin 44 | prefix = TWO_BITS[len(input_bin) % 4] 45 | if len(padded_bin) % 8 == 4: 46 | return decode_from_bin(PREFIX_00 + prefix + padded_bin) 47 | else: 48 | return decode_from_bin(PREFIX_100000 + prefix + padded_bin) 49 | 50 | 51 | def decode_to_bin_keypath(path): 52 | """ 53 | Decodes bytes into a sequence of 0s and 1s 54 | Used in decoding key path of a KV-NODE 55 | """ 56 | path = encode_to_bin(path) 57 | if path[0] == 1: 58 | path = path[4:] 59 | assert path[0:2] == PREFIX_00 60 | padded_len = TWO_BITS.index(path[2:4]) 61 | return path[4 + ((4 - padded_len) % 4) :] 62 | -------------------------------------------------------------------------------- /trie/utils/db.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | from eth_utils import ( 4 | to_dict, 5 | ) 6 | from eth_utils.toolz import ( 7 | merge, 8 | valfilter, 9 | ) 10 | 11 | DELETED = object() 12 | 13 | 14 | class ScratchDB: 15 | """ 16 | A wrapper of basic DB objects with uncommitted DB changes stored in local cache, 17 | which represents as a dictionary of database keys and values. 18 | None values cannot be represented, because they signify a deleted value. 19 | 20 | The method batch_commit() can be used as a context manager. 21 | Upon exiting the context, it writes all of the key value pairs from the cache into 22 | the underlying database. It optionally pushes deletes to the underlying databes. 23 | If any exception occurrs before committing phase, no changes are applied. 24 | """ 25 | 26 | def __init__(self, wrapped_db): 27 | self.wrapped_db = wrapped_db 28 | self.cache = {} 29 | 30 | # 31 | # Dictionary API 32 | # 33 | # if not key is found, return None 34 | def __getitem__(self, key): 35 | if key in self.cache: 36 | val = self.cache[key] 37 | if val is not DELETED: 38 | return val 39 | else: 40 | return self.wrapped_db[key] 41 | else: 42 | return self.wrapped_db[key] 43 | 44 | def __setitem__(self, key, value): 45 | self.cache[key] = value 46 | 47 | def __delitem__(self, key): 48 | self.cache[key] = DELETED 49 | 50 | def __contains__(self, key): 51 | if key in self.cache and self.cache[key] is not DELETED: 52 | return True 53 | else: 54 | return key in self.wrapped_db 55 | 56 | @to_dict 57 | def copy(self): 58 | combined = merge(self.wrapped_db, self.cache) 59 | return valfilter(lambda val: val is not DELETED, combined) 60 | 61 | @contextlib.contextmanager 62 | def batch_commit(self, *, do_deletes=False): 63 | """ 64 | Batch and commit and end of context 65 | """ 66 | try: 67 | yield 68 | except Exception as exc: 69 | raise exc 70 | else: 71 | for key, value in self.cache.items(): 72 | if value is not DELETED: 73 | self.wrapped_db[key] = value 74 | elif do_deletes: 75 | self.wrapped_db.pop(key, None) 76 | # if do_deletes is False, ignore deletes to underlying db 77 | finally: 78 | self.cache = {} 79 | -------------------------------------------------------------------------------- /trie/utils/nibbles.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from eth_utils import ( 4 | to_tuple, 5 | ) 6 | from eth_utils.toolz import ( 7 | partition, 8 | ) 9 | 10 | from trie.constants import ( 11 | HP_FLAG_0, 12 | HP_FLAG_2, 13 | NIBBLE_TERMINATOR, 14 | ) 15 | from trie.exceptions import ( 16 | InvalidNibbles, 17 | ) 18 | 19 | NIBBLES_LOOKUPS = {byte: (byte >> 4, byte & 15) for byte in range(256)} 20 | 21 | 22 | def _bytes_to_nibbles(value): 23 | """ 24 | Convert a byte string to nibbles 25 | """ 26 | for byte in value: 27 | yield from NIBBLES_LOOKUPS[byte] 28 | 29 | 30 | def bytes_to_nibbles(value): 31 | return tuple(_bytes_to_nibbles(value)) 32 | 33 | 34 | VALID_NIBBLES = set(range(16)) 35 | REVERSE_NIBBLES_LOOKUP = {value: key for key, value in NIBBLES_LOOKUPS.items()} 36 | 37 | 38 | def nibbles_to_bytes(nibbles): 39 | if any(nibble not in VALID_NIBBLES for nibble in nibbles): 40 | raise InvalidNibbles( 41 | "Nibbles contained invalid value. Must be constrained between [0, 15]" 42 | ) 43 | 44 | if len(nibbles) % 2: 45 | raise InvalidNibbles("Nibbles must be even in length") 46 | 47 | value = bytes(REVERSE_NIBBLES_LOOKUP[pair] for pair in partition(2, nibbles)) 48 | return value 49 | 50 | 51 | def is_nibbles_terminated(nibbles): 52 | return nibbles and nibbles[-1] == NIBBLE_TERMINATOR 53 | 54 | 55 | @to_tuple 56 | def add_nibbles_terminator(nibbles): 57 | if is_nibbles_terminated(nibbles): 58 | return nibbles 59 | return itertools.chain(nibbles, (NIBBLE_TERMINATOR,)) 60 | 61 | 62 | @to_tuple 63 | def remove_nibbles_terminator(nibbles): 64 | if is_nibbles_terminated(nibbles): 65 | return nibbles[:-1] 66 | return nibbles 67 | 68 | 69 | def encode_nibbles(nibbles): 70 | """ 71 | The Hex Prefix function 72 | """ 73 | if is_nibbles_terminated(nibbles): 74 | flag = HP_FLAG_2 75 | else: 76 | flag = HP_FLAG_0 77 | 78 | raw_nibbles = remove_nibbles_terminator(nibbles) 79 | 80 | is_odd = len(raw_nibbles) % 2 81 | 82 | if is_odd: 83 | flagged_nibbles = tuple( 84 | itertools.chain( 85 | (flag + 1,), 86 | raw_nibbles, 87 | ) 88 | ) 89 | else: 90 | flagged_nibbles = tuple( 91 | itertools.chain( 92 | (flag, 0), 93 | raw_nibbles, 94 | ) 95 | ) 96 | 97 | prefixed_value = nibbles_to_bytes(flagged_nibbles) 98 | 99 | return prefixed_value 100 | 101 | 102 | def decode_nibbles(value): 103 | """ 104 | The inverse of the Hex Prefix function 105 | """ 106 | nibbles_with_flag = bytes_to_nibbles(value) 107 | flag = nibbles_with_flag[0] 108 | 109 | needs_terminator = flag in {HP_FLAG_2, HP_FLAG_2 + 1} 110 | is_odd_length = flag in {HP_FLAG_0 + 1, HP_FLAG_2 + 1} 111 | 112 | if is_odd_length: 113 | raw_nibbles = nibbles_with_flag[1:] 114 | else: 115 | raw_nibbles = nibbles_with_flag[2:] 116 | 117 | if needs_terminator: 118 | nibbles = add_nibbles_terminator(raw_nibbles) 119 | else: 120 | nibbles = raw_nibbles 121 | 122 | return nibbles 123 | -------------------------------------------------------------------------------- /trie/utils/nodes.py: -------------------------------------------------------------------------------- 1 | import rlp 2 | 3 | from trie.constants import ( 4 | BLANK_NODE, 5 | BRANCH_TYPE, 6 | BRANCH_TYPE_PREFIX, 7 | KV_TYPE, 8 | KV_TYPE_PREFIX, 9 | LEAF_TYPE, 10 | LEAF_TYPE_PREFIX, 11 | NODE_TYPE_BLANK, 12 | NODE_TYPE_BRANCH, 13 | NODE_TYPE_EXTENSION, 14 | NODE_TYPE_LEAF, 15 | ) 16 | from trie.exceptions import ( 17 | InvalidNode, 18 | ValidationError, 19 | ) 20 | from trie.typing import ( 21 | HexaryTrieNode, 22 | Nibbles, 23 | NodeType, 24 | RawHexaryNode, 25 | ) 26 | from trie.utils.binaries import ( 27 | decode_to_bin_keypath, 28 | encode_from_bin_keypath, 29 | ) 30 | from trie.validation import ( 31 | validate_is_bytes, 32 | validate_length, 33 | ) 34 | 35 | from .nibbles import ( 36 | add_nibbles_terminator, 37 | decode_nibbles, 38 | encode_nibbles, 39 | is_nibbles_terminated, 40 | remove_nibbles_terminator, 41 | ) 42 | 43 | 44 | def get_node_type(node): 45 | if node == BLANK_NODE: 46 | return NODE_TYPE_BLANK 47 | elif len(node) == 2: 48 | key, _ = node 49 | nibbles = decode_nibbles(key) 50 | if is_nibbles_terminated(nibbles): 51 | return NODE_TYPE_LEAF 52 | else: 53 | return NODE_TYPE_EXTENSION 54 | elif len(node) == 17: 55 | return NODE_TYPE_BRANCH 56 | else: 57 | raise InvalidNode("Unable to determine node type") 58 | 59 | 60 | def is_blank_node(node): 61 | return node == BLANK_NODE 62 | 63 | 64 | def is_leaf_node(node): 65 | if len(node) != 2: 66 | return False 67 | key, _ = node 68 | nibbles = decode_nibbles(key) 69 | return is_nibbles_terminated(nibbles) 70 | 71 | 72 | def is_extension_node(node): 73 | if len(node) != 2: 74 | return False 75 | key, _ = node 76 | nibbles = decode_nibbles(key) 77 | return not is_nibbles_terminated(nibbles) 78 | 79 | 80 | def is_branch_node(node): 81 | return len(node) == 17 82 | 83 | 84 | def decode_node(encoded_node_or_hash): 85 | if encoded_node_or_hash == BLANK_NODE: 86 | return BLANK_NODE 87 | elif isinstance(encoded_node_or_hash, list): 88 | return encoded_node_or_hash 89 | else: 90 | return rlp.decode(encoded_node_or_hash) 91 | 92 | 93 | def extract_key(node): 94 | prefixed_key, _ = node 95 | key = remove_nibbles_terminator(decode_nibbles(prefixed_key)) 96 | return key 97 | 98 | 99 | def compute_leaf_key(nibbles): 100 | return encode_nibbles(add_nibbles_terminator(nibbles)) 101 | 102 | 103 | def compute_extension_key(nibbles): 104 | return encode_nibbles(nibbles) 105 | 106 | 107 | def get_common_prefix_length(left_key, right_key): 108 | for idx, (left_nibble, right_nibble) in enumerate(zip(left_key, right_key)): 109 | if left_nibble != right_nibble: 110 | return idx 111 | return min(len(left_key), len(right_key)) 112 | 113 | 114 | def consume_common_prefix(left_key, right_key): 115 | common_prefix_length = get_common_prefix_length(left_key, right_key) 116 | common_prefix = left_key[:common_prefix_length] 117 | left_remainder = left_key[common_prefix_length:] 118 | right_remainder = right_key[common_prefix_length:] 119 | return common_prefix, left_remainder, right_remainder 120 | 121 | 122 | def key_starts_with(full_key, partial_key): 123 | if len(full_key) < len(partial_key): 124 | return False 125 | else: 126 | return all(left == right for left, right in zip(full_key, partial_key)) 127 | 128 | 129 | # Binary Trie node utils 130 | def parse_node(node): 131 | """ 132 | Input: a serialized node 133 | """ 134 | if node is None or node == b"": 135 | raise InvalidNode("Blank node is not a valid node type in Binary Trie") 136 | elif node[0] == BRANCH_TYPE: 137 | if len(node) != 65: 138 | raise InvalidNode( 139 | "Invalid branch node, both child node should be 32 bytes long each" 140 | ) 141 | # Output: node type, left child, right child 142 | return BRANCH_TYPE, node[1:33], node[33:] 143 | elif node[0] == KV_TYPE: 144 | if len(node) <= 33: 145 | raise InvalidNode("Invalid kv node, short of key path or child node hash") 146 | # Output: node type, keypath: child 147 | return KV_TYPE, decode_to_bin_keypath(node[1:-32]), node[-32:] 148 | elif node[0] == LEAF_TYPE: 149 | if len(node) == 1: 150 | raise InvalidNode("Invalid leaf node, can not contain empty value") 151 | # Output: node type, None, value 152 | return LEAF_TYPE, None, node[1:] 153 | else: 154 | raise InvalidNode("Unable to parse node") 155 | 156 | 157 | def encode_kv_node(keypath, child_node_hash): 158 | """ 159 | Serializes a key/value node 160 | """ 161 | if keypath is None or keypath == b"": 162 | raise ValidationError("Key path can not be empty") 163 | validate_is_bytes(keypath) 164 | validate_is_bytes(child_node_hash) 165 | validate_length(child_node_hash, 32) 166 | return KV_TYPE_PREFIX + encode_from_bin_keypath(keypath) + child_node_hash 167 | 168 | 169 | def encode_branch_node(left_child_node_hash, right_child_node_hash): 170 | """ 171 | Serializes a branch node 172 | """ 173 | validate_is_bytes(left_child_node_hash) 174 | validate_length(left_child_node_hash, 32) 175 | validate_is_bytes(right_child_node_hash) 176 | validate_length(right_child_node_hash, 32) 177 | return BRANCH_TYPE_PREFIX + left_child_node_hash + right_child_node_hash 178 | 179 | 180 | def encode_leaf_node(value): 181 | """ 182 | Serializes a leaf node 183 | """ 184 | validate_is_bytes(value) 185 | if value is None or value == b"": 186 | raise ValidationError("Value of leaf node can not be empty") 187 | return LEAF_TYPE_PREFIX + value 188 | 189 | 190 | def annotate_node(node_body: RawHexaryNode) -> HexaryTrieNode: 191 | """ 192 | Normalize the raw node body to a HexaryTrieNode, for external consumption. 193 | """ 194 | node_type = get_node_type(node_body) 195 | if node_type == NODE_TYPE_LEAF: 196 | return HexaryTrieNode( 197 | sub_segments=(), 198 | value=bytes(node_body[-1]), 199 | suffix=Nibbles(extract_key(node_body)), 200 | raw=node_body, 201 | node_type=NodeType(node_type), 202 | ) 203 | elif node_type == NODE_TYPE_BRANCH: 204 | sub_segments = tuple( 205 | Nibbles((nibble,)) for nibble in range(16) if bool(node_body[nibble]) 206 | ) 207 | return HexaryTrieNode( 208 | sub_segments=sub_segments, 209 | value=bytes(node_body[-1]), 210 | suffix=Nibbles(()), 211 | raw=node_body, 212 | node_type=NodeType(node_type), 213 | ) 214 | elif node_type == NODE_TYPE_EXTENSION: 215 | key_extension = extract_key(node_body) 216 | return HexaryTrieNode( 217 | sub_segments=(Nibbles(key_extension),), 218 | value=b"", 219 | suffix=Nibbles(()), 220 | raw=node_body, 221 | node_type=NodeType(node_type), 222 | ) 223 | elif node_type == NODE_TYPE_BLANK: 224 | # empty trie 225 | return HexaryTrieNode( 226 | sub_segments=(), 227 | value=b"", 228 | suffix=Nibbles(()), 229 | raw=node_body, 230 | node_type=NodeType(node_type), 231 | ) 232 | else: 233 | raise NotImplementedError() 234 | -------------------------------------------------------------------------------- /trie/validation.py: -------------------------------------------------------------------------------- 1 | from trie.constants import ( 2 | BINARY_TRIE_NODE_TYPES, 3 | BLANK_HASH, 4 | BLANK_NODE, 5 | ) 6 | from trie.exceptions import ( 7 | ValidationError, 8 | ) 9 | 10 | 11 | def validate_is_bytes(value): 12 | if not isinstance(value, bytes): 13 | raise ValidationError(f"Value is not of type `bytes`: got '{type(value)}'") 14 | 15 | 16 | def validate_length(value, length): 17 | if len(value) != length: 18 | raise ValidationError(f"Value is of length {len(value)}. Must be {length}") 19 | 20 | 21 | def validate_is_node(node): 22 | if node == BLANK_NODE: 23 | return 24 | elif len(node) == 2: 25 | key, value = node 26 | validate_is_bytes(key) 27 | if isinstance(value, list): 28 | validate_is_node(value) 29 | else: 30 | validate_is_bytes(value) 31 | elif len(node) == 17: 32 | validate_is_bytes(node[16]) 33 | for sub_node in node[:16]: 34 | if sub_node == BLANK_NODE: 35 | continue 36 | elif isinstance(sub_node, list): 37 | validate_is_node(sub_node) 38 | else: 39 | validate_is_bytes(sub_node) 40 | validate_length(sub_node, 32) 41 | else: 42 | raise ValidationError(f"Invalid Node: {node}") 43 | 44 | 45 | def validate_is_bin_node(node): 46 | if node == BLANK_HASH or node[0] in BINARY_TRIE_NODE_TYPES: 47 | return 48 | else: 49 | raise ValidationError(f"Invalid Node: {node}") 50 | --------------------------------------------------------------------------------