├── tests ├── __init__.py ├── cmds │ ├── __init__.py │ ├── object_files │ │ ├── coinrecords │ │ │ ├── coinrecord.hex │ │ │ ├── coinrecord_metadata.json │ │ │ ├── coinrecord_invalid.json │ │ │ └── coinrecord.json │ │ ├── coins │ │ │ ├── coin.json │ │ │ ├── coin_invalid.json │ │ │ └── coin_metadata.json │ │ ├── spends │ │ │ ├── spend_invalid.json │ │ │ ├── spend.hex │ │ │ ├── spend_2.json │ │ │ ├── spend_metadata.json │ │ │ └── spend.json │ │ └── spendbundles │ │ │ ├── spendbundle.hex │ │ │ ├── spendbundle_metadata.json │ │ │ ├── spendbundle_invalid.json │ │ │ └── spendbundle.json │ ├── test_cdv.py │ ├── test_clsp.py │ └── test_inspect.py ├── test_skeleton.py └── build-init-files.py ├── cdv ├── clibs │ ├── __init__.py │ ├── sha256tree.clib │ ├── utility_macros.clib │ ├── condition_codes.clib │ ├── singleton_truths.clib │ └── curry_and_treehash.clib ├── cmds │ ├── __init__.py │ ├── util.py │ ├── cli.py │ ├── clsp.py │ ├── rpc.py │ └── chia_inspect.py ├── util │ ├── __init__.py │ ├── keys.py │ ├── sign_coin_spends.py │ └── load_clvm.py ├── examples │ ├── __init__.py │ ├── clsp │ │ ├── __init__.py │ │ ├── piggybank.clsp.hex │ │ └── piggybank.clsp │ ├── clvm │ │ └── __init__.py │ ├── drivers │ │ ├── __init__.py │ │ └── piggybank_drivers.py │ └── tests │ │ ├── __init__.py │ │ └── test_piggybank.py ├── __init__.py └── test │ ├── test_skeleton.py │ └── __init__.py ├── .github ├── actions │ └── install │ │ ├── install.sh │ │ ├── install.ps1 │ │ └── action.yml ├── workflows │ ├── codeql-analysis.yml │ ├── dependency-review.yml │ ├── test-blockchain-main.yml │ ├── publish-to-pypi.yml │ ├── super-linter.yml │ ├── precommit.yml │ └── run-test-suite.yml └── dependabot.yml ├── .repo-content-updater.yml ├── pytest.ini ├── .gitignore ├── pyproject.toml ├── activated.sh ├── activated.ps1 ├── mypy.ini ├── activated.py ├── .markdown-lint.yml ├── .pre-commit-config.yaml ├── setup.py ├── ruff.toml ├── README.md └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdv/clibs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdv/cmds/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdv/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cmds/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdv/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdv/examples/clsp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdv/examples/clvm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdv/examples/drivers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdv/examples/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/actions/install/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pip install --upgrade pip wheel 3 | pip install ."[dev]" 4 | -------------------------------------------------------------------------------- /.repo-content-updater.yml: -------------------------------------------------------------------------------- 1 | var_overrides: 2 | DEPENDABOT_ACTIONS_REVIEWERS: '["cmmarslender", "altendky"]' 3 | -------------------------------------------------------------------------------- /.github/actions/install/install.ps1: -------------------------------------------------------------------------------- 1 | python -m pip install --upgrade pip wheel 2 | python -m pip install .[dev] 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore:pkg_resources is deprecated as an API:DeprecationWarning 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /chia_dev_tools.egg-info 2 | /build 3 | /venv 4 | /dist 5 | /include 6 | /.eggs 7 | __pycache__/ 8 | main.sym 9 | .coverage 10 | .idea 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=80", 4 | "setuptools_scm[toml]>=8", 5 | "wheel" 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | -------------------------------------------------------------------------------- /tests/cmds/object_files/coinrecords/coinrecord.hex: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000001010000000036356868 -------------------------------------------------------------------------------- /tests/cmds/object_files/coins/coin.json: -------------------------------------------------------------------------------- 1 | {"amount": 0, 2 | "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", 3 | "puzzle_hash": "0x0000000000000000000000000000000000000000000000000000000000000000"} 4 | -------------------------------------------------------------------------------- /tests/cmds/object_files/coins/coin_invalid.json: -------------------------------------------------------------------------------- 1 | {"amount": -1, 2 | "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", 3 | "puzzle_hash": "00000000000000000000000000000000000000000000000000000000000000001"} 4 | -------------------------------------------------------------------------------- /cdv/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib.metadata 4 | 5 | try: 6 | __version__ = importlib.metadata.version("chia-dev-tools") 7 | except importlib.metadata.PackageNotFoundError: 8 | # package is not installed 9 | __version__ = "unknown" 10 | -------------------------------------------------------------------------------- /activated.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | 5 | SCRIPT_DIRECTORY=$( 6 | cd -- "$(dirname -- "$0")" 7 | pwd 8 | ) 9 | # shellcheck disable=SC1091 10 | if [ -d "${SCRIPT_DIRECTORY}/venv" ]; then # CI does not use venv 11 | source "${SCRIPT_DIRECTORY}/venv/bin/activate" 12 | fi 13 | "$@" 14 | -------------------------------------------------------------------------------- /cdv/clibs/sha256tree.clib: -------------------------------------------------------------------------------- 1 | ( 2 | ;; hash a tree 3 | ;; This is used to calculate a puzzle hash given a puzzle program. 4 | (defun sha256tree 5 | (TREE) 6 | (if (l TREE) 7 | (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) 8 | (sha256 1 TREE) 9 | ) 10 | ) 11 | ) -------------------------------------------------------------------------------- /tests/cmds/object_files/coins/coin_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", 3 | "bytes": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 4 | "type": "Coin" 5 | } 6 | -------------------------------------------------------------------------------- /tests/cmds/object_files/spends/spend_invalid.json: -------------------------------------------------------------------------------- 1 | {"coin": {"amount": -1, 2 | "parent_coin_info": "0x00000000000000000000000000000000000000000000000000000000000000001", 3 | "puzzle_hash": "z000000000000000000000000000000000000000000000000000000000000000"}, 4 | "puzzle_reveal": "0xff01018", 5 | "solution": "801"} 6 | -------------------------------------------------------------------------------- /tests/cmds/object_files/coinrecords/coinrecord_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", 3 | "bytes": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000001010000000036356868", 4 | "type": "CoinRecord" 5 | } 6 | -------------------------------------------------------------------------------- /tests/cmds/object_files/spendbundles/spendbundle.hex: -------------------------------------------------------------------------------- 1 | 000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -------------------------------------------------------------------------------- /tests/cmds/object_files/spends/spend.hex: -------------------------------------------------------------------------------- 1 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff2480ffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff248080 -------------------------------------------------------------------------------- /activated.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | $script_directory = Split-Path $MyInvocation.MyCommand.Path -Parent 4 | $command = $args[0] 5 | $parameters = [System.Collections.ArrayList]$args 6 | $parameters.RemoveAt(0) 7 | 8 | if (Test-Path -Path $script_directory/venv) { 9 | & $script_directory/venv/Scripts/Activate.ps1 10 | } 11 | & $command @parameters 12 | 13 | exit $LASTEXITCODE 14 | -------------------------------------------------------------------------------- /tests/cmds/object_files/coinrecords/coinrecord_invalid.json: -------------------------------------------------------------------------------- 1 | {"coin": {"amount": -1, 2 | "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", 3 | "puzzle_hash": "00000000000000000000000000000000000000000000000000000000000000001"}, 4 | "coinbase": "yes", 5 | "confirmed_block_index": -1, 6 | "spent": "no", 7 | "spent_block_index": -1, 8 | "timestamp": -1} 9 | -------------------------------------------------------------------------------- /tests/cmds/object_files/coinrecords/coinrecord.json: -------------------------------------------------------------------------------- 1 | { 2 | "coin":{ 3 | "parent_coin_info":"0x0000000000000000000000000000000000000000000000000000000000000000", 4 | "puzzle_hash":"0x0000000000000000000000000000000000000000000000000000000000000000", 5 | "amount":0 6 | }, 7 | "confirmed_block_index":1, 8 | "spent_block_index":1, 9 | "coinbase":true, 10 | "timestamp":909469800 11 | } 12 | -------------------------------------------------------------------------------- /tests/cmds/object_files/spends/spend_2.json: -------------------------------------------------------------------------------- 1 | {"coin": {"amount": 0, 2 | "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000001", 3 | "puzzle_hash": "0x0000000000000000000000000000000000000000000000000000000000000000"}, 4 | "puzzle_reveal": "0x01", 5 | "solution": "0xffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff248080"} 6 | -------------------------------------------------------------------------------- /cdv/examples/clsp/piggybank.clsp.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ffff03ffff15ff5fff2f80ffff01ff02ffff03ffff15ff5fff0580ffff01ff04ffff04ff0affff04ff0bffff04ff5fff80808080ffff04ffff04ff0affff04ff17ffff01ff80808080ffff04ffff04ff08ffff04ff2fff808080ffff04ffff04ff0cffff04ff17ff808080ffff04ffff04ff0effff04ff5fff808080ff808080808080ffff01ff04ffff04ff0affff04ff17ffff04ff5fff80808080ffff04ffff04ff08ffff04ff2fff808080ffff04ffff04ff0cffff04ff17ff808080ffff04ffff04ff0effff04ff5fff808080ff808080808080ff0180ffff01ff088080ff0180ffff04ffff01ffff4948ff333cff018080 2 | -------------------------------------------------------------------------------- /tests/cmds/object_files/spendbundles/spendbundle_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1df141df6d92e1d983a0e9b6a3b057aea2da927e781c47dfa41294f131aa969a", 3 | "bytes": "000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 4 | "type": "SpendBundle" 5 | } 6 | -------------------------------------------------------------------------------- /tests/cmds/object_files/spends/spend_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", 3 | "bytes": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff2480ffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff248080", 4 | "type": "CoinSpend" 5 | } 6 | -------------------------------------------------------------------------------- /tests/cmds/object_files/spends/spend.json: -------------------------------------------------------------------------------- 1 | {"coin": {"amount": 0, 2 | "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", 3 | "puzzle_hash": "0x0000000000000000000000000000000000000000000000000000000000000000"}, 4 | "puzzle_reveal": "0x01", 5 | "solution": "0xffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff2480ffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff248080"} 6 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = cdv,tests,*.py 3 | show_error_codes = True 4 | warn_unused_ignores = True 5 | disallow_subclassing_any = True 6 | disallow_untyped_decorators = True 7 | no_implicit_optional = True 8 | warn_return_any = True 9 | strict_equality = True 10 | # TODO: Jack, do more linting and uncomment below. 11 | #disallow_untyped_calls = True 12 | #disallow_untyped_defs = True 13 | #check_untyped_defs = True 14 | #disallow_incomplete_defs = True 15 | #disallow_any_generics = True 16 | #no_implicit_reexport = True 17 | 18 | 19 | [mypy - lib] 20 | ignore_errors = True 21 | -------------------------------------------------------------------------------- /cdv/clibs/utility_macros.clib: -------------------------------------------------------------------------------- 1 | ( 2 | (defmacro assert items 3 | (if (r items) 4 | (list if (f items) (c assert (r items)) (q . (x))) 5 | (f items) 6 | ) 7 | ) 8 | 9 | (defmacro or ARGS 10 | (if ARGS 11 | (qq (if (unquote (f ARGS)) 12 | 1 13 | (unquote (c or (r ARGS))) 14 | )) 15 | 0) 16 | ) 17 | 18 | (defmacro and ARGS 19 | (if ARGS 20 | (qq (if (unquote (f ARGS)) 21 | (unquote (c and (r ARGS))) 22 | () 23 | )) 24 | 1) 25 | ) 26 | ) -------------------------------------------------------------------------------- /cdv/test/test_skeleton.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from cdv.test import setup as setup_test 7 | 8 | 9 | class TestSomething: 10 | @pytest_asyncio.fixture(scope="function") 11 | async def setup(self): 12 | async with setup_test() as (network, alice, bob): 13 | await network.farm_block() 14 | yield network, alice, bob 15 | 16 | @pytest.mark.asyncio 17 | async def test_something(self, setup): 18 | _network, _alice, _bob = setup 19 | try: 20 | pass 21 | finally: 22 | pass 23 | -------------------------------------------------------------------------------- /tests/test_skeleton.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from cdv.test import setup as setup_test 7 | 8 | 9 | class TestSomething: 10 | @pytest_asyncio.fixture(scope="function") 11 | async def setup(self): 12 | async with setup_test() as (network, alice, bob): 13 | await network.farm_block() 14 | yield network, alice, bob 15 | 16 | @pytest.mark.asyncio 17 | async def test_something(self, setup): 18 | _network, _alice, _bob = setup 19 | try: 20 | pass 21 | finally: 22 | pass 23 | -------------------------------------------------------------------------------- /tests/cmds/object_files/spendbundles/spendbundle_invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "coin_spends": [ 3 | {"coin": {"amount": -1, 4 | "parent_coin_info": "0x00000000000000000000000000000000000000000000000000000000000000001", 5 | "puzzle_hash": "z0000000000000000000000000000000000000000000000000000000000000000"}, 6 | "puzzle_reveal": "0xff01018", 7 | "solution": "801"} 8 | ], 9 | "aggregated_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" 10 | } 11 | -------------------------------------------------------------------------------- /tests/cmds/object_files/spendbundles/spendbundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "spend_bundle": { 3 | "coin_spends": [ 4 | {"coin": {"amount": 0, 5 | "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", 6 | "puzzle_hash": "0x0000000000000000000000000000000000000000000000000000000000000000"}, 7 | "puzzle_reveal": "0x01", 8 | "solution": "0x80"} 9 | ], 10 | "aggregated_signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /activated.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import pathlib 7 | import subprocess 8 | import sys 9 | 10 | here = pathlib.Path(__file__).parent.absolute() 11 | 12 | 13 | def main(*args: str) -> int: 14 | if len(args) == 0: 15 | print("Parameters required") 16 | return 1 17 | 18 | if sys.platform == "win32": 19 | script = "activated.ps1" 20 | command = ["powershell", os.fspath(here.joinpath(script)), *args] 21 | else: 22 | script = "activated.sh" 23 | command = ["sh", os.fspath(here.joinpath(script)), *args] 24 | 25 | completed_process = subprocess.run(command, check=False) 26 | 27 | return completed_process.returncode 28 | 29 | 30 | sys.exit(main(*sys.argv[1:])) 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "main" ] 9 | schedule: 10 | - cron: '20 21 * * 1' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'python' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v6 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v4 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v4 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v4 41 | -------------------------------------------------------------------------------- /.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################### 3 | ########################### 4 | ## Markdown Linter rules ## 5 | ########################### 6 | ########################### 7 | 8 | # Linter rules doc: 9 | # - https://github.com/DavidAnson/markdownlint 10 | # 11 | # Note: 12 | # To comment out a single error: 13 | # 14 | # any violations you want 15 | # 16 | # 17 | 18 | ############### 19 | # Rules by id # 20 | ############### 21 | MD004: false # Unordered list style 22 | MD007: 23 | indent: 2 # Unordered list indentation 24 | MD013: 25 | line_length: 808 # Line length 26 | MD024: 27 | allow_different_nesting: true 28 | MD026: 29 | punctuation: ".,;:!。,;:" # List of not allowed 30 | MD029: false # Ordered list item prefix 31 | MD033: false # Allow inline HTML 32 | MD036: false # Emphasis used instead of a heading 33 | MD041: false # Allow file to start without h1 34 | 35 | ################# 36 | # Rules by tags # 37 | ################# 38 | blank_lines: false # Error on blank lines 39 | -------------------------------------------------------------------------------- /cdv/clibs/condition_codes.clib: -------------------------------------------------------------------------------- 1 | ; See chia/types/condition_opcodes.py 2 | 3 | ( 4 | (defconstant AGG_SIG_UNSAFE 49) 5 | (defconstant AGG_SIG_ME 50) 6 | 7 | ; the conditions below reserve coin amounts and have to be accounted for in output totals 8 | 9 | (defconstant CREATE_COIN 51) 10 | (defconstant RESERVE_FEE 52) 11 | 12 | ; the conditions below deal with announcements, for inter-coin communication 13 | 14 | ; coin announcements 15 | (defconstant CREATE_COIN_ANNOUNCEMENT 60) 16 | (defconstant ASSERT_COIN_ANNOUNCEMENT 61) 17 | 18 | ; puzzle announcements 19 | (defconstant CREATE_PUZZLE_ANNOUNCEMENT 62) 20 | (defconstant ASSERT_PUZZLE_ANNOUNCEMENT 63) 21 | 22 | ; the conditions below let coins inquire about themselves 23 | 24 | (defconstant ASSERT_MY_COIN_ID 70) 25 | (defconstant ASSERT_MY_PARENT_ID 71) 26 | (defconstant ASSERT_MY_PUZZLEHASH 72) 27 | (defconstant ASSERT_MY_AMOUNT 73) 28 | 29 | ; the conditions below ensure that we're "far enough" in the future 30 | 31 | ; wall-clock time 32 | (defconstant ASSERT_SECONDS_RELATIVE 80) 33 | (defconstant ASSERT_SECONDS_ABSOLUTE 81) 34 | 35 | ; block index 36 | (defconstant ASSERT_HEIGHT_RELATIVE 82) 37 | (defconstant ASSERT_HEIGHT_ABSOLUTE 83) 38 | ) 39 | -------------------------------------------------------------------------------- /.github/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: "Install chia-dev=tools" 2 | 3 | description: "Run the platform appropriate installer script." 4 | 5 | inputs: 6 | python-version: 7 | description: "Value to be set for INSTALL_PYTHON_VERSION." 8 | required: false 9 | default: "" 10 | development: 11 | description: "Install development dependencies." 12 | required: false 13 | default: "" 14 | automated: 15 | description: "Automated install, no questions." 16 | required: false 17 | default: "true" 18 | command-prefix: 19 | description: "Text to place before the command such as `arch -arm64` for non-native macOS runners." 20 | required: false 21 | default: "" 22 | 23 | runs: 24 | using: "composite" 25 | steps: 26 | - name: Run install script (macOS, Ubuntu) 27 | if: runner.os == 'macos' || runner.os == 'linux' 28 | shell: bash 29 | env: 30 | INSTALL_PYTHON_VERSION: ${{ inputs.python-version }} 31 | run: | 32 | ${{ inputs.command-prefix }} ./.github/actions/install/install.sh 33 | 34 | - name: Run install script (Windows) 35 | if: runner.os == 'windows' 36 | shell: pwsh 37 | env: 38 | INSTALL_PYTHON_VERSION: ${{ inputs.python-version }} 39 | run: | 40 | ${{ inputs.command-prefix }} ./.github/actions/install/install.ps1 41 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Managed by repo-content-updater 2 | # Dependency Review Action 3 | # 4 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 5 | # 6 | # Source repository: https://github.com/actions/dependency-review-action 7 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 8 | name: "🚨 Dependency Review" 9 | on: [pull_request] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | dependency-review: 16 | if: github.repository_owner == 'Chia-Network' 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: "Checkout Repository" 20 | uses: actions/checkout@v6 21 | 22 | - name: "Dependency Review" 23 | uses: actions/dependency-review-action@v4 24 | with: 25 | allow-dependencies-licenses: pkg:pypi/pyinstaller 26 | deny-licenses: AGPL-1.0-only, AGPL-1.0-or-later, AGPL-1.0-or-later, AGPL-3.0-or-later, GPL-1.0-only, GPL-1.0-or-later, GPL-2.0-only, GPL-2.0-or-later, GPL-3.0-only, GPL-3.0-or-later 27 | -------------------------------------------------------------------------------- /cdv/util/keys.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from chia.util.hash import std_hash 4 | from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey 5 | 6 | 7 | def secret_exponent_for_index(index: int) -> int: 8 | blob = index.to_bytes(32, "big") 9 | hashed_blob = AugSchemeMPL.key_gen(std_hash(b"foo" + blob)) 10 | r = int.from_bytes(hashed_blob, "big") 11 | return r 12 | 13 | 14 | def private_key_for_index(index: int) -> PrivateKey: 15 | r = secret_exponent_for_index(index) 16 | return PrivateKey.from_bytes(r.to_bytes(32, "big")) 17 | 18 | 19 | def public_key_for_index(index: int) -> G1Element: 20 | return private_key_for_index(index).get_g1() 21 | 22 | 23 | def sign_message_with_index(index: int, message: str) -> G2Element: 24 | sk = private_key_for_index(index) 25 | return AugSchemeMPL.sign(sk, bytes(message, "utf-8")) 26 | 27 | 28 | def sign_messages_with_indexes(sign_ops: list[dict[int, str]]) -> G2Element: 29 | signatures = [] 30 | for _ in sign_ops: 31 | for index, message in _.items(): 32 | sk = private_key_for_index(index) 33 | signatures.append(AugSchemeMPL.sign(sk, bytes(message, "utf-8"))) 34 | return AugSchemeMPL.aggregate(signatures) 35 | 36 | 37 | def aggregate_signatures(signatures: list[G2Element]) -> G2Element: 38 | return AugSchemeMPL.aggregate(signatures) 39 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: init_py_files 5 | name: __init__.py files 6 | entry: python3 tests/build-init-files.py -v --root . 7 | language: python 8 | pass_filenames: false 9 | additional_dependencies: [click~=8.1] 10 | - repo: local 11 | hooks: 12 | - id: ruff_format 13 | name: ruff format 14 | entry: ./activated.py ruff format 15 | language: system 16 | require_serial: true 17 | types_or: [python, pyi] 18 | - repo: local 19 | hooks: 20 | - id: ruff 21 | name: Ruff 22 | entry: ./activated.py ruff check --fix 23 | language: system 24 | types: [python] 25 | - repo: https://github.com/scop/pre-commit-shfmt 26 | rev: v3.10.0-2 27 | hooks: 28 | - id: shfmt 29 | args: ["--diff", "--write", "-i", "2"] 30 | - repo: https://github.com/pre-commit/pre-commit-hooks 31 | rev: v5.0.0 32 | hooks: 33 | - id: check-yaml 34 | - id: end-of-file-fixer 35 | exclude: ".*?(.hex|.clvm|.clib)" 36 | - id: trailing-whitespace 37 | - id: check-merge-conflict 38 | - id: check-ast 39 | - id: debug-statements 40 | - repo: local 41 | hooks: 42 | - id: mypy 43 | name: mypy 44 | entry: ./activated.py mypy 45 | language: system 46 | pass_filenames: false 47 | -------------------------------------------------------------------------------- /cdv/clibs/singleton_truths.clib: -------------------------------------------------------------------------------- 1 | ( 2 | (defun-inline truth_data_to_truth_struct (my_id full_puzhash innerpuzhash my_amount lineage_proof singleton_struct) (c (c my_id full_puzhash) (c (c innerpuzhash my_amount) (c lineage_proof singleton_struct)))) 3 | 4 | (defun-inline my_id_truth (Truths) (f (f Truths))) 5 | (defun-inline my_full_puzzle_hash_truth (Truths) (r (f Truths))) 6 | (defun-inline my_inner_puzzle_hash_truth (Truths) (f (f (r Truths)))) 7 | (defun-inline my_amount_truth (Truths) (r (f (r Truths)))) 8 | (defun-inline my_lineage_proof_truth (Truths) (f (r (r Truths)))) 9 | (defun-inline singleton_struct_truth (Truths) (r (r (r Truths)))) 10 | 11 | (defun-inline singleton_mod_hash_truth (Truths) (f (singleton_struct_truth Truths))) 12 | (defun-inline singleton_launcher_id_truth (Truths) (f (r (singleton_struct_truth Truths)))) 13 | (defun-inline singleton_launcher_puzzle_hash_truth (Truths) (f (r (r (singleton_struct_truth Truths))))) 14 | 15 | (defun-inline parent_info_for_lineage_proof (lineage_proof) (f lineage_proof)) 16 | (defun-inline puzzle_hash_for_lineage_proof (lineage_proof) (f (r lineage_proof))) 17 | (defun-inline amount_for_lineage_proof (lineage_proof) (f (r (r lineage_proof)))) 18 | (defun-inline is_not_eve_proof (lineage_proof) (r (r lineage_proof))) 19 | (defun-inline parent_info_for_eve_proof (lineage_proof) (f lineage_proof)) 20 | (defun-inline amount_for_eve_proof (lineage_proof) (f (r lineage_proof))) 21 | ) -------------------------------------------------------------------------------- /.github/workflows/test-blockchain-main.yml: -------------------------------------------------------------------------------- 1 | name: Test against chia-blockchain main 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | tags: 9 | - "**" 10 | pull_request: 11 | branches: 12 | - "**" 13 | 14 | concurrency: 15 | group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}--${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/heads/long_lived/')) && github.sha || '' }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build: 20 | name: All tests@main 21 | runs-on: ubuntu-latest 22 | timeout-minutes: 30 23 | strategy: 24 | fail-fast: false 25 | max-parallel: 4 26 | 27 | steps: 28 | - name: Checkout Code 29 | uses: actions/checkout@v6 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Setup Python environment 34 | uses: Chia-Network/actions/setup-python@main 35 | with: 36 | python-version: "3.12" 37 | 38 | - name: Test code with pytest 39 | run: | 40 | python3 -m venv venv 41 | . ./venv/bin/activate 42 | sed -i 's/chia-blockchain.*/chia-blockchain @ git+https:\/\/github.com\/Chia-Network\/chia-blockchain.git@main\",/g' setup.py 43 | pip install ."[dev]" 44 | ./venv/bin/chia init 45 | ./venv/bin/pytest tests/ cdv/examples/tests -s -v --durations 0 46 | -------------------------------------------------------------------------------- /cdv/examples/clsp/piggybank.clsp: -------------------------------------------------------------------------------- 1 | ; Note that this puzzle is "insecure" as it stands 2 | ; The person who owns the CASH_OUT_PUZHASH can take out a flash loan to: 3 | ; - fill the piggybank 4 | ; - receive their funds 5 | ; - return the flash loan 6 | ; 7 | ; This effectively lets someone access the money whenever they want if they have access to a flash loan 8 | (mod ( 9 | TARGET_AMOUNT 10 | CASH_OUT_PUZHASH 11 | my_puzhash 12 | my_amount 13 | new_amount 14 | ) 15 | 16 | (include condition_codes.clib) 17 | 18 | (defun-inline reached_goal (CASH_OUT_PUZHASH new_amount my_puzhash my_amount) 19 | (list 20 | (list CREATE_COIN CASH_OUT_PUZHASH new_amount) 21 | (list CREATE_COIN my_puzhash 0) 22 | (list ASSERT_MY_AMOUNT my_amount) 23 | (list ASSERT_MY_PUZZLEHASH my_puzhash) 24 | (list CREATE_COIN_ANNOUNCEMENT new_amount) 25 | ) 26 | ) 27 | 28 | (defun-inline recreate_self (new_amount my_puzhash my_amount) 29 | (list 30 | (list CREATE_COIN my_puzhash new_amount) 31 | (list ASSERT_MY_AMOUNT my_amount) 32 | (list ASSERT_MY_PUZZLEHASH my_puzhash) 33 | (list CREATE_COIN_ANNOUNCEMENT new_amount) 34 | ) 35 | ) 36 | 37 | ; main 38 | (if (> new_amount my_amount) 39 | (if (> new_amount TARGET_AMOUNT) 40 | (reached_goal CASH_OUT_PUZHASH new_amount my_puzhash my_amount) 41 | (recreate_self new_amount my_puzhash my_amount) 42 | ) 43 | (x) 44 | ) 45 | ) 46 | -------------------------------------------------------------------------------- /cdv/examples/drivers/piggybank_drivers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from chia.types.blockchain_format.coin import Coin 6 | from chia.types.blockchain_format.program import Program 7 | from chia.types.condition_opcodes import ConditionOpcode 8 | from chia.util.hash import std_hash 9 | from chia_rs.sized_bytes import bytes32 10 | from chia_rs.sized_ints import uint64 11 | from clvm.casts import int_to_bytes 12 | 13 | import cdv.clibs as std_lib 14 | from cdv.util.load_clvm import load_clvm 15 | 16 | clibs_path: Path = Path(std_lib.__file__).parent 17 | PIGGYBANK_MOD: Program = load_clvm("piggybank.clsp", "cdv.examples.clsp", search_paths=[clibs_path]) 18 | 19 | 20 | # Create a piggybank 21 | def create_piggybank_puzzle(amount: uint64, cash_out_puzhash: bytes32) -> Program: 22 | return PIGGYBANK_MOD.curry(amount, cash_out_puzhash) 23 | 24 | 25 | # Generate a solution to contribute to a piggybank 26 | def solution_for_piggybank(pb_coin: Coin, contrib_amount: uint64) -> Program: 27 | result: Program = Program.to([pb_coin.puzzle_hash, pb_coin.amount, (pb_coin.amount + contrib_amount)]) # mypy sucks 28 | return result 29 | 30 | 31 | # Return the condition to assert the announcement 32 | def piggybank_announcement_assertion(pb_coin: Coin, contrib_amount: uint64) -> list: 33 | return [ 34 | ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, 35 | std_hash(pb_coin.name() + int_to_bytes(pb_coin.amount + contrib_amount)), 36 | ] 37 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python distributions to PyPI and TestPyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | id-token: write 10 | 11 | jobs: 12 | build-n-publish: 13 | name: Build and publish Python distributions to PyPI and TestPyPI 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - uses: actions/checkout@v6 17 | with: 18 | fetch-depth: 0 19 | 20 | - run: | 21 | git fetch origin +refs/tags/*:refs/tags/* 22 | 23 | - name: Set Job Env 24 | uses: Chia-Network/actions/setjobenv@main 25 | env: 26 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Set up Python 3.12 29 | uses: Chia-Network/actions/setup-python@main 30 | with: 31 | python-version: "3.12" 32 | 33 | - name: Install build tools (pep517 compatible) 34 | run: >- 35 | python -m 36 | pip install ."[dev]" 37 | build 38 | --user 39 | 40 | - name: Build a binary wheel and a source tarball 41 | run: >- 42 | python -m 43 | build 44 | --sdist 45 | --outdir dist/ 46 | . 47 | - name: Publish distribution to Test PyPI 48 | if: env.PRE_RELEASE == 'true' 49 | uses: pypa/gh-action-pypi-publish@release/v1 50 | with: 51 | repository_url: https://test.pypi.org/legacy/ 52 | packages-dir: dist/ 53 | skip-existing: true 54 | 55 | - name: Publish distribution to PyPI 56 | if: env.FULL_RELEASE == 'true' 57 | uses: pypa/gh-action-pypi-publish@release/v1 58 | with: 59 | packages-dir: dist/ 60 | skip-existing: true 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import annotations 4 | 5 | from setuptools import find_packages, setup 6 | 7 | with open("README.md") as fh: 8 | long_description = fh.read() 9 | 10 | dependencies = [ 11 | "packaging", 12 | "pytest", 13 | "pytest-asyncio", 14 | "pytimeparse", 15 | "anyio", 16 | "chia-blockchain==2.5.7", 17 | ] 18 | 19 | dev_dependencies = [ 20 | "anyio", 21 | "mypy", 22 | "ruff==0.14.6", 23 | "types-aiofiles", 24 | "types-click", 25 | "types-cryptography", 26 | "types-setuptools", 27 | "types-pyyaml", 28 | "types-setuptools", 29 | "pre-commit", 30 | ] 31 | 32 | setup( 33 | name="chia_dev_tools", 34 | packages=find_packages(exclude=("tests",)), 35 | author="Quexington", 36 | entry_points={ 37 | "console_scripts": ["cdv = cdv.cmds.cli:main"], 38 | }, 39 | package_data={ 40 | "": ["*.clvm", "*.clvm.hex", "*.clib", "*.clsp", "*.clsp.hex"], 41 | }, 42 | author_email="m.hauff@chia.net", 43 | setup_requires=["setuptools_scm"], 44 | install_requires=dependencies, 45 | url="https://github.com/Chia-Network", 46 | license="https://opensource.org/licenses/Apache-2.0", 47 | description="Chia development commands", 48 | long_description=long_description, 49 | long_description_content_type="text/markdown", 50 | classifiers=[ 51 | "Programming Language :: Python :: 3", 52 | "Programming Language :: Python :: 3.10", 53 | "Programming Language :: Python :: 3.11", 54 | "License :: OSI Approved :: Apache Software License", 55 | "Topic :: Security :: Cryptography", 56 | ], 57 | extras_require=dict( 58 | dev=dev_dependencies, 59 | ), 60 | project_urls={ 61 | "Bug Reports": "https://github.com/Chia-Network/chia-dev-tools", 62 | "Source": "https://github.com/Chia-Network/chia-dev-tools", 63 | }, 64 | ) 65 | -------------------------------------------------------------------------------- /cdv/cmds/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from collections.abc import Iterable 5 | 6 | from chia.types.blockchain_format.program import Program 7 | from clvm_tools.binutils import assemble 8 | from clvm_tools.clvmc import compile_clvm_text 9 | 10 | 11 | # This is do trick inspect commands into thinking they're commands 12 | def fake_context() -> dict: 13 | ctx = {"obj": {"json": True}} 14 | return ctx 15 | 16 | 17 | # The clvm loaders in this library automatically search for includable files in the directory './include' 18 | def append_include(search_paths: Iterable[str]) -> list[str]: 19 | if search_paths: 20 | search_list = list(search_paths) 21 | search_list.append("./include") 22 | return search_list 23 | else: 24 | return ["./include"] 25 | 26 | 27 | # This is used in many places to go from CLI string -> Program object 28 | def parse_program(program: str | Program, include: Iterable = []) -> Program: 29 | if isinstance(program, Program): 30 | return program 31 | else: 32 | if "(" in program: # If it's raw clvm 33 | prog: Program = Program.to(assemble(program)) 34 | elif "." not in program: # If it's a byte string 35 | prog = Program.fromhex(program) 36 | else: # If it's a file 37 | with open(program) as file: 38 | filestring: str = file.read() 39 | if "(" in filestring: # If it's not compiled 40 | # TODO: This should probably be more robust 41 | if re.compile(r"\(mod\s").search(filestring): # If it's Chialisp 42 | prog = Program.to(compile_clvm_text(filestring, append_include(include))) 43 | else: # If it's CLVM 44 | prog = Program.to(assemble(filestring)) 45 | else: # If it's serialized CLVM 46 | prog = Program.fromhex(filestring) 47 | return prog 48 | -------------------------------------------------------------------------------- /.github/workflows/super-linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################### 3 | ########################### 4 | ## Linter GitHub Actions ## 5 | ########################### 6 | ########################### 7 | name: GithHub Super Linter 8 | 9 | # 10 | # Documentation: 11 | # https://github.com/github/super-linter 12 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 13 | # 14 | 15 | on: 16 | push: 17 | branches: 18 | - main 19 | tags: 20 | - '**' 21 | pull_request: 22 | branches: 23 | - '**' 24 | 25 | ############### 26 | # Set the Job # 27 | ############### 28 | jobs: 29 | build: 30 | # Name the Job 31 | name: Lint Code Base 32 | # Set the agent to run on 33 | runs-on: ubuntu-latest 34 | timeout-minutes: 60 35 | 36 | ################## 37 | # Load all steps # 38 | ################## 39 | steps: 40 | ########################## 41 | # Checkout the code base # 42 | ########################## 43 | - name: Checkout Code 44 | uses: actions/checkout@v6 45 | 46 | ################################ 47 | # Run Linter against code base # 48 | ################################ 49 | - name: Lint Code Base 50 | uses: github/super-linter@v5 51 | env: 52 | VALIDATE_ALL_CODEBASE: true 53 | DEFAULT_BRANCH: main 54 | LINTER_RULES_PATH: . 55 | VALIDATE_BASH: true 56 | VALIDATE_CSS: true 57 | VALIDATE_DOCKER: true 58 | VALIDATE_GO: true 59 | VALIDATE_HTML: true 60 | VALIDATE_JAVASCRIPT_ES: true 61 | VALIDATE_JSON: true 62 | VALIDATE_MD: true 63 | VALIDATE_POWERSHELL: true 64 | VALIDATE_PYTHON: false 65 | VALIDATE_TYPESCRIPT_ES: true 66 | VALIDATE_YAML: true 67 | DISABLE_ERRORS: false 68 | PYTHONPATH: ${{ github.workspace }}:$PYTHONPATH 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | FILTER_REGEX_EXCLUDE: .*github/ISSUE_TEMPLATE/config.yml 71 | # ACTIONS_RUNNER_DEBUG: true 72 | 73 | ... 74 | -------------------------------------------------------------------------------- /.github/workflows/precommit.yml: -------------------------------------------------------------------------------- 1 | name: 🚨 pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | # SHA is added to the end if on `main` to let all main workflows run 11 | group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/heads/long_lived/')) && github.sha || '' }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | pre-commit: 16 | name: ${{ matrix.os.name }} ${{ matrix.arch.name }} 17 | runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: 22 | - name: Linux 23 | matrix: linux 24 | runs-on: 25 | intel: ubuntu-latest 26 | arm: [linux, arm64] 27 | - name: macOS 28 | matrix: macos 29 | runs-on: 30 | intel: macos-15-intel 31 | arm: macos-13-arm64 32 | - name: Windows 33 | matrix: windows 34 | runs-on: 35 | intel: windows-latest 36 | arch: 37 | - name: ARM64 38 | matrix: arm 39 | - name: Intel 40 | matrix: intel 41 | python: 42 | - major_dot_minor: "3.12" 43 | exclude: 44 | - os: 45 | matrix: windows 46 | arch: 47 | matrix: arm 48 | 49 | steps: 50 | - name: Clean workspace 51 | uses: Chia-Network/actions/clean-workspace@main 52 | 53 | - name: Add safe git directory 54 | uses: Chia-Network/actions/git-mark-workspace-safe@main 55 | 56 | - name: disable git autocrlf 57 | run: | 58 | git config --global core.autocrlf false 59 | 60 | - uses: actions/checkout@v6 61 | with: 62 | fetch-depth: 0 63 | 64 | - uses: Chia-Network/actions/setup-python@main 65 | with: 66 | python-version: ${{ matrix.python.major_dot_minor }} 67 | 68 | - uses: ./.github/actions/install 69 | with: 70 | development: true 71 | 72 | - uses: pre-commit/action@v3.0.1 73 | with: 74 | extra_args: --verbose --all-files 75 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is managed by the repo-content-updater project. Manual changes here will result in a PR to bring back 2 | # inline with the upstream template, unless you remove the dependabot managed file property from the repo 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "gomod" 7 | directory: / 8 | schedule: 9 | interval: "weekly" 10 | day: "tuesday" 11 | cooldown: 12 | default-days: 7 13 | open-pull-requests-limit: 10 14 | rebase-strategy: auto 15 | labels: 16 | - dependencies 17 | - go 18 | - "Changed" 19 | reviewers: ["cmmarslender", "Starttoaster"] 20 | groups: 21 | global: 22 | patterns: 23 | - "*" 24 | 25 | - package-ecosystem: "pip" 26 | directory: / 27 | schedule: 28 | interval: "weekly" 29 | day: "tuesday" 30 | cooldown: 31 | default-days: 7 32 | open-pull-requests-limit: 10 33 | rebase-strategy: auto 34 | labels: 35 | - dependencies 36 | - python 37 | - "Changed" 38 | reviewers: ["emlowe", "altendky"] 39 | 40 | - package-ecosystem: "github-actions" 41 | directories: ["/", ".github/actions/*"] 42 | schedule: 43 | interval: "weekly" 44 | day: "tuesday" 45 | cooldown: 46 | default-days: 7 47 | open-pull-requests-limit: 10 48 | rebase-strategy: auto 49 | labels: 50 | - dependencies 51 | - github_actions 52 | - "Changed" 53 | reviewers: ["cmmarslender", "altendky"] 54 | 55 | - package-ecosystem: "npm" 56 | directory: / 57 | schedule: 58 | interval: "weekly" 59 | day: "tuesday" 60 | cooldown: 61 | default-days: 7 62 | open-pull-requests-limit: 10 63 | rebase-strategy: auto 64 | labels: 65 | - dependencies 66 | - javascript 67 | - "Changed" 68 | reviewers: ["cmmarslender", "ChiaMineJP"] 69 | 70 | - package-ecosystem: cargo 71 | directory: / 72 | schedule: 73 | interval: "weekly" 74 | day: "tuesday" 75 | cooldown: 76 | default-days: 7 77 | open-pull-requests-limit: 10 78 | rebase-strategy: auto 79 | labels: 80 | - dependencies 81 | - rust 82 | - "Changed" 83 | 84 | - package-ecosystem: swift 85 | directory: / 86 | schedule: 87 | interval: "weekly" 88 | day: "tuesday" 89 | cooldown: 90 | default-days: 7 91 | open-pull-requests-limit: 10 92 | rebase-strategy: auto 93 | -------------------------------------------------------------------------------- /tests/build-init-files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Create missing `__init__.py` files in the source code folders (in "cdv/" and "tests/"). 4 | # 5 | # They are required by the python interpreter to properly identify modules/packages so that tools like `mypy` or an IDE 6 | # can work with their full capabilities. 7 | # 8 | # See https://docs.python.org/3/tutorial/modules.html#packages. 9 | # 10 | # Note: This script is run in a `pre-commit` hook (which runs on CI) to make sure we don't miss out any folder. 11 | 12 | from __future__ import annotations 13 | 14 | import logging 15 | import pathlib 16 | import sys 17 | 18 | import click 19 | 20 | log_levels = { 21 | 0: logging.ERROR, 22 | 1: logging.WARNING, 23 | 2: logging.INFO, 24 | } 25 | 26 | 27 | ignores = {"__pycache__", ".pytest_cache"} 28 | 29 | 30 | @click.command() 31 | @click.option( 32 | "-r", "--root", "root_str", type=click.Path(dir_okay=True, file_okay=False, resolve_path=True), default="." 33 | ) 34 | @click.option("-v", "--verbose", count=True, help=f"Increase verbosity up to {len(log_levels) - 1} times") 35 | def command(verbose, root_str): 36 | logger = logging.getLogger() 37 | log_level = log_levels.get(verbose, min(log_levels.values())) 38 | logger.setLevel(log_level) 39 | stream_handler = logging.StreamHandler() 40 | logger.addHandler(stream_handler) 41 | 42 | tree_roots = [] # Disabled for now, ask Jack. 43 | failed = False 44 | root = pathlib.Path(root_str).resolve() 45 | directories = sorted( 46 | path 47 | for tree_root in tree_roots 48 | for path in root.joinpath(tree_root).rglob("**/") 49 | if all(part not in ignores for part in path.parts) 50 | ) 51 | 52 | for path in directories: 53 | init_path = path.joinpath("__init__.py") 54 | # This has plenty of race hazards. If it messes up, 55 | # it will likely get caught the next time. 56 | if init_path.is_file() and not init_path.is_symlink(): 57 | logger.info(f"Found : {init_path}") 58 | continue 59 | elif not init_path.exists(): 60 | failed = True 61 | init_path.touch() 62 | logger.warning(f"Created : {init_path}") 63 | else: 64 | failed = True 65 | logger.error(f"Fail : present but not a regular file: {init_path}", file=sys.stderr) 66 | 67 | if failed: 68 | raise click.ClickException("At least one __init__.py created or not a regular file") 69 | 70 | 71 | command() # pylint: disable=no-value-for-parameter 72 | -------------------------------------------------------------------------------- /tests/cmds/test_cdv.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from click.testing import CliRunner, Result 6 | 7 | from cdv.cmds.cli import cli 8 | 9 | 10 | class TestCdvCommands: 11 | def test_encode_decode(self): 12 | runner = CliRunner() 13 | puzhash: str = "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" 14 | 15 | # Without prefix 16 | result: Result = runner.invoke(cli, ["encode", puzhash]) 17 | address: str = "xch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqxsnauc" 18 | assert result.exit_code == 0 19 | assert address in result.output 20 | result = runner.invoke(cli, ["decode", address]) 21 | assert result.exit_code == 0 22 | assert puzhash in result.output 23 | 24 | # With prefix 25 | result = runner.invoke(cli, ["encode", puzhash, "--prefix", "txch"]) 26 | test_address: str = "txch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqth5tat" 27 | assert result.exit_code == 0 28 | assert test_address in result.output 29 | result = runner.invoke(cli, ["decode", test_address]) 30 | assert result.exit_code == 0 31 | assert puzhash in result.output 32 | 33 | def test_hash(self): 34 | runner = CliRunner() 35 | str_msg: str = "chia" 36 | b_msg: str = "0xcafef00d" 37 | 38 | result: Result = runner.invoke(cli, ["hash", str_msg]) 39 | assert result.exit_code == 0 40 | assert "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" in result.output 41 | result = runner.invoke(cli, ["hash", b_msg]) 42 | assert result.exit_code == 0 43 | assert "8f6e594e007ca1a1676ef64469c58f7ece8cddc9deae0faf66fbce2466519ebd" in result.output 44 | 45 | def test_test(self): 46 | runner = CliRunner() 47 | with runner.isolated_filesystem(): 48 | result: Result = runner.invoke(cli, ["test", "--init"]) 49 | assert result.exit_code == 0 50 | assert Path("./tests").exists() and Path("./tests/test_skeleton.py").exists() 51 | 52 | # 2023-10-17 Commenting subsequent tests because pytest throws an import file mismatch error 53 | # It seems that the test_skeleton module is being imported from the test enviroment but is 54 | # also present in the tests directory, causing the mismatch. 55 | # This repo has a tool (build-init-files.py) for creating missing __init__ files which may 56 | # have been intended as a solution to this problem, but as of now it doesn't work. 57 | 58 | # result = runner.invoke(cli, ["test", "--discover"]) 59 | # assert result.exit_code == 0 60 | # assert "TestSomething" in result.output 61 | 62 | # result = runner.invoke(cli, ["test"]) 63 | # assert result.exit_code == 0 64 | # assert "test_skeleton.py ." in result.output 65 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 120 2 | 3 | [lint] 4 | preview = true 5 | select = [ 6 | "PL", # Pylint 7 | "I", # Isort 8 | "FA", # Flake8: future-annotations 9 | "UP", # Pyupgrade 10 | "RUF", # Ruff specific 11 | "F", # Flake8 core 12 | "ASYNC", # Flake8 async 13 | "ISC", # flake8-implicit-str-concat 14 | "TID", # flake8-tidy-imports 15 | "E", 16 | "W", 17 | ] 18 | explicit-preview-rules = false 19 | ignore = [ 20 | # Pylint convention 21 | "PLC0415", # import-outside-top-level 22 | "PLC1901", # compare-to-empty-string 23 | # Should probably fix these 24 | "PLC2801", # unnecessary-dunder-call 25 | "PLC2701", # import-private-name 26 | 27 | # Pylint refactor 28 | "PLR0915", # too-many-statements 29 | "PLR0914", # too-many-locals 30 | "PLR0913", # too-many-arguments 31 | "PLR0912", # too-many-branches 32 | "PLR1702", # too-many-nested-blocks 33 | "PLR0904", # too-many-public-methods 34 | "PLR0917", # too-many-positional-arguments 35 | "PLR0916", # too-many-boolean-expressions 36 | "PLR0911", # too-many-return-statements 37 | # Should probably fix these 38 | "PLR6301", # no-self-use 39 | "PLR2004", # magic-value-comparison 40 | "PLR1704", # redefined-argument-from-local 41 | "PLR5501", # collapsible-else-if 42 | 43 | # Pylint warning 44 | "PLW1641", # eq-without-hash 45 | # Should probably fix these 46 | "PLW2901", # redefined-loop-name 47 | "PLW1514", # unspecified-encoding 48 | "PLW0603", # global-statement 49 | 50 | # Flake8 async 51 | # Should probably fix these 52 | "ASYNC230", # blocking-open-call-in-async-function 53 | # Should probably fix these after dealing with shielding for anyio 54 | "ASYNC109", # async-function-with-timeout 55 | 56 | # flake8-implicit-str-concat 57 | "ISC003", # explicit-string-concatenation 58 | 59 | # Ruff Specific 60 | 61 | # This code is problematic because using instantiated types as defaults is so common across the codebase. 62 | # Its purpose is to prevent accidenatally assigning mutable defaults. However, this is a bit overkill for that. 63 | # That being said, it would be nice to add some way to actually guard against that specific mutable default behavior. 64 | "RUF009", # function-call-in-dataclass-default-argument 65 | # Should probably fix this 66 | "RUF029", 67 | ] 68 | 69 | 70 | [lint.flake8-implicit-str-concat] 71 | # Found 3279 errors. 72 | # allow-multiline = false 73 | 74 | [lint.flake8-tidy-imports] 75 | ban-relative-imports = "all" 76 | 77 | [lint.flake8-tidy-imports.banned-api] 78 | # for use with another pr 79 | # "asyncio.create_task".msg = "Use `from chia.util.pit import pit` and `pit.create_task()`" 80 | 81 | [lint.isort] 82 | required-imports = ["from __future__ import annotations"] 83 | 84 | [lint.pylint] 85 | max-args = 5 86 | max-locals = 15 87 | max-returns = 6 88 | max-branches = 12 89 | max-statements = 50 90 | max-nested-blocks = 5 91 | max-public-methods = 20 92 | max-bool-expr = 5 93 | 94 | [lint.pyupgrade] 95 | keep-runtime-typing = true 96 | -------------------------------------------------------------------------------- /cdv/clibs/curry_and_treehash.clib: -------------------------------------------------------------------------------- 1 | ( 2 | ;; The code below is used to calculate of the tree hash of a curried function 3 | ;; without actually doing the curry, and using other optimization tricks 4 | ;; like unrolling `sha256tree`. 5 | 6 | (defconstant ONE 1) 7 | (defconstant TWO 2) 8 | (defconstant A_KW #a) 9 | (defconstant Q_KW #q) 10 | (defconstant C_KW #c) 11 | 12 | ;; Given the tree hash `environment-hash` of an environment tree E 13 | ;; and the tree hash `parameter-hash` of a constant parameter P 14 | ;; return the tree hash of the tree corresponding to 15 | ;; `(c (q . P) E)` 16 | ;; This is the new environment tree with the addition parameter P curried in. 17 | ;; 18 | ;; Note that `(c (q . P) E)` = `(c . ((q . P) . (E . 0)))` 19 | 20 | (defun-inline update-hash-for-parameter-hash (parameter-hash environment-hash) 21 | (sha256 TWO (sha256 ONE C_KW) 22 | (sha256 TWO (sha256 TWO (sha256 ONE Q_KW) parameter-hash) 23 | (sha256 TWO environment-hash (sha256 ONE 0)))) 24 | ) 25 | 26 | ;; This function recursively calls `update-hash-for-parameter-hash`, updating `environment-hash` 27 | ;; along the way. 28 | 29 | (defun build-curry-list (reversed-curry-parameter-hashes environment-hash) 30 | (if reversed-curry-parameter-hashes 31 | (build-curry-list (r reversed-curry-parameter-hashes) 32 | (update-hash-for-parameter-hash (f reversed-curry-parameter-hashes) environment-hash)) 33 | environment-hash 34 | ) 35 | ) 36 | 37 | ;; Given the tree hash `environment-hash` of an environment tree E 38 | ;; and the tree hash `function-hash` of a function tree F 39 | ;; return the tree hash of the tree corresponding to 40 | ;; `(a (q . F) E)` 41 | ;; This is the hash of a new function that adopts the new environment E. 42 | ;; This is used to build of the tree hash of a curried function. 43 | ;; 44 | ;; Note that `(a (q . F) E)` = `(a . ((q . F) . (E . 0)))` 45 | 46 | (defun-inline tree-hash-of-apply (function-hash environment-hash) 47 | (sha256 TWO (sha256 ONE A_KW) 48 | (sha256 TWO (sha256 TWO (sha256 ONE Q_KW) function-hash) 49 | (sha256 TWO environment-hash (sha256 ONE 0)))) 50 | ) 51 | 52 | ;; function-hash: 53 | ;; the hash of a puzzle function, ie. a `mod` 54 | ;; 55 | ;; reversed-curry-parameter-hashes: 56 | ;; a list of pre-hashed trees representing parameters to be curried into the puzzle. 57 | ;; Note that this must be applied in REVERSED order. This may seem strange, but it greatly simplifies 58 | ;; the underlying code, since we calculate the tree hash from the bottom nodes up, and the last 59 | ;; parameters curried must have their hashes calculated first. 60 | ;; 61 | ;; we return the hash of the curried expression 62 | ;; (a (q . function-hash) (c (cp1 (c cp2 (c ... 1)...)))) 63 | 64 | (defun puzzle-hash-of-curried-function (function-hash . reversed-curry-parameter-hashes) 65 | (tree-hash-of-apply function-hash 66 | (build-curry-list reversed-curry-parameter-hashes (sha256 ONE ONE))) 67 | ) 68 | ) 69 | -------------------------------------------------------------------------------- /.github/workflows/run-test-suite.yml: -------------------------------------------------------------------------------- 1 | name: Run Test Suite 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | tags: 9 | - "**" 10 | pull_request: 11 | branches: 12 | - "**" 13 | 14 | concurrency: 15 | group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}--${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/heads/long_lived/')) && github.sha || '' }} 16 | cancel-in-progress: true 17 | 18 | defaults: 19 | run: 20 | shell: bash 21 | 22 | jobs: 23 | build: 24 | name: Test - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }} 25 | runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }} 26 | timeout-minutes: 30 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | os: 31 | - name: macOS 32 | matrix: macos 33 | runs-on: 34 | arm: [macos-13-arm64] 35 | intel: [macos-15-intel] 36 | - name: Ubuntu 37 | matrix: ubuntu 38 | runs-on: 39 | arm: [Linux, ARM64] 40 | intel: [ubuntu-latest] 41 | - name: Windows 42 | matrix: windows 43 | runs-on: 44 | intel: [windows-latest] 45 | python: 46 | - major-dot-minor: "3.10" 47 | matrix: "3.10" 48 | - major-dot-minor: "3.11" 49 | matrix: "3.11" 50 | - major-dot-minor: "3.12" 51 | matrix: "3.12" 52 | arch: 53 | - name: ARM 54 | matrix: arm 55 | - name: Intel 56 | matrix: intel 57 | exclude: 58 | - os: 59 | matrix: windows 60 | arch: 61 | matrix: arm 62 | - os: 63 | matrix: macos 64 | arch: 65 | matrix: arm 66 | 67 | steps: 68 | - name: Clean workspace 69 | uses: Chia-Network/actions/clean-workspace@main 70 | 71 | - name: Checkout Code 72 | uses: actions/checkout@v6 73 | with: 74 | fetch-depth: 0 75 | 76 | - uses: Chia-Network/actions/setup-python@main 77 | with: 78 | python-version: ${{ matrix.python.major-dot-minor }} 79 | 80 | - uses: Chia-Network/actions/create-venv@main 81 | id: create-venv 82 | 83 | - uses: Chia-Network/actions/activate-venv@main 84 | with: 85 | directories: ${{ steps.create-venv.outputs.activate-venv-directories }} 86 | 87 | - name: Test code with pytest 88 | if: runner.os == 'Linux' || runner.os == 'macOS' 89 | shell: bash 90 | run: | 91 | python3 -m venv venv 92 | . ./venv/bin/activate 93 | pip install ."[dev]" 94 | ./venv/bin/chia init 95 | ./venv/bin/pytest tests/ cdv/examples/tests -s -v --durations 0 96 | 97 | - name: Test code with pytest 98 | if: runner.os == 'Windows' 99 | shell: powershell 100 | run: | 101 | python -m pip install --upgrade pip 102 | python -m pip install .[dev] 103 | chia init 104 | pytest tests\ cdv\examples\tests -s -v --durations 0 105 | -------------------------------------------------------------------------------- /cdv/util/sign_coin_spends.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | from collections.abc import Callable 5 | from typing import Any 6 | 7 | from chia.consensus.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict 8 | from chia.types.coin_spend import CoinSpend 9 | from chia_rs import AugSchemeMPL, G1Element, G2Element, SpendBundle 10 | from chia_rs.sized_bytes import bytes32 11 | 12 | 13 | async def sign_coin_spends( 14 | coin_spends: list[CoinSpend], 15 | secret_key_for_public_key_f: Any, # Potentially awaitable function from G1Element => Optional[PrivateKey] 16 | secret_key_for_puzzle_hash: Any, # Potentially awaitable function from bytes32 => Optional[PrivateKey] 17 | additional_data: bytes, 18 | max_cost: int, 19 | potential_derivation_functions: list[Callable[[G1Element], bytes32]], 20 | ) -> SpendBundle: 21 | """ 22 | Sign_coin_spends runs the puzzle code with the given argument and searches the 23 | result for an AGG_SIG_ME condition, which it attempts to sign by requesting a 24 | matching PrivateKey corresponding with the given G1Element (public key) specified 25 | in the resulting condition output. 26 | It's important to note that as mentioned in the documentation about the standard 27 | spend that the public key presented to the secret_key_for_public_key_f function 28 | provided to sign_coin_spends must be prepared to do the key derivations required 29 | by the coin types it's allowed to spend (at least the derivation of the standard 30 | spend as done by calculate_synthetic_secret_key with DEFAULT_PUZZLE_HASH). 31 | If a coin performed a different key derivation, the pk presented to this function 32 | would be similarly alien, and would need to be tried against the first stage 33 | derived keys (those returned by master_sk_to_wallet_sk from the ['sk'] member of 34 | wallet rpc's get_private_key method). 35 | """ 36 | signatures: list[G2Element] = [] 37 | pk_list: list[G1Element] = [] 38 | msg_list: list[bytes] = [] 39 | for coin_spend in coin_spends: 40 | # Get AGG_SIG conditions 41 | conditions_dict = conditions_dict_for_solution(coin_spend.puzzle_reveal, coin_spend.solution, max_cost) 42 | # Create signature 43 | for pk, msg in pkm_pairs_for_conditions_dict(conditions_dict, coin_spend.coin, additional_data): 44 | pk_list.append(pk) 45 | msg_list.append(msg) 46 | if inspect.iscoroutinefunction(secret_key_for_public_key_f): 47 | secret_key = await secret_key_for_public_key_f(pk) 48 | else: 49 | secret_key = secret_key_for_public_key_f(pk) 50 | if secret_key is None or secret_key.get_g1() != pk: 51 | for derive in potential_derivation_functions: 52 | if inspect.iscoroutinefunction(secret_key_for_puzzle_hash): 53 | secret_key = await secret_key_for_puzzle_hash(derive(pk)) 54 | else: 55 | secret_key = secret_key_for_puzzle_hash(derive(pk)) 56 | if secret_key is not None and secret_key.get_g1() == pk: 57 | break 58 | else: 59 | raise ValueError(f"no secret key for {pk}") 60 | signature = AugSchemeMPL.sign(secret_key, msg) 61 | signatures.append(signature) 62 | 63 | # Aggregate signatures 64 | aggsig = AugSchemeMPL.aggregate(signatures) 65 | return SpendBundle(coin_spends, aggsig) 66 | -------------------------------------------------------------------------------- /cdv/util/load_clvm.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib 4 | import inspect 5 | import os 6 | import pathlib 7 | 8 | import importlib_resources 9 | from chia.types.blockchain_format.program import Program 10 | from chia.types.blockchain_format.serialized_program import SerializedProgram 11 | from clvm_tools.clvmc import compile_clvm as compile_clvm_py 12 | 13 | compile_clvm = compile_clvm_py 14 | 15 | 16 | # Handle optional use of clvm_tools_rs if available and requested 17 | if "CLVM_TOOLS_RS" in os.environ: 18 | try: 19 | 20 | def sha256file(f): 21 | import hashlib 22 | 23 | m = hashlib.sha256() 24 | m.update(open(f).read().encode("utf8")) 25 | return m.hexdigest() 26 | 27 | from clvm_tools_rs import compile_clvm as compile_clvm_rs # type: ignore[import-untyped] 28 | 29 | def translate_path(p_): 30 | p = str(p_) 31 | if os.path.isdir(p): 32 | return p 33 | else: 34 | try: 35 | module_object = importlib.import_module(p) 36 | return os.path.dirname(inspect.getfile(module_object)) 37 | except Exception: 38 | return p 39 | 40 | def rust_compile_clvm(full_path, output, search_paths=[]): 41 | treated_include_paths = list(map(translate_path, search_paths)) 42 | print("compile_clvm_rs", full_path, output, treated_include_paths) 43 | compile_clvm_rs(str(full_path), str(output), treated_include_paths) 44 | 45 | if os.environ["CLVM_TOOLS_RS"] == "check": 46 | assert False 47 | orig = str(output) + ".orig" 48 | compile_clvm_py(full_path, orig, search_paths=search_paths) 49 | orig256 = sha256file(orig) 50 | rs256 = sha256file(output) 51 | 52 | if orig256 != rs256: 53 | print(f"Compiled {full_path}: {orig256} vs {rs256}\n") 54 | print("Aborting compilation due to mismatch with rust") 55 | assert orig256 == rs256 56 | 57 | compile_clvm = rust_compile_clvm 58 | finally: 59 | pass 60 | 61 | 62 | def load_serialized_clvm(clvm_filename, package_or_requirement=__name__, search_paths=[]) -> SerializedProgram: 63 | """ 64 | This function takes a .clvm file in the given package and compiles it to a 65 | .clvm.hex file if the .hex file is missing or older than the .clvm file, then 66 | returns the contents of the .hex file as a `Program`. 67 | 68 | clvm_filename: file name 69 | package_or_requirement: usually `__name__` if the clvm file is in the same package 70 | """ 71 | 72 | hex_filename = f"{clvm_filename}.hex" 73 | 74 | resources = importlib_resources.files(package_or_requirement) 75 | 76 | try: 77 | full_path = resources.joinpath(clvm_filename) 78 | output = full_path.parent / hex_filename 79 | compile_clvm( 80 | full_path, 81 | output, 82 | search_paths=[full_path.parent, pathlib.Path.cwd().joinpath("include"), *search_paths], 83 | ) 84 | except Exception: 85 | # so we just fall through to loading the hex clvm 86 | pass 87 | 88 | clvm_path = resources.joinpath(hex_filename) 89 | clvm_hex = clvm_path.read_text(encoding="utf-8") 90 | clvm_blob = bytes.fromhex(clvm_hex) 91 | return SerializedProgram.from_bytes(clvm_blob) 92 | 93 | 94 | def load_clvm(clvm_filename, package_or_requirement=__name__, search_paths=[]) -> Program: 95 | return Program.from_bytes( 96 | bytes( 97 | load_serialized_clvm( 98 | clvm_filename, package_or_requirement=package_or_requirement, search_paths=search_paths 99 | ) 100 | ) 101 | ) 102 | -------------------------------------------------------------------------------- /cdv/cmds/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import shutil 5 | from pathlib import Path 6 | 7 | import click 8 | import pytest 9 | from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash 10 | from chia.util.hash import std_hash 11 | from chia_rs.sized_bytes import bytes32 12 | 13 | from cdv import __version__ 14 | from cdv.cmds.chia_inspect import inspect_cmd 15 | from cdv.cmds.clsp import clsp_cmd 16 | from cdv.cmds.rpc import rpc_cmd 17 | 18 | CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) 19 | 20 | 21 | def monkey_patch_click() -> None: 22 | # this hacks around what seems to be an incompatibility between the python from `pyinstaller` 23 | # and `click` 24 | # 25 | # Not 100% sure on the details, but it seems that `click` performs a check on start-up 26 | # that `codecs.lookup(locale.getpreferredencoding()).name != 'ascii'`, and refuses to start 27 | # if it's not. The python that comes with `pyinstaller` fails this check. 28 | # 29 | # This will probably cause problems with the command-line tools that use parameters that 30 | # are not strict ascii. The real fix is likely with the `pyinstaller` python. 31 | 32 | import click.core 33 | 34 | click.core._verify_python3_env = lambda *args, **kwargs: 0 # type: ignore 35 | 36 | 37 | @click.group( 38 | help="\n Dev tooling for Chia development \n", 39 | context_settings=CONTEXT_SETTINGS, 40 | ) 41 | @click.version_option(__version__) 42 | @click.pass_context 43 | def cli(ctx: click.Context) -> None: 44 | ctx.ensure_object(dict) 45 | 46 | 47 | @cli.command("test", short_help="Run the local test suite (located in ./tests)") 48 | @click.argument("tests", default="./tests", required=False) 49 | @click.option( 50 | "-d", 51 | "--discover", 52 | is_flag=True, 53 | help="list the tests without running them", 54 | ) 55 | @click.option( 56 | "-i", 57 | "--init", 58 | is_flag=True, 59 | help="Create the test directory and/or add a new test skeleton", 60 | ) 61 | def test_cmd(tests: str, discover: bool, init: str): 62 | test_paths: list[str] = list(map(str, Path.cwd().glob(tests))) 63 | if init: 64 | test_dir = Path(os.getcwd()).joinpath("tests") 65 | if not test_dir.exists(): 66 | os.mkdir("tests") 67 | 68 | import cdv.test as testlib 69 | 70 | # It finds these files relative to its position in the venv 71 | # If the cdv/test/__init__.py file or any of the relvant files move, this will break 72 | src_path = Path(testlib.__file__).parent.joinpath("test_skeleton.py") 73 | dest_path: Path = test_dir.joinpath("test_skeleton.py") 74 | shutil.copyfile(src_path, dest_path) 75 | dest_path_init: Path = test_dir.joinpath("__init__.py") 76 | open(dest_path_init, "w") 77 | if discover: 78 | pytest.main(["--collect-only", *test_paths]) 79 | elif not init: 80 | pytest.main([*test_paths]) 81 | 82 | 83 | @cli.command("hash", short_help="SHA256 hash UTF-8 strings or bytes (use 0x prefix for bytes)") 84 | @click.argument("data", nargs=1, required=True) 85 | def hash_cmd(data: str): 86 | if data[:2] == "0x": 87 | hash_data = bytes.fromhex(data[2:]) 88 | else: 89 | hash_data = bytes(data, "utf-8") 90 | print(std_hash(hash_data)) 91 | 92 | 93 | @cli.command("encode", short_help="Encode a puzzle hash to a bech32m address") 94 | @click.argument("puzzle_hash", nargs=1, required=True) 95 | @click.option( 96 | "-p", 97 | "--prefix", 98 | default="xch", 99 | show_default=True, 100 | required=False, 101 | help="The prefix to encode with", 102 | ) 103 | def encode_cmd(puzzle_hash: str, prefix: str): 104 | print(encode_puzzle_hash(bytes32.from_hexstr(puzzle_hash), prefix)) 105 | 106 | 107 | @cli.command("decode", short_help="Decode a bech32m address to a puzzle hash") 108 | @click.argument("address", nargs=1, required=True) 109 | def decode_cmd(address: str): 110 | print(decode_puzzle_hash(address).hex()) 111 | 112 | 113 | cli.add_command(clsp_cmd) 114 | cli.add_command(inspect_cmd) 115 | cli.add_command(rpc_cmd) 116 | 117 | 118 | def main() -> None: 119 | monkey_patch_click() 120 | cli() # pylint: disable=no-value-for-parameter 121 | 122 | 123 | if __name__ == "__main__": 124 | main() 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Chia Dev Tools 2 | ======= 3 | 4 | Install 5 | ------- 6 | 7 | Initialize a new project directory and `cd` into it. Then follow the following instructions to get set up: 8 | 9 | **Ubuntu/MacOSs** 10 | ``` 11 | python3 -m venv venv 12 | . ./venv/bin/activate 13 | pip install --extra-index-url https://pypi.chia.net/simple/ chia-dev-tools 14 | cdv --version 15 | ``` 16 | (If you're on an M1 Mac, make sure you are running an ARM64 native python virtual environment) 17 | 18 | **Windows Powershell** 19 | 20 | Requires: Installation of [Python 3 for Windows](https://www.python.org/downloads/windows/) 21 | 22 | ``` 23 | py -m venv venv 24 | ./venv/Scripts/activate 25 | pip install --extra-index-url https://pypi.chia.net/simple/ chia-dev-tools 26 | cdv --version 27 | ``` 28 | 29 | **From Source** 30 | 31 | Alternatively, you can clone the repo, and install from source: 32 | ``` 33 | git clone https://github.com/Chia-Network/chia-dev-tools.git 34 | cd chia-dev-tools 35 | # The following for Linux/MacOS 36 | python3 -m venv venv 37 | . ./venv/bin/activate 38 | # The following for Windows 39 | py -m venv venv 40 | ./venv/Scripts/activate 41 | # To install chia-dev-tools 42 | pip install --extra-index-url https://pypi.chia.net/simple/ . 43 | ``` 44 | 45 | What's in it? 46 | ------------- 47 | 48 | This python wheel will bring in commands from other chia repositories such as `brun` or even `chia`! 49 | 50 | The command unique to this repository is `cdv`. Run `cdv --help` to see what it does: 51 | 52 | ``` 53 | Usage: cdv [OPTIONS] COMMAND [ARGS]... 54 | 55 | Dev tooling for Chia development 56 | 57 | Options: 58 | --version Show the version and exit. 59 | -h, --help Show this message and exit. 60 | 61 | Commands: 62 | clsp Commands to use when developing with chialisp 63 | decode Decode a bech32m address to a puzzle hash 64 | encode Encode a puzzle hash to a bech32m address 65 | hash SHA256 hash UTF-8 strings or bytes (use 0x prefix for bytes) 66 | inspect Inspect various data structures 67 | rpc Make RPC requests to a Chia full node 68 | test Run the local test suite (located in ./tests) 69 | ``` 70 | 71 | Tests 72 | ---------- 73 | 74 | The test command allows you to initialize and run local tests. 75 | ``` 76 | cdv test 77 | ``` 78 | 79 | Optionally, to make new tests, you can bootstrap the creation of a test file by running: 80 | ``` 81 | cdv test --init 82 | # Make changes to the ./tests/test_skeleton.py file 83 | cdv test 84 | ``` 85 | 86 | 87 | Chialisp Commands 88 | ----------------- 89 | 90 | The `clsp` family of commands are helpful when writing, building, and hashing Chialisp and CLVM programs. 91 | 92 | ``` 93 | cdv clsp build ./puzzles/password.clsp 94 | cdv clsp retrieve condition_codes sha256tree 95 | cdv clsp treehash '(a 2 3)' 96 | cdv clsp curry ./puzzles/password.clsp.hex -a 0xdeadbeef -a "(q . 'I'm an inner puzzle!')" 97 | cdv clsp disassemble ff0180 98 | ``` 99 | 100 | Inspect Commands 101 | ---------------- 102 | 103 | The `inspect` family of commands allows you to build and examine certain Chia related objects 104 | 105 | ``` 106 | cdv inspect -id coins --parent-id e16dbc782f500aa24891886779067792b3305cff8b873ae1e77273ad0b7e6c05 --puzzle-hash e16dbc782f500aa24891886779067792b3305cff8b873ae1e77273ad0b7e6c05 --amount 123 107 | cdv inspect --json spends --coin ./coin.json --puzzle-reveal ff0180 --solution '()' 108 | cdv inspect --bytes spendbundles ./spend_bundle.json 109 | cdv inspect --json any 0e1074f76177216b011668c35b1496cbd10eff5ae43f6a7924798771ac131b0a0e1074f76177216b011668c35b1496cbd10eff5ae43f6a7924798771ac131b0a0000000000000001ff018080 110 | ``` 111 | 112 | RPC Commands 113 | ------------ 114 | 115 | There are also commands for interacting with the full node's RPC endpoints (in development, more to come). The family of commands finds the full node the same way that the `chia` commands do. Make sure to have a local node running before you try these. 116 | 117 | ``` 118 | cdv rpc state 119 | cdv rpc blocks -s 0 -e 1 120 | cdv rpc coinrecords --by id 6ce8fa56321d954f54ba27e58f4a025eb1081d2e1f38fc089a2e72927bcde0d1 121 | cdv rpc pushtx ./spend_bundle.json 122 | ``` 123 | 124 | Python Packages 125 | --------------- 126 | 127 | Being in a virtual environment with this tool will also give your python programs access to all of the chia repository packages. 128 | It also comes with a package of its own that lives in the `cdv` namespace with some helpful utilities. Of particular interest is the `cdv.test` package which comes with all sorts of tools to help you write lifecycle tests of smart coins. Check out [the examples](https://github.com/Chia-Network/chia-dev-tools/tree/main/cdv/examples) to see it in action. 129 | -------------------------------------------------------------------------------- /cdv/examples/tests/test_piggybank.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | import pytest_asyncio 5 | from chia.types.blockchain_format.coin import Coin 6 | from chia.types.condition_opcodes import ConditionOpcode 7 | from chia_rs import SpendBundle 8 | from chia_rs.sized_ints import uint64 9 | 10 | from cdv.examples.drivers.piggybank_drivers import ( 11 | create_piggybank_puzzle, 12 | piggybank_announcement_assertion, 13 | solution_for_piggybank, 14 | ) 15 | from cdv.test import CoinWrapper 16 | from cdv.test import setup as setup_test 17 | 18 | 19 | class TestStandardTransaction: 20 | @pytest_asyncio.fixture(scope="function") 21 | async def setup(self): 22 | async with setup_test() as (network, alice, bob): 23 | await network.farm_block() 24 | yield network, alice, bob 25 | 26 | async def make_and_spend_piggybank(self, network, alice, bob, CONTRIBUTION_AMOUNT) -> dict[str, list[Coin]]: 27 | # Get our alice wallet some money 28 | await network.farm_block(farmer=alice) 29 | 30 | # This will use one mojo to create our piggybank on the blockchain. 31 | piggybank_coin: CoinWrapper | None = await alice.launch_smart_coin( 32 | create_piggybank_puzzle(uint64(1000000000000), bob.puzzle_hash) 33 | ) 34 | # This retrieves us a coin that is at least 500 mojos. 35 | contribution_coin: CoinWrapper | None = await alice.choose_coin(CONTRIBUTION_AMOUNT) 36 | 37 | # Make sure everything succeeded 38 | if not piggybank_coin or not contribution_coin: 39 | raise ValueError("Something went wrong launching/choosing a coin") 40 | 41 | # This is the spend of the piggy bank coin. We use the driver code to create the solution. 42 | piggybank_spend: SpendBundle = await alice.spend_coin( 43 | piggybank_coin, 44 | pushtx=False, 45 | args=solution_for_piggybank(piggybank_coin.coin, CONTRIBUTION_AMOUNT), 46 | ) 47 | # This is the spend of a standard coin. We simply spend to ourselves but minus the CONTRIBUTION_AMOUNT. 48 | contribution_spend: SpendBundle = await alice.spend_coin( 49 | contribution_coin, 50 | pushtx=False, 51 | amt=(contribution_coin.amount - CONTRIBUTION_AMOUNT), 52 | custom_conditions=[ 53 | [ 54 | ConditionOpcode.CREATE_COIN, 55 | contribution_coin.puzzle_hash, 56 | (contribution_coin.amount - CONTRIBUTION_AMOUNT), 57 | ], 58 | piggybank_announcement_assertion(piggybank_coin.coin, CONTRIBUTION_AMOUNT), 59 | ], 60 | ) 61 | 62 | # Aggregate them to make sure they are spent together 63 | combined_spend = SpendBundle.aggregate([contribution_spend, piggybank_spend]) 64 | 65 | result: dict[str, list[Coin]] = await network.push_tx(combined_spend) 66 | return result 67 | 68 | @pytest.mark.asyncio 69 | async def test_piggybank_contribution(self, setup): 70 | network, alice, bob = setup 71 | try: 72 | result: dict[str, list[Coin]] = await self.make_and_spend_piggybank(network, alice, bob, 500) 73 | 74 | assert "error" not in result 75 | 76 | # Make sure there is exactly one piggybank with the new amount 77 | filtered_result: list[Coin] = list( 78 | filter( 79 | lambda addition: (addition.amount == 501) 80 | and ( 81 | addition.puzzle_hash == create_piggybank_puzzle(1000000000000, bob.puzzle_hash).get_tree_hash() 82 | ), 83 | result["additions"], 84 | ) 85 | ) 86 | assert len(filtered_result) == 1 87 | finally: 88 | pass 89 | 90 | @pytest.mark.asyncio 91 | async def test_piggybank_completion(self, setup): 92 | network, alice, bob = setup 93 | try: 94 | result: dict[str, list[Coin]] = await self.make_and_spend_piggybank(network, alice, bob, 1000000000000) 95 | 96 | assert "error" not in result 97 | 98 | # Make sure there is exactly one piggybank with value 0 99 | filtered_result: list[Coin] = list( 100 | filter( 101 | lambda addition: (addition.amount == 0) 102 | and ( 103 | addition.puzzle_hash == create_piggybank_puzzle(1000000000000, bob.puzzle_hash).get_tree_hash() 104 | ), 105 | result["additions"], 106 | ) 107 | ) 108 | assert len(filtered_result) == 1 109 | 110 | # Make sure there is exactly one coin that has been cashed out to bob 111 | filtered_result: list[Coin] = list( 112 | filter( 113 | lambda addition: (addition.amount == 1000000000001) and (addition.puzzle_hash == bob.puzzle_hash), 114 | result["additions"], 115 | ) 116 | ) 117 | assert len(filtered_result) == 1 118 | finally: 119 | pass 120 | 121 | @pytest.mark.asyncio 122 | async def test_piggybank_stealing(self, setup): 123 | network, alice, bob = setup 124 | try: 125 | result: dict[str, list[Coin]] = await self.make_and_spend_piggybank(network, alice, bob, -100) 126 | assert "error" in result 127 | assert ( 128 | "GENERATOR_RUNTIME_ERROR" in result["error"] 129 | ) # This fails during puzzle execution, not in driver code 130 | finally: 131 | pass 132 | -------------------------------------------------------------------------------- /cdv/cmds/clsp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import shutil 5 | from pathlib import Path 6 | 7 | import click 8 | from chia.types.blockchain_format.program import Program 9 | from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash 10 | from chia_rs.sized_bytes import bytes32 11 | from clvm_tools.binutils import SExp, assemble, disassemble 12 | 13 | from cdv.cmds.util import append_include, parse_program 14 | from cdv.util.load_clvm import compile_clvm 15 | 16 | 17 | @click.group("clsp", short_help="Commands to use when developing with chialisp") 18 | def clsp_cmd() -> None: 19 | pass 20 | 21 | 22 | @clsp_cmd.command( 23 | "build", 24 | short_help="Build all specified CLVM files (i.e mypuz.clsp or ./puzzles/*.clsp)", 25 | ) 26 | @click.argument("files", nargs=-1, required=True, default=None) 27 | @click.option( 28 | "-i", 29 | "--include", 30 | required=False, 31 | multiple=True, 32 | help="Paths to search for include files (./include will be searched automatically)", 33 | ) 34 | def build_cmd(files: tuple[str], include: tuple[str]) -> None: 35 | project_path = Path.cwd() 36 | clvm_files = [] 37 | for glob in files: 38 | for path in Path(project_path).rglob(glob): 39 | if path.is_dir(): 40 | for clvm_path in Path(path).rglob("*.cl[vs][mp]"): 41 | clvm_files.append(clvm_path) 42 | else: 43 | clvm_files.append(path) 44 | 45 | for filename in clvm_files: 46 | hex_file_name: str = filename.name + ".hex" 47 | full_hex_file_name = Path(filename.parent).joinpath(hex_file_name) 48 | # We only rebuild the file if the .hex is older 49 | if not (full_hex_file_name.exists() and full_hex_file_name.stat().st_mtime > filename.stat().st_mtime): 50 | outfile = str(filename) + ".hex" 51 | try: 52 | print("Beginning compilation of " + filename.name + "...") 53 | compile_clvm(str(filename), outfile, search_paths=append_include(include)) 54 | print("...Compilation finished") 55 | except Exception as e: 56 | print("Couldn't build " + filename.name + ": " + str(e)) 57 | 58 | 59 | @clsp_cmd.command("disassemble", short_help="Disassemble serialized clvm into human readable form.") 60 | @click.argument("programs", nargs=-1, required=True) 61 | def disassemble_cmd(programs: tuple[str]): 62 | for program in programs: 63 | print(disassemble(parse_program(program))) 64 | 65 | 66 | @clsp_cmd.command("treehash", short_help="Return the tree hash of a clvm file or string") 67 | @click.argument("program", nargs=1, required=True) 68 | @click.option( 69 | "-i", 70 | "--include", 71 | required=False, 72 | multiple=True, 73 | help="Paths to search for include files (./include will be searched automatically)", 74 | ) 75 | def treehash_cmd(program: str, include: tuple[str]): 76 | print(parse_program(program, include).get_tree_hash()) 77 | 78 | 79 | @clsp_cmd.command("curry", short_help="Curry a program with specified arguments") 80 | @click.argument("program", required=True) 81 | @click.option( 82 | "-a", 83 | "--args", 84 | multiple=True, 85 | help="An argument to be curried in (i.e. -a 0xdeadbeef -a '(a 2 3)')", 86 | ) 87 | @click.option("-H", "--treehash", is_flag=True, help="Output the tree hash of the curried puzzle") 88 | @click.option( 89 | "-x", 90 | "--dump", 91 | is_flag=True, 92 | help="Output the hex serialized program rather that the CLVM form", 93 | ) 94 | @click.option( 95 | "-i", 96 | "--include", 97 | required=False, 98 | multiple=True, 99 | help="Paths to search for include files (./include will be searched automatically)", 100 | ) 101 | def curry_cmd(program: str, args: tuple[str], treehash: bool, dump: bool, include: tuple[str]): 102 | prog: Program = parse_program(program, include) 103 | curry_args: list[SExp] = [assemble(arg) for arg in args] 104 | 105 | prog_final: Program = prog.curry(*curry_args) 106 | if treehash: 107 | print(prog_final.get_tree_hash()) 108 | elif dump: 109 | print(prog_final) 110 | else: 111 | print(disassemble(prog_final)) 112 | 113 | 114 | @clsp_cmd.command("uncurry", short_help="Uncurry a program and list the arguments") 115 | @click.argument("program", required=True) 116 | @click.option("-H", "--treehash", is_flag=True, help="Output the tree hash of the curried puzzle") 117 | @click.option( 118 | "-x", 119 | "--dump", 120 | is_flag=True, 121 | help="Output the hex serialized program rather that the CLVM form", 122 | ) 123 | def uncurry_cmd(program: str, treehash: bool, dump: bool): 124 | prog: Program = parse_program(program) 125 | 126 | prog_final, curried_args = prog.uncurry() 127 | if treehash: 128 | print(prog_final.get_tree_hash()) 129 | elif dump: 130 | print("--- Uncurried Module ---") 131 | print(prog_final) 132 | print("--- Curried Args ---") 133 | for arg in curried_args.as_iter(): 134 | print("- " + str(arg)) 135 | else: 136 | print("--- Uncurried Module ---") 137 | print(disassemble(prog_final)) 138 | print("--- Curried Args ---") 139 | for arg in curried_args.as_iter(): 140 | print("- " + disassemble(arg)) 141 | 142 | 143 | @clsp_cmd.command( 144 | "cat_puzzle_hash", 145 | short_help=( 146 | "Return the outer puzzle address/hash for a CAT with the given tail hash" 147 | " & inner puzzlehash/receive address (can be hex or bech32m)" 148 | ), 149 | ) 150 | @click.argument("inner_puzzlehash", required=True) 151 | @click.option( 152 | "-t", 153 | "--tail", 154 | "tail_hash", 155 | required=True, 156 | help="The tail hash of the CAT (hex or one of the standard CAT symbols, e.g. MRMT)", 157 | ) 158 | def cat_puzzle_hash(inner_puzzlehash: str, tail_hash: str): 159 | from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS 160 | from chia.wallet.cat_wallet.cat_utils import CAT_MOD 161 | 162 | default_cats_by_symbols = {cat["symbol"]: cat for cat in DEFAULT_CATS.values()} 163 | if tail_hash in default_cats_by_symbols: 164 | tail_hash = default_cats_by_symbols[tail_hash]["asset_id"] 165 | prefix = "" 166 | try: 167 | # User passed in a hex puzzlehash 168 | inner_puzzlehash_bytes32: bytes32 = bytes32.from_hexstr(inner_puzzlehash) 169 | except ValueError: 170 | # If that failed, we're dealing with a bech32m inner puzzlehash. 171 | inner_puzzlehash_bytes32 = decode_puzzle_hash(inner_puzzlehash) 172 | prefix = inner_puzzlehash[: inner_puzzlehash.rfind("1")] 173 | # get_tree_hash supports a special "already hashed" list. We're supposed to 174 | # curry in the full inner puzzle into CAT_MOD, but we only have its hash. 175 | # We can still compute the treehash similarly to how the CAT puzzle does it 176 | # using `puzzle-hash-of-curried-function` in curry_and_treehash.clib. 177 | outer_puzzlehash = CAT_MOD.curry( 178 | CAT_MOD.get_tree_hash(), bytes32.from_hexstr(tail_hash), inner_puzzlehash_bytes32 179 | ).get_tree_hash_precalc(inner_puzzlehash_bytes32) 180 | 181 | if prefix: 182 | print(encode_puzzle_hash(outer_puzzlehash, prefix)) 183 | else: 184 | print(outer_puzzlehash) 185 | 186 | 187 | @clsp_cmd.command( 188 | "retrieve", 189 | short_help="Copy the specified .clib file to the current directory (for example sha256tree)", 190 | ) 191 | @click.argument("libraries", nargs=-1, required=True) 192 | def retrieve_cmd(libraries: tuple[str]): 193 | from cdv import clibs 194 | 195 | for lib in libraries: 196 | if lib[-5:] == ".clib": # We'll take it with or without the extension 197 | lib = lib[:-5] 198 | src_path = Path(clibs.__file__).parent.joinpath(f"{lib}.clib") 199 | include_path = Path(os.getcwd()).joinpath("include") 200 | if not include_path.exists(): 201 | os.mkdir("include") 202 | if src_path.exists(): 203 | shutil.copyfile(src_path, include_path.joinpath(f"{lib}.clib")) 204 | else: 205 | print(f"Could not find {lib}.clib. You can always create it in ./include yourself.") 206 | -------------------------------------------------------------------------------- /tests/cmds/test_clsp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import shutil 5 | from pathlib import Path 6 | from typing import IO 7 | 8 | from chia.types.blockchain_format.program import Program 9 | from click.testing import CliRunner, Result 10 | from clvm_tools.binutils import disassemble 11 | 12 | from cdv.cmds.cli import cli 13 | 14 | 15 | class TestClspCommands: 16 | program: str = "(q . 1)" 17 | serialized: str = "ff0101" 18 | mod: str = "(mod () (include condition_codes.clib) CREATE_COIN)" 19 | 20 | # This comes before build because build is going to use retrieve 21 | def test_retrieve(self): 22 | runner = CliRunner() 23 | with runner.isolated_filesystem(): 24 | result: Result = runner.invoke(cli, ["clsp", "retrieve", "condition_codes"]) 25 | assert result.exit_code == 0 26 | assert Path("./include/condition_codes.clib").exists() 27 | 28 | result = runner.invoke(cli, ["clsp", "retrieve", "sha256tree.clib"]) 29 | assert result.exit_code == 0 30 | assert Path("./include/condition_codes.clib").exists() 31 | 32 | def test_build(self): 33 | runner = CliRunner() 34 | with runner.isolated_filesystem(): 35 | # Test building CLVM 36 | program_file: IO = open("program.clvm", "w") 37 | program_file.write(self.program) 38 | program_file.close() 39 | result: Result = runner.invoke(cli, ["clsp", "build", "."]) 40 | assert result.exit_code == 0 41 | assert Path("./program.clvm.hex").exists() 42 | hex_output: str = open("program.clvm.hex").read() 43 | assert "01" in hex_output 44 | assert len(hex_output) <= 3 # With or without newline 45 | 46 | # Use the retrieve command for the include file 47 | runner.invoke(cli, ["clsp", "retrieve", "condition_codes"]) 48 | 49 | # Test building Chialisp (automatic include search) 50 | mod_file: IO = open("mod.clsp", "w") 51 | mod_file.write(self.mod) 52 | mod_file.close() 53 | result = runner.invoke(cli, ["clsp", "build", "./mod.clsp"]) 54 | assert result.exit_code == 0 55 | assert Path("./mod.clsp.hex").exists() 56 | hex_output: str = open("mod.clsp.hex").read() 57 | assert "ff0133" in hex_output 58 | assert len(hex_output) <= 7 # With or without newline 59 | 60 | # Test building Chialisp (specified include search) 61 | os.remove(Path("./mod.clsp.hex")) 62 | shutil.copytree("./include", "./include_test") 63 | shutil.rmtree("./include") 64 | result = runner.invoke(cli, ["clsp", "build", ".", "--include", "./include_test"]) 65 | assert result.exit_code == 0 66 | assert Path("./mod.clsp.hex").exists() 67 | hex_output: str = open("mod.clsp.hex").read() 68 | assert "ff0133" in hex_output 69 | assert len(hex_output) <= 7 # With or without newline 70 | 71 | def test_curry(self): 72 | integer: int = 1 73 | hexadecimal = bytes.fromhex("aabbccddeeff") 74 | string: str = "hello" 75 | program = Program.to([2, 2, 3]) 76 | mod = Program.from_bytes(bytes.fromhex(self.serialized)) 77 | curried_mod: Program = mod.curry(integer, hexadecimal, string, program) 78 | 79 | runner = CliRunner() 80 | # Curry one of each kind of argument 81 | cmd: list[str] = [ 82 | "clsp", 83 | "curry", 84 | str(mod), 85 | "-a", 86 | str(integer), 87 | "-a", 88 | "0x" + hexadecimal.hex(), 89 | "-a", 90 | string, 91 | "-a", 92 | disassemble(program), 93 | ] 94 | 95 | result: Result = runner.invoke(cli, cmd) 96 | assert result.exit_code == 0 97 | assert disassemble(curried_mod) in result.output 98 | 99 | cmd.append("-x") 100 | result = runner.invoke(cli, cmd) 101 | assert result.exit_code == 0 102 | assert str(curried_mod) in result.output 103 | 104 | cmd.append("-H") 105 | result = runner.invoke(cli, cmd) 106 | assert result.exit_code == 0 107 | assert curried_mod.get_tree_hash().hex() in result.output 108 | 109 | def test_uncurry(self): 110 | hexadecimal = bytes.fromhex("aabbccddeeff") 111 | mod = Program.from_bytes(bytes.fromhex(self.serialized)) 112 | curried_mod: Program = mod.curry(hexadecimal) 113 | 114 | runner = CliRunner() 115 | # Curry one of each kind of argument 116 | cmd: list[str] = [ 117 | "clsp", 118 | "uncurry", 119 | str(curried_mod), 120 | ] 121 | 122 | result: Result = runner.invoke(cli, cmd) 123 | assert result.exit_code == 0 124 | assert disassemble(mod) in result.output 125 | assert disassemble(curried_mod) not in result.output 126 | 127 | cmd.append("-x") 128 | result = runner.invoke(cli, cmd) 129 | assert result.exit_code == 0 130 | assert str(mod) in result.output 131 | assert str(curried_mod) not in result.output 132 | 133 | cmd.append("-H") 134 | result = runner.invoke(cli, cmd) 135 | assert result.exit_code == 0 136 | assert mod.get_tree_hash().hex() in result.output 137 | 138 | # The following two functions aim to test every branch of the parse_program utility function between them 139 | def test_disassemble(self): 140 | runner = CliRunner() 141 | # Test the program passed in as a hex string 142 | result: Result = runner.invoke(cli, ["clsp", "disassemble", self.serialized]) 143 | assert result.exit_code == 0 144 | assert self.program in result.output 145 | # Test the program passed in as a hex file 146 | with runner.isolated_filesystem(): 147 | program_file = open("program.clvm.hex", "w") 148 | program_file.write(self.serialized) 149 | program_file.close() 150 | result = runner.invoke(cli, ["clsp", "disassemble", "program.clvm.hex"]) 151 | assert result.exit_code == 0 152 | assert self.program in result.output 153 | 154 | def test_treehash(self): 155 | # Test a program passed as a string 156 | runner = CliRunner() 157 | program: str = "(a 2 3)" 158 | program_as_mod: str = "(mod (arg . arg2) (a arg arg2))" 159 | program_hash: str = "530d1b3283c802be3a7bdb34b788c1898475ed76c89ecb2224e4b4f40c32d1a4" 160 | result: Result = runner.invoke(cli, ["clsp", "treehash", program]) 161 | assert result.exit_code == 0 162 | assert program_hash in result.output 163 | 164 | # Test a program passed in as a CLVM file 165 | filename: str = "program.clvm" 166 | with runner.isolated_filesystem(): 167 | program_file: IO = open(filename, "w") 168 | program_file.write(program) 169 | program_file.close() 170 | result = runner.invoke(cli, ["clsp", "treehash", filename]) 171 | assert result.exit_code == 0 172 | assert program_hash in result.output 173 | 174 | # Test a program passed in as a Chialisp file 175 | with runner.isolated_filesystem(): 176 | program_file: IO = open(filename, "w") 177 | program_file.write(program_as_mod) 178 | program_file.close() 179 | result = runner.invoke(cli, ["clsp", "treehash", filename]) 180 | assert result.exit_code == 0 181 | assert program_hash in result.output 182 | 183 | def test_cat_puzzle_hash(self): 184 | runner = CliRunner() 185 | args_bech32m = [ 186 | "xch18h6rmktpdgms23sqj009hxjwz7szmumwy257uzv8dnvqcuaz0ltqmu9ret", 187 | "-t", 188 | "0x7efa9f202cfd8e174e1376790232f1249e71fbe46dc428f7237a47d871a2b78b", 189 | ] 190 | expected_bech32m = "xch1wtlyxwdl99xk5war69jzxpafx4kmvcjrmevlxn4yet8r7cfc63tqww5qwg" 191 | args_hex = [ 192 | "0x3df43dd9616a3705460093de5b9a4e17a02df36e22a9ee09876cd80c73a27fd6", 193 | "-t", 194 | "0x7efa9f202cfd8e174e1376790232f1249e71fbe46dc428f7237a47d871a2b78b", 195 | ] 196 | expected_hex = "72fe4339bf294d6a3ba3d1642307a9356db66243de59f34ea4cace3f6138d456" 197 | args_usds = ["xch16ay8wdjtl8f58gml4vl5jw4vm6ychhu3lk9hddhykhcmt6l6599s9lrvqn", "-t", "USDSC"] 198 | expected_usds = "xch1hurndm0nx93epskq496rt25yf5ar070wzhcdtpf3rt5gx2vu97wq4q5g3k" 199 | 200 | result_bech32m: Result = runner.invoke(cli, ["clsp", "cat_puzzle_hash", *args_bech32m]) 201 | assert result_bech32m.exit_code == 0 202 | assert expected_bech32m in result_bech32m.output 203 | result_hex: Result = runner.invoke(cli, ["clsp", "cat_puzzle_hash", *args_hex]) 204 | assert result_hex.exit_code == 0 205 | assert expected_hex in result_hex.output 206 | result_usds: Result = runner.invoke(cli, ["clsp", "cat_puzzle_hash", *args_usds]) 207 | assert result_usds.exit_code == 0 208 | assert expected_usds in result_usds.output 209 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2025 Chia Network Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /cdv/cmds/rpc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import json 5 | from pprint import pprint 6 | 7 | import aiohttp 8 | import click 9 | from chia.cmds.cmds_util import format_bytes 10 | from chia.full_node.full_node_rpc_client import FullNodeRpcClient 11 | from chia.types.blockchain_format.coin import Coin 12 | from chia.types.coin_record import CoinRecord 13 | from chia.types.coin_spend import CoinSpend 14 | from chia.types.unfinished_header_block import UnfinishedHeaderBlock 15 | from chia.util.byte_types import hexstr_to_bytes 16 | from chia.util.config import load_config 17 | from chia.util.default_root import DEFAULT_ROOT_PATH 18 | from chia_rs import BlockRecord, FullBlock 19 | from chia_rs.sized_bytes import bytes32 20 | from chia_rs.sized_ints import uint16, uint64 21 | 22 | from cdv.cmds.chia_inspect import do_inspect_spend_bundle_cmd 23 | from cdv.cmds.util import fake_context 24 | 25 | """ 26 | These functions are untested because it is relatively basic code that would be very complex to test. 27 | Please be careful when making changes. 28 | """ 29 | 30 | 31 | @click.group("rpc", short_help="Make RPC requests to a Chia full node") 32 | def rpc_cmd() -> None: 33 | pass 34 | 35 | 36 | # Loading the client requires the standard chia root directory configuration that all of the chia commands rely on 37 | async def get_client() -> FullNodeRpcClient | None: 38 | try: 39 | config = load_config(DEFAULT_ROOT_PATH, "config.yaml") 40 | self_hostname = config["self_hostname"] 41 | full_node_rpc_port = config["full_node"]["rpc_port"] 42 | full_node_client: FullNodeRpcClient | None = await FullNodeRpcClient.create( 43 | self_hostname, uint16(full_node_rpc_port), DEFAULT_ROOT_PATH, config 44 | ) 45 | return full_node_client 46 | except Exception as e: 47 | if isinstance(e, aiohttp.ClientConnectorError): 48 | pprint(f"Connection error. Check if full node is running at {full_node_rpc_port}") 49 | else: 50 | pprint(f"Exception from 'harvester' {e}") 51 | return None 52 | 53 | 54 | @rpc_cmd.command("state", short_help="Gets the status of the blockchain (get_blockchain_state)") 55 | def rpc_state_cmd(): 56 | async def do_command(): 57 | try: 58 | node_client: FullNodeRpcClient = await get_client() 59 | state: dict = await node_client.get_blockchain_state() 60 | state["peak"] = state["peak"].to_json_dict() 61 | print(json.dumps(state, sort_keys=True, indent=4)) 62 | finally: 63 | node_client.close() 64 | await node_client.await_closed() 65 | 66 | asyncio.get_event_loop().run_until_complete(do_command()) 67 | 68 | 69 | @rpc_cmd.command("blocks", short_help="Gets blocks between two indexes (get_block(s))") 70 | @click.option("-hh", "--header-hash", help="The header hash of the block to get") 71 | @click.option("-s", "--start", help="The block index to start at (included)") 72 | @click.option("-e", "--end", help="The block index to end at (excluded)") 73 | def rpc_blocks_cmd(header_hash: str, start: int, end: int): 74 | async def do_command(): 75 | try: 76 | node_client: FullNodeRpcClient = await get_client() 77 | if header_hash: 78 | blocks: list[FullBlock] = [await node_client.get_block(hexstr_to_bytes(header_hash))] 79 | elif start and end: 80 | blocks: list[FullBlock] = await node_client.get_all_block(start, end) 81 | else: 82 | print("Invalid arguments specified") 83 | return 84 | print(json.dumps([block.to_json_dict() for block in blocks], sort_keys=True, indent=4)) 85 | finally: 86 | node_client.close() 87 | await node_client.await_closed() 88 | 89 | asyncio.get_event_loop().run_until_complete(do_command()) 90 | 91 | 92 | @rpc_cmd.command( 93 | "blockrecords", 94 | short_help="Gets block records between two indexes (get_block_record(s), get_block_record_by_height)", 95 | ) 96 | @click.option("-hh", "--header-hash", help="The header hash of the block to get") 97 | @click.option("-i", "--height", help="The height of the block to get") # This option is not in the standard RPC API 98 | @click.option("-s", "--start", help="The block index to start at (included)") 99 | @click.option("-e", "--end", help="The block index to end at (excluded)") 100 | def rpc_blockrecords_cmd(header_hash: str, height: int, start: int, end: int): 101 | async def do_command(): 102 | try: 103 | node_client: FullNodeRpcClient = await get_client() 104 | if header_hash: 105 | block_record: BlockRecord = await node_client.get_block_record(hexstr_to_bytes(header_hash)) 106 | block_records: list = block_record.to_json_dict() if block_record else [] 107 | elif height: 108 | block_record: BlockRecord = await node_client.get_block_record_by_height(height) 109 | block_records: list = block_record.to_json_dict() if block_record else [] 110 | elif start and end: 111 | block_records: list = await node_client.get_block_records(start, end) 112 | else: 113 | print("Invalid arguments specified") 114 | print(json.dumps(block_records, sort_keys=True, indent=4)) 115 | finally: 116 | node_client.close() 117 | await node_client.await_closed() 118 | 119 | asyncio.get_event_loop().run_until_complete(do_command()) 120 | 121 | 122 | # This maybe shouldn't exist, I have yet to see it return anything but an empty list 123 | @rpc_cmd.command( 124 | "unfinished", 125 | short_help="Returns the current unfinished header blocks (get_unfinished_block_headers)", 126 | ) 127 | def rpc_unfinished_cmd(): 128 | async def do_command(): 129 | try: 130 | node_client: FullNodeRpcClient = await get_client() 131 | header_blocks: list[UnfinishedHeaderBlock] = await node_client.get_unfinished_block_headers() 132 | print(json.dumps([block.to_json_dict() for block in header_blocks], sort_keys=True, indent=4)) 133 | finally: 134 | node_client.close() 135 | await node_client.await_closed() 136 | 137 | asyncio.get_event_loop().run_until_complete(do_command()) 138 | 139 | 140 | # Running this command plain should return the current total netspace estimation 141 | @rpc_cmd.command( 142 | "space", 143 | short_help="Gets the netspace of the network between two blocks (get_network_space)", 144 | ) 145 | @click.option("-old", "--older", help="The header hash of the older block") # Default block 0 146 | @click.option("-new", "--newer", help="The header hash of the newer block") # Default block 0 147 | @click.option("-s", "--start", help="The height of the block to start at") # Default latest block 148 | @click.option("-e", "--end", help="The height of the block to end at") # Default latest block 149 | def rpc_space_cmd(older: str, newer: str, start: int, end: int): 150 | async def do_command(): 151 | try: 152 | node_client: FullNodeRpcClient = await get_client() 153 | 154 | if (older and start) or (newer and end): 155 | pprint("Invalid arguments specified.") 156 | return 157 | elif not any([older, start, newer, end]): 158 | pprint(format_bytes((await node_client.get_blockchain_state())["space"])) 159 | return 160 | else: 161 | if start: 162 | start_hash: bytes32 = (await node_client.get_block_record_by_height(start)).header_hash 163 | elif older: 164 | start_hash: bytes32 = hexstr_to_bytes(older) 165 | else: 166 | start_hash: bytes32 = (await node_client.get_block_record_by_height(0)).header_hash 167 | 168 | if end: 169 | end_hash: bytes32 = (await node_client.get_block_record_by_height(end)).header_hash 170 | elif newer: 171 | end_hash: bytes32 = hexstr_to_bytes(newer) 172 | else: 173 | end_hash: bytes32 = ( 174 | await node_client.get_block_record_by_height( 175 | (await node_client.get_blockchain_state())["peak"].height 176 | ) 177 | ).header_hash 178 | 179 | netspace: uint64 | None = await node_client.get_network_space(start_hash, end_hash) 180 | if netspace: 181 | pprint(format_bytes(netspace)) 182 | else: 183 | pprint("Invalid block range specified") 184 | 185 | finally: 186 | node_client.close() 187 | await node_client.await_closed() 188 | 189 | asyncio.get_event_loop().run_until_complete(do_command()) 190 | 191 | 192 | @rpc_cmd.command( 193 | "blockcoins", 194 | short_help="Gets the coins added and removed for a specific header hash (get_additions_and_removals)", 195 | ) 196 | @click.argument("headerhash", nargs=1, required=True) 197 | def rpc_addrem_cmd(headerhash: str): 198 | async def do_command(): 199 | try: 200 | node_client: FullNodeRpcClient = await get_client() 201 | additions, removals = await node_client.get_additions_and_removals(hexstr_to_bytes(headerhash)) 202 | additions: list[dict] = [rec.to_json_dict() for rec in additions] 203 | removals: list[dict] = [rec.to_json_dict() for rec in removals] 204 | print(json.dumps({"additions": additions, "removals": removals}, sort_keys=True, indent=4)) 205 | finally: 206 | node_client.close() 207 | await node_client.await_closed() 208 | 209 | asyncio.get_event_loop().run_until_complete(do_command()) 210 | 211 | 212 | @rpc_cmd.command( 213 | "blockspends", 214 | short_help="Gets the puzzle and solution for a coin spent at the specified block height (get_puzzle_and_solution)", 215 | ) 216 | @click.option("-id", "--coinid", required=True, help="The id of the coin that was spent") 217 | @click.option( 218 | "-h", 219 | "--block-height", 220 | required=True, 221 | type=int, 222 | help="The block height in which the coin was spent", 223 | ) 224 | def rpc_puzsol_cmd(coinid: str, block_height: int): 225 | async def do_command(): 226 | try: 227 | node_client: FullNodeRpcClient = await get_client() 228 | coin_spend: CoinSpend | None = await node_client.get_puzzle_and_solution( 229 | bytes.fromhex(coinid), block_height 230 | ) 231 | print(json.dumps(coin_spend.to_json_dict(), sort_keys=True, indent=4)) 232 | finally: 233 | node_client.close() 234 | await node_client.await_closed() 235 | 236 | asyncio.get_event_loop().run_until_complete(do_command()) 237 | 238 | 239 | @rpc_cmd.command("pushtx", short_help="Pushes a spend bundle to the network (push_tx)") 240 | @click.argument("spendbundles", nargs=-1, required=True) 241 | def rpc_pushtx_cmd(spendbundles: tuple[str]): 242 | async def do_command(): 243 | try: 244 | node_client: FullNodeRpcClient = await get_client() 245 | # It loads the spend bundle using cdv inspect 246 | for bundle in do_inspect_spend_bundle_cmd(fake_context(), spendbundles, print_results=False): 247 | try: 248 | result: dict = await node_client.push_tx(bundle) 249 | print(json.dumps(result, sort_keys=True, indent=4)) 250 | except ValueError as e: 251 | pprint(str(e)) 252 | finally: 253 | node_client.close() 254 | await node_client.await_closed() 255 | 256 | asyncio.get_event_loop().run_until_complete(do_command()) 257 | 258 | 259 | @rpc_cmd.command( 260 | "mempool", 261 | short_help="Gets items that are currently sitting in the mempool (get_(all_)mempool_*)", 262 | ) 263 | @click.option( 264 | "-txid", 265 | "--transaction-id", 266 | help="The ID of a spend bundle that is sitting in the mempool", 267 | ) 268 | @click.option("--ids-only", is_flag=True, help="Only show the IDs of the retrieved spend bundles") 269 | def rpc_mempool_cmd(transaction_id: str, ids_only: bool): 270 | async def do_command(): 271 | try: 272 | node_client: FullNodeRpcClient = await get_client() 273 | if transaction_id: 274 | items = { 275 | transaction_id: await node_client.get_mempool_item_by_tx_id(bytes32.from_hexstr(transaction_id)) 276 | } 277 | else: 278 | b_items: dict = await node_client.get_all_mempool_items() 279 | items = {} 280 | for key, value in b_items.items(): 281 | items[key.hex()] = value 282 | 283 | if ids_only: 284 | print(json.dumps(list(items.keys()), sort_keys=True, indent=4)) 285 | else: 286 | print(json.dumps(items, sort_keys=True, indent=4)) 287 | finally: 288 | node_client.close() 289 | await node_client.await_closed() 290 | 291 | asyncio.get_event_loop().run_until_complete(do_command()) 292 | 293 | 294 | @rpc_cmd.command( 295 | "coinrecords", 296 | short_help="Gets coin records by a specified information (get_coin_records_by_*)", 297 | ) 298 | @click.argument("values", nargs=-1, required=True) 299 | @click.option("--by", help="The property to use (id, puzzlehash, parentid, hint)") 300 | @click.option( 301 | "-nd", 302 | "--as-name-dict", 303 | is_flag=True, 304 | help="Return the records as a dictionary with ids as the keys", 305 | ) 306 | @click.option( 307 | "-ou", 308 | "--only-unspent", 309 | is_flag=True, 310 | help="Exclude already spent coins from the search", 311 | ) 312 | @click.option("-s", "--start", type=int, help="The block index to start at (included)") 313 | @click.option("-e", "--end", type=int, help="The block index to end at (excluded)") 314 | def rpc_coinrecords_cmd(values: tuple[str], by: str, as_name_dict: bool, **kwargs): 315 | async def do_command(): 316 | try: 317 | node_client: FullNodeRpcClient = await get_client() 318 | coin_info: list[bytes32] = [bytes32.from_hexstr(hexstr) for hexstr in values] 319 | if by in {"name", "id"}: 320 | # TODO: When a by-multiple-names rpc exits, use it instead 321 | coin_records: list[CoinRecord] = await node_client.get_coin_records_by_names(coin_info, **kwargs) 322 | elif by in {"puzhash", "puzzle_hash", "puzzlehash"}: 323 | coin_records: list[CoinRecord] = await node_client.get_coin_records_by_puzzle_hashes( 324 | coin_info, **kwargs 325 | ) 326 | elif by in { 327 | "parent_id", 328 | "parent_info", 329 | "parent_coin_info", 330 | "parentid", 331 | "parentinfo", 332 | "parent", 333 | "pid", 334 | }: 335 | coin_records: list[CoinRecord] = await node_client.get_coin_records_by_parent_ids(coin_info, **kwargs) 336 | elif by in {"hint"}: 337 | hint = next(iter(coin_info)) 338 | coin_records: list[CoinRecord] = await node_client.get_coin_records_by_hint(hint=hint, **kwargs) 339 | else: 340 | print(f"Unaware of property {by}.") 341 | return 342 | 343 | coin_record_dicts: list[dict] = [rec.to_json_dict() for rec in coin_records] 344 | 345 | if as_name_dict: 346 | cr_dict = {} 347 | for record in coin_record_dicts: 348 | cr_dict[Coin.from_json_dict(record["coin"]).name().hex()] = record 349 | print(json.dumps(cr_dict, sort_keys=True, indent=4)) 350 | else: 351 | print(json.dumps(coin_record_dicts, sort_keys=True, indent=4)) 352 | finally: 353 | node_client.close() 354 | await node_client.await_closed() 355 | 356 | # Have to rename the kwargs as they will be directly passed to the RPC client 357 | kwargs["include_spent_coins"] = not kwargs.pop("only_unspent") 358 | kwargs["start_height"] = kwargs.pop("start") 359 | kwargs["end_height"] = kwargs.pop("end") 360 | asyncio.get_event_loop().run_until_complete(do_command()) 361 | -------------------------------------------------------------------------------- /tests/cmds/test_inspect.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from pathlib import Path 5 | 6 | from click.testing import CliRunner, Result 7 | 8 | from cdv.cmds.cli import cli 9 | 10 | EMPTY_SIG = ( 11 | "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" 12 | "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" 13 | ) 14 | 15 | 16 | class TestInspectCommands: 17 | def test_any(self): 18 | runner = CliRunner() 19 | 20 | # Try to inspect a program 21 | result: Result = runner.invoke(cli, ["inspect", "any", "ff0101"]) 22 | assert result.exit_code == 0 23 | assert "guess" not in result.output 24 | 25 | # Try to inspect a private key 26 | result = runner.invoke( 27 | cli, 28 | [ 29 | "inspect", 30 | "any", 31 | "05ec9428fc2841a79e96631a633b154b57a45311c0602269a6500732093a52cd", 32 | ], 33 | ) 34 | assert result.exit_code == 0 35 | assert "guess" not in result.output 36 | 37 | # Try to inspect a public key 38 | result = runner.invoke( 39 | cli, 40 | [ 41 | "inspect", 42 | "any", 43 | "b364a088d9df9423e54bff4c62e4bd854445fb8f5b8f6d80dea06773a4f828734a3a75318b180364ca8468836f0742db", 44 | ], 45 | ) 46 | assert result.exit_code == 0 47 | assert "guess" not in result.output 48 | 49 | # Try to inspect an aggsig 50 | result = runner.invoke( 51 | cli, 52 | [ 53 | "inspect", 54 | "any", 55 | EMPTY_SIG, 56 | ], 57 | ) 58 | assert result.exit_code == 0 59 | assert "guess" not in result.output 60 | 61 | for class_type in ["coinrecord", "coin", "spendbundle", "spend"]: 62 | valid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}.json") 63 | invalid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}_invalid.json") 64 | metadata_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}_metadata.json") 65 | valid_json: dict = json.loads(open(valid_json_path).read()) 66 | metadata_json: dict = json.loads(open(metadata_path).read()) 67 | 68 | # Try to load the invalid and make sure it fails 69 | result = runner.invoke(cli, ["inspect", "any", str(invalid_json_path)]) 70 | assert result.exit_code == 0 71 | # If this succeeds, there should be no file name, if it fails, the file name should be output as info 72 | assert str(invalid_json_path) in result.output 73 | 74 | # Try to load the valid json 75 | result = runner.invoke(cli, ["inspect", "any", json.dumps(valid_json)]) 76 | assert result.exit_code == 0 77 | for key, value in valid_json.items(): 78 | key_type = type(value) 79 | if (key_type is not dict) and (key_type is not list): 80 | assert (str(value) in result.output) or (str(value).lower() in result.output) 81 | 82 | # Try to load bytes 83 | if class_type != "coin": 84 | valid_hex_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}.hex") 85 | 86 | # From a file 87 | result = runner.invoke(cli, ["inspect", "--json", "any", str(valid_hex_path)]) 88 | assert result.exit_code == 0 89 | assert '"coin":' in result.output 90 | 91 | # From a string 92 | valid_hex: str = open(valid_hex_path).read() 93 | result = runner.invoke(cli, ["inspect", "--json", "any", valid_hex]) 94 | assert result.exit_code == 0 95 | assert '"coin":' in result.output 96 | 97 | # Make sure the ID calculation is correct 98 | result = runner.invoke(cli, ["inspect", "--id", "any", str(valid_json_path)]) 99 | assert result.exit_code == 0 100 | assert metadata_json["id"] in result.output 101 | 102 | # Make sure the bytes encoding is correct 103 | result = runner.invoke(cli, ["inspect", "--bytes", "any", str(valid_json_path)]) 104 | assert result.exit_code == 0 105 | assert metadata_json["bytes"] in result.output 106 | 107 | # Make sure the type guessing is correct 108 | result = runner.invoke(cli, ["inspect", "--type", "any", str(valid_json_path)]) 109 | assert result.exit_code == 0 110 | assert metadata_json["type"] in result.output 111 | 112 | def test_coins(self): 113 | pid: str = "0x0000000000000000000000000000000000000000000000000000000000000000" 114 | ph: str = "0000000000000000000000000000000000000000000000000000000000000000" 115 | amount: str = "0" 116 | id: str = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" 117 | 118 | runner = CliRunner() 119 | result: Result = runner.invoke(cli, ["inspect", "--id", "coins", "-pid", pid, "-ph", ph, "-a", amount]) 120 | assert result.exit_code == 0 121 | assert id in result.output 122 | 123 | def test_spends(self): 124 | coin_path = Path(__file__).parent.joinpath("object_files/coins/coin.json") 125 | pid: str = "0x0000000000000000000000000000000000000000000000000000000000000000" 126 | ph: str = "0000000000000000000000000000000000000000000000000000000000000000" 127 | amount: str = "0" 128 | id: str = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" 129 | puzzle_reveal: str = "01" 130 | solution: str = "80" 131 | cost: str = "564064" 132 | modified_cost: str = "552064" 133 | 134 | runner = CliRunner() 135 | 136 | # Specify the coin file 137 | result: Result = runner.invoke( 138 | cli, 139 | [ 140 | "inspect", 141 | "--id", 142 | "spends", 143 | "-c", 144 | str(coin_path), 145 | "-pr", 146 | puzzle_reveal, 147 | "-s", 148 | solution, 149 | ], 150 | ) 151 | assert result.exit_code == 0 152 | assert id in result.output 153 | 154 | # Specify all of the arguments 155 | base_command: list[str] = [ 156 | "inspect", 157 | "--id", 158 | "spends", 159 | "-pid", 160 | pid, 161 | "-ph", 162 | ph, 163 | "-a", 164 | amount, 165 | "-pr", 166 | puzzle_reveal, 167 | "-s", 168 | solution, 169 | ] 170 | result = runner.invoke(cli, base_command) 171 | assert result.exit_code == 0 172 | assert id in result.output 173 | 174 | # Ask for the cost 175 | base_command.append("-ec") 176 | result = runner.invoke(cli, base_command) 177 | assert result.exit_code == 0 178 | assert id in result.output 179 | assert cost in result.output 180 | 181 | # Change the cost per byte 182 | base_command.append("--ignore-byte-cost") 183 | result = runner.invoke(cli, base_command) 184 | assert result.exit_code == 0 185 | assert id in result.output 186 | assert modified_cost in result.output 187 | 188 | def test_spendbundles(self): 189 | spend_path = Path(__file__).parent.joinpath("object_files/spends/spend.json") 190 | spend_path_2 = Path(__file__).parent.joinpath("object_files/spends/spend_2.json") 191 | pubkey: str = "80df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0" 192 | signable_data: str = ( 193 | "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4bccd5bb71183532bff220" 194 | "ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" 195 | ) 196 | agg_sig: str = ( 197 | "b83fe374efbc5776735df7cbfb7e27ede5079b41cd282091450e4de21c4b772e254ce906508834b0c2d" 198 | "cd3d58c47a96914c782f0baf8eaff7ece3b070d2035cd878f744deadcd6c6625c1d0a1b418437ee3f25c2df08ffe08bdfe06b8a83b514" 199 | ) 200 | id_no_sig: str = "3fc441c1048a4e0b9fd1648d7647fdebd220cf7dd51b6967dcaf76f7043e83d6" 201 | id_with_sig: str = "7d6f0da915deed117ad5589aa8bd6bf99beb69f48724b14b2134f6f8af6d8afc" 202 | network_modifier: str = "testnet11" 203 | modified_signable_data: str = ( 204 | "2458e8f2a1f78f0a591feb75aebecaaa81076e4290894b1c445cc32953604db08937a90eb5185a" 205 | "9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615" 206 | ) 207 | cost: str = "6684108" 208 | modified_cost: str = "6660108" 209 | 210 | runner = CliRunner() 211 | 212 | # Build with only the spends 213 | result: Result = runner.invoke( 214 | cli, 215 | [ 216 | "inspect", 217 | "--id", 218 | "spendbundles", 219 | "-s", 220 | str(spend_path), 221 | "-s", 222 | str(spend_path_2), 223 | ], 224 | ) 225 | assert result.exit_code == 0 226 | assert id_no_sig in result.output 227 | 228 | # Build with the aggsig as well 229 | base_command: list[str] = [ 230 | "inspect", 231 | "--id", 232 | "spendbundles", 233 | "-s", 234 | str(spend_path), 235 | "-s", 236 | str(spend_path_2), 237 | "-as", 238 | agg_sig, 239 | ] 240 | result = runner.invoke(cli, base_command) 241 | assert result.exit_code == 0 242 | assert id_with_sig in result.output 243 | 244 | # Test that debugging info comes up 245 | base_command.append("-db") 246 | result = runner.invoke(cli, base_command) 247 | assert result.exit_code == 0 248 | assert "Debugging Information" in result.output 249 | 250 | # Make sure our signable data comes out 251 | base_command.append("-sd") 252 | result = runner.invoke(cli, base_command) 253 | assert result.exit_code == 0 254 | assert pubkey in result.output 255 | assert result.output.count(signable_data) == 2 256 | 257 | # Try a different network for different signable data 258 | base_command.append("-n") 259 | base_command.append(network_modifier) 260 | result = runner.invoke(cli, base_command) 261 | assert result.exit_code == 0 262 | assert modified_signable_data in result.output 263 | 264 | # Output the execution cost 265 | base_command.append("-ec") 266 | result = runner.invoke(cli, base_command) 267 | assert result.exit_code == 0 268 | assert cost in result.output 269 | 270 | # Try a new cost per bytes 271 | base_command.append("--ignore-byte-cost") 272 | result = runner.invoke(cli, base_command) 273 | assert result.exit_code == 0 274 | assert modified_cost in result.output 275 | 276 | # Try to use it the programmatic way (like cdv rpc pushtx does) 277 | from cdv.cmds.chia_inspect import do_inspect_spend_bundle_cmd 278 | from cdv.cmds.util import fake_context 279 | 280 | bundle_path = Path(__file__).parent.joinpath("object_files/spendbundles/spendbundle.json") 281 | assert len(do_inspect_spend_bundle_cmd(fake_context(), [str(bundle_path)], print_results=False)) > 0 282 | 283 | def test_coinrecords(self): 284 | coin_path = Path(__file__).parent.joinpath("object_files/coins/coin.json") 285 | pid: str = "0x0000000000000000000000000000000000000000000000000000000000000000" 286 | ph: str = "0000000000000000000000000000000000000000000000000000000000000000" 287 | amount: str = "0" 288 | id: str = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" 289 | coinbase: str = "False" 290 | confirmed_block_index: str = "500" 291 | spent_block_index: str = "501" 292 | timestamp: str = "909469800" 293 | 294 | runner = CliRunner() 295 | 296 | # Try to load it from a file 297 | record_path = Path(__file__).parent.joinpath("object_files/coinrecords/coinrecord.json") 298 | result: Result = runner.invoke(cli, ["inspect", "coinrecords", str(record_path)]) 299 | assert result.exit_code == 0 300 | assert '"coin":' in result.output 301 | 302 | # Specify the coin file 303 | result = runner.invoke( 304 | cli, 305 | [ 306 | "inspect", 307 | "--id", 308 | "coinrecords", 309 | "-c", 310 | str(coin_path), 311 | "-cb", 312 | coinbase, 313 | "-ci", 314 | confirmed_block_index, 315 | "-si", 316 | spent_block_index, 317 | "-t", 318 | timestamp, 319 | ], 320 | ) 321 | assert result.exit_code == 0 322 | assert id in result.output 323 | 324 | # Specify all of the arguments 325 | result = runner.invoke( 326 | cli, 327 | [ 328 | "inspect", 329 | "--id", 330 | "coinrecords", 331 | "-pid", 332 | pid, 333 | "-ph", 334 | ph, 335 | "-a", 336 | amount, 337 | "-cb", 338 | coinbase, 339 | "-ci", 340 | confirmed_block_index, 341 | "-si", 342 | spent_block_index, 343 | "-t", 344 | timestamp, 345 | ], 346 | ) 347 | assert result.exit_code == 0 348 | assert id in result.output 349 | 350 | def test_programs(self): 351 | program: str = "ff0101" 352 | id: str = "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" 353 | 354 | runner = CliRunner() 355 | 356 | result: Result = runner.invoke(cli, ["inspect", "--id", "programs", program]) 357 | assert result.exit_code == 0 358 | assert id in result.output 359 | 360 | def test_keys(self): 361 | mnemonic: str = ( 362 | "spend spend spend spend spend spend spend spend spend spend spend spend spend spend spend spend" 363 | " spend spend spend spend spend spend spend spend" 364 | ) 365 | sk: str = "1ef0ff42df2fdd4472312e033f555c569d18b85ba0d9f1b09ed87b254dc18a8e" 366 | pk: str = "ae6c7589432cb60a00d84fc83971f50a98fd728863d3ceb189300f2f80d6839e9a2e761ef6cdce809caee83a4e73b623" 367 | hd_modifier: str = "m/12381/8444/0/0" 368 | type_modifier: str = "farmer" # Should be same as above HD Path 369 | farmer_sk: str = "6a97995a8b35c69418ad60152a5e1c9a32d159bcb7c343c5ccf83c71e4df2038" 370 | synthetic_sk: str = "4272b71ba1c628948e308148e92c1a9b24a785d52f604610a436d2088f72d578" 371 | ph_modifier: str = "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" 372 | modified_synthetic_sk: str = "405d969856846304eec6b243d810665cb3b7e94b56747b87e8e5597948ba1da6" 373 | 374 | runner = CliRunner() 375 | 376 | # Build the key from secret key 377 | result: Result = runner.invoke(cli, ["inspect", "keys", "-sk", sk]) 378 | assert result.exit_code == 0 379 | assert sk in result.output 380 | assert pk in result.output 381 | key_output: str = result.output 382 | 383 | # Build the key from mnemonic 384 | result = runner.invoke(cli, ["inspect", "keys", "-m", mnemonic]) 385 | assert result.exit_code == 0 386 | assert result.output == key_output 387 | 388 | # Use only the public key 389 | result = runner.invoke(cli, ["inspect", "keys", "-pk", pk]) 390 | assert result.exit_code == 0 391 | assert result.output in key_output 392 | 393 | # Generate a random one 394 | result = runner.invoke(cli, ["inspect", "keys", "--random"]) 395 | assert result.exit_code == 0 396 | assert "Secret Key" in result.output 397 | assert "Public Key" in result.output 398 | 399 | # Check the HD derivation is working 400 | result = runner.invoke(cli, ["inspect", "keys", "-sk", sk, "-hd", hd_modifier]) 401 | assert result.exit_code == 0 402 | assert farmer_sk in result.output 403 | 404 | # Check the type derivation is working 405 | result = runner.invoke(cli, ["inspect", "keys", "-sk", sk, "-t", type_modifier]) 406 | assert result.exit_code == 0 407 | assert farmer_sk in result.output 408 | 409 | # Check that synthetic calculation is working 410 | result = runner.invoke(cli, ["inspect", "keys", "-sk", sk, "-sy"]) 411 | assert result.exit_code == 0 412 | assert synthetic_sk in result.output 413 | 414 | # Check that using a non default puzzle hash is working 415 | result = runner.invoke(cli, ["inspect", "keys", "-sk", sk, "-sy", "-ph", ph_modifier]) 416 | assert result.exit_code == 0 417 | assert modified_synthetic_sk in result.output 418 | 419 | def test_signatures(self): 420 | secret_key_1: str = "70432627e84c13c1a6e6007bf6d9a7a0342018fdef7fc911757aad5a6929d20a" 421 | secret_key_2: str = "0f01f7f68935f8594548bca3892fec419c6b2aa7cff54c3353a2e9b1011f09c7" 422 | text_message: str = "cafe food" 423 | bytes_message: str = "0xcafef00d" 424 | extra_signature: str = ( 425 | "b5d4e653ec9a737d19abe9af7050d37b0f464f9570ec66a8457fbdabdceb50a77c6610eb442ed1e4ace39d9" 426 | "ecc6d40560de239c1c8f7a115e052438385d594be7394df9287cf30c3254d39f0ae21daefc38d3d07ba3e373628bf8ed73f074a80" 427 | ) 428 | final_signature: str = ( 429 | "b7a6ab2c825068eb40298acab665f95c13779e828d900b8056215b54e47d8b8314e8b61fbb9c98a23ef8a1" 430 | "34155a35b109ba284bd5f1f90f96e0d41427132b3ca6a83faae0806daa632ee6b1602a0b4bad92f2743fdeb452822f0599dfa147c0" 431 | ) 432 | 433 | runner = CliRunner() 434 | 435 | # Test that an empty command returns an empty signature 436 | result = runner.invoke(cli, ["inspect", "signatures"]) 437 | assert result.exit_code == 0 438 | assert EMPTY_SIG in result.output 439 | 440 | # Test a complex signature calculation 441 | result = runner.invoke( 442 | cli, 443 | [ 444 | "inspect", 445 | "signatures", 446 | "-sk", 447 | secret_key_1, 448 | "-t", 449 | text_message, 450 | "-sk", 451 | secret_key_2, 452 | "-b", 453 | bytes_message, 454 | "-sig", 455 | extra_signature, 456 | ], 457 | ) 458 | assert result.exit_code == 0 459 | assert final_signature in result.output 460 | -------------------------------------------------------------------------------- /cdv/test/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import binascii 4 | import contextlib 5 | import datetime 6 | import struct 7 | from collections.abc import AsyncIterator 8 | from contextlib import asynccontextmanager 9 | 10 | import pytimeparse # type: ignore[import-untyped] 11 | from chia._tests.util.spend_sim import SimClient, SpendSim 12 | from chia.consensus.condition_tools import ConditionOpcode 13 | from chia.consensus.default_constants import DEFAULT_CONSTANTS 14 | from chia.types.blockchain_format.coin import Coin 15 | from chia.types.blockchain_format.program import Program 16 | from chia.types.coin_record import CoinRecord 17 | from chia.types.coin_spend import CoinSpend, make_spend 18 | from chia.util.hash import std_hash 19 | from chia.wallet.derive_keys import master_sk_to_wallet_sk 20 | from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( # standard_transaction 21 | DEFAULT_HIDDEN_PUZZLE_HASH, 22 | calculate_synthetic_secret_key, 23 | puzzle_for_pk, 24 | ) 25 | from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey, SpendBundle 26 | from chia_rs.sized_bytes import bytes32 27 | from chia_rs.sized_ints import uint32, uint64 28 | from typing_extensions import Self 29 | 30 | from cdv.util.keys import private_key_for_index, public_key_for_index 31 | from cdv.util.sign_coin_spends import sign_coin_spends 32 | 33 | duration_div = 86400.0 34 | block_time = (600.0 / 32.0) / duration_div 35 | # Allowed subdivisions of 1 coin 36 | 37 | 38 | class SpendResult: 39 | def __init__(self, result: dict): 40 | """Constructor for internal use. 41 | 42 | error - a string describing the error or None 43 | result - the raw result from Network::push_tx 44 | outputs - a list of new Coin objects surviving the transaction 45 | """ 46 | self.result = result 47 | if "error" in result: 48 | self.error: str | None = result["error"] 49 | self.outputs: list[Coin] = [] 50 | else: 51 | self.error = None 52 | self.outputs = result["additions"] 53 | 54 | def find_standard_coins(self, puzzle_hash: bytes32) -> list[Coin]: 55 | """Given a Wallet's puzzle_hash, find standard coins usable by it. 56 | 57 | These coins are recognized as changing the Wallet's chia balance and are 58 | usable for any purpose.""" 59 | return list(filter(lambda x: x.puzzle_hash == puzzle_hash, self.outputs)) 60 | 61 | 62 | class CoinWrapper: 63 | """A class that provides some useful methods on coins.""" 64 | 65 | def __init__(self, parent_hash: bytes32, amount: int, source: Program): 66 | """Given parent, puzzle_hash and amount, give an object representing the coin""" 67 | self.coin = Coin(parent_hash, source.get_tree_hash(), uint64(amount)) 68 | self.source = source 69 | self.amount = self.coin.amount 70 | self.puzzle_hash = self.coin.puzzle_hash 71 | self.parent_coin_info = self.coin.parent_coin_info 72 | 73 | def name(self) -> bytes32: 74 | """Return the name / id of this coin""" 75 | return self.coin.name() 76 | 77 | def puzzle(self) -> Program: 78 | """Return the program that unlocks this coin""" 79 | return self.source 80 | 81 | def smart_coin(self) -> SmartCoinWrapper: 82 | """Return a smart coin object wrapping this coin's program""" 83 | return SmartCoinWrapper(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, self.source) 84 | 85 | @classmethod 86 | def from_coin(cls, coin: Coin, puzzle: Program) -> CoinWrapper: 87 | return cls( 88 | coin.parent_coin_info, 89 | coin.amount, 90 | puzzle, 91 | ) 92 | 93 | def create_standard_spend(self, priv: PrivateKey, conditions: list[list]): 94 | delegated_puzzle_solution = Program.to((1, conditions)) 95 | solution = Program.to([[], delegated_puzzle_solution, []]) 96 | 97 | coin_spend_object = make_spend( 98 | self.coin, 99 | self.puzzle(), 100 | solution, 101 | ) 102 | 103 | # Create a signature for each of these. We'll aggregate them at the end. 104 | signature: G2Element = AugSchemeMPL.sign( 105 | calculate_synthetic_secret_key(priv, DEFAULT_HIDDEN_PUZZLE_HASH), 106 | (delegated_puzzle_solution.get_tree_hash() + self.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), 107 | ) 108 | 109 | return coin_spend_object, signature 110 | 111 | 112 | # We have two cases for coins: 113 | # - Wallet coins which contribute to the "wallet balance" of the user. 114 | # They enable a user to "spend money" and "take actions on the network" 115 | # that have monetary value. 116 | # 117 | # - Smart coins which either lock value or embody information and 118 | # services. These also contain a chia balance but are used for purposes 119 | # other than a fungible, liquid, spendable resource. They should not show 120 | # up in a "wallet" in the same way. We should use them by locking value 121 | # into wallet coins. We should ensure that value contained in a smart coin 122 | # coin is never destroyed. 123 | class SmartCoinWrapper: 124 | def __init__(self, genesis_challenge: bytes32, source: Program): 125 | """A wrapper for a smart coin carrying useful methods for interacting with chia.""" 126 | self.genesis_challenge = genesis_challenge 127 | self.source = source 128 | 129 | def puzzle(self) -> Program: 130 | """Give this smart coin's program""" 131 | return self.source 132 | 133 | def puzzle_hash(self) -> bytes32: 134 | """Give this smart coin's puzzle hash""" 135 | return self.source.get_tree_hash() 136 | 137 | def custom_coin(self, parent: Coin, amt: uint64) -> CoinWrapper: 138 | """Given a parent and an amount, create the Coin object representing this 139 | smart coin as it would exist post launch""" 140 | return CoinWrapper(parent.name(), amt, self.source) 141 | 142 | 143 | # Used internally to accumulate a search for coins we can combine to the 144 | # target amount. 145 | # Result is the smallest set of coins whose sum of amounts is greater 146 | # than target_amount. 147 | class CoinPairSearch: 148 | def __init__(self, target_amount: uint64): 149 | self.target = target_amount 150 | self.total: uint64 = uint64(0) 151 | self.max_coins: list[Coin] = [] 152 | 153 | def get_result(self) -> tuple[list[Coin], uint64]: 154 | return self.max_coins, self.total 155 | 156 | def insort(self, coin: Coin): 157 | for i in range(len(self.max_coins)): 158 | if self.max_coins[i].amount < coin.amount: 159 | self.max_coins.insert(i, coin) 160 | break 161 | else: 162 | self.max_coins.append(coin) 163 | 164 | def process_coin_for_combine_search(self, coin: Coin): 165 | self.total = uint64(self.total + coin.amount) 166 | if len(self.max_coins) == 0: 167 | self.max_coins.append(coin) 168 | else: 169 | self.insort(coin) 170 | while ( 171 | (len(self.max_coins) > 0) 172 | and (self.total - self.max_coins[-1].amount >= self.target) 173 | and ((self.total - self.max_coins[-1].amount > 0) or (len(self.max_coins) > 1)) 174 | ): 175 | self.total = uint64(self.total - self.max_coins[-1].amount) 176 | self.max_coins = self.max_coins[:-1] 177 | 178 | 179 | # A basic wallet that knows about standard coins. 180 | # We can use this to track our balance as an end user and keep track of 181 | # chia that is released by smart coins, if the smart coins interact 182 | # meaningfully with them, as many likely will. 183 | class Wallet: 184 | def __init__(self, parent: Network, name: str, key_idx: int): 185 | """Internal use constructor, use Network::make_wallet 186 | 187 | Fields: 188 | parent - The Network object that created this Wallet 189 | name - The textural name of the actor 190 | pk_ - The actor's public key 191 | sk_ - The actor's private key 192 | usable_coins - Standard coins spendable by this actor 193 | puzzle - A program for creating this actor's standard coin 194 | puzzle_hash - The puzzle hash for this actor's standard coin 195 | pk_to_sk_dict - a dictionary for retrieving the secret keys when presented with the corresponding public key 196 | """ 197 | 198 | self.generator_pk_ = public_key_for_index(key_idx) 199 | self.generator_sk_ = private_key_for_index(key_idx) 200 | 201 | self.parent = parent 202 | self.name = name 203 | 204 | # Use an indexed key off the main key. 205 | self.sk_ = master_sk_to_wallet_sk(self.generator_sk_, uint32(0)) 206 | self.pk_ = self.sk_.get_g1() 207 | 208 | self.usable_coins: dict[bytes32, Coin | CoinWrapper] = {} 209 | self.puzzle: Program = puzzle_for_pk(self.pk()) 210 | self.puzzle_hash: bytes32 = self.puzzle.get_tree_hash() 211 | 212 | synth_sk: PrivateKey = calculate_synthetic_secret_key(self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH) 213 | self.pk_to_sk_dict: dict[str, PrivateKey] = { 214 | str(self.pk_): self.sk_, 215 | str(synth_sk.get_g1()): synth_sk, 216 | } 217 | 218 | def __repr__(self) -> str: 219 | return f"" 220 | 221 | # Wallet RPC methods 222 | async def get_public_keys(self): 223 | (synthetic_fingerprint,) = struct.unpack(" PrivateKey: 240 | assert str(pk) in self.pk_to_sk_dict 241 | return self.pk_to_sk_dict[str(pk)] 242 | 243 | def sk_for_puzzle_hash(self, puzzle_hash: bytes32) -> PrivateKey | None: 244 | """ 245 | This method is a stub, required for the sign_coin_spends method in chia wallet. 246 | """ 247 | return None 248 | 249 | def compute_combine_action( 250 | self, amt: uint64, actions: list, usable_coins: dict[bytes32, Coin | CoinWrapper] 251 | ) -> list[Coin] | None: 252 | # No one coin is enough, try to find a best fit pair, otherwise combine the two 253 | # maximum coins. 254 | searcher = CoinPairSearch(amt) 255 | # Process coins for this round. 256 | for k, c in usable_coins.items(): 257 | if isinstance(c, CoinWrapper): 258 | searcher.process_coin_for_combine_search(c.coin) 259 | else: 260 | searcher.process_coin_for_combine_search(c) 261 | max_coins, total = searcher.get_result() 262 | 263 | if total >= amt: 264 | return max_coins 265 | else: 266 | return None 267 | 268 | # Given some coins, combine them, causing the network to make us 269 | # recompute our balance in return. 270 | # 271 | # This is a metaphor for the meaning of "solution" in the coin "puzzle program" context: 272 | # 273 | # The arguments to the coin's puzzle program, which is the first kind 274 | # of object called a 'solution' in this case refers to the arguments 275 | # needed to cause the program to emit blockchain compatible opcodes. 276 | # It's a "puzzle" in the sense that a ctf RE exercise is a "puzzle". 277 | # "solving" it means producing input that causes the unknown program to 278 | # do something desirable. 279 | # 280 | # Spending multiple coins: 281 | # There are two running lists that need to be kept when spending multiple coins: 282 | # 283 | # - A list of signatures. Each coin has its own signature requirements, but standard 284 | # coins are signed like this: 285 | # 286 | # AugSchemeMPL.sign( 287 | # calculate_synthetic_secret_key(self.sk_,DEFAULT_HIDDEN_PUZZLE_HASH), 288 | # (delegated_puzzle_solution.get_tree_hash() + c.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA) 289 | # ) 290 | # 291 | # where c.name is the coin's "name" (in the code) or coinID (in the 292 | # chialisp docs). delegated_puzzle_solution is a clvm program that 293 | # produces the conditions we want to give the puzzle program (the first 294 | # kind of 'solution'), which will add the basic ones needed by owned 295 | # standard coins. 296 | # 297 | # In most cases, we'll give a tuple like (1, [some, python, [data, 298 | # here]]) for these arguments, because '1' is the quote function 'q' in 299 | # clvm. One could write this program with any valid clvm code though. 300 | # The important thing is that it's runnable code, not literal data as 301 | # one might expect. 302 | # 303 | # - A list of CoinSpend objects. 304 | # Theese consist of (with c : Coin): 305 | # 306 | # CoinSpend( 307 | # c, 308 | # c.puzzle(), 309 | # solution, 310 | # ) 311 | # 312 | # Where solution is a second formulation of a 'solution' to a puzzle (the third form 313 | # of 'solution' we'll use in this documentation is the CoinSpend object.). This is 314 | # related to the program arguments above, but prefixes and suffixes an empty list on 315 | # them (admittedly i'm cargo culting this part): 316 | # 317 | # solution = Program.to([[], delegated_puzzle_solution, []]) 318 | # 319 | # So you do whatever you want with a bunch of coins at once and now you have two lists: 320 | # 1) A list of G1Element objects yielded by AugSchemeMPL.sign 321 | # 2) A list of CoinSpend objects. 322 | # 323 | # Now to spend them at once: 324 | # 325 | # signature = AugSchemeMPL.aggregate(signatures) 326 | # spend_bundle = SpendBundle(coin_spends, signature) 327 | # 328 | async def combine_coins(self, coins: list[CoinWrapper]) -> SpendResult | None: 329 | # Overall structure: 330 | # Create len-1 spends that just assert that the final coin is created with full value. 331 | # Create 1 spend for the final coin that asserts the other spends occurred and 332 | # Creates the new coin. 333 | 334 | beginning_balance: uint64 = self.balance() 335 | beginning_coins: int = len(self.usable_coins) 336 | 337 | # We need the final coin to know what the announced coin name will be. 338 | final_coin = CoinWrapper( 339 | coins[-1].name(), 340 | uint64(sum(c.amount for c in coins)), 341 | self.puzzle, 342 | ) 343 | 344 | destroyed_coin_spends: list[CoinSpend] = [] 345 | 346 | # Each coin wants agg_sig_me so we aggregate them at the end. 347 | signatures: list[G2Element] = [] 348 | 349 | for c in coins[:-1]: 350 | announce_conditions: list[list] = [ 351 | # Each coin expects the final coin creation announcement 352 | [ 353 | ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, 354 | std_hash(coins[-1].name() + final_coin.name()), 355 | ] 356 | ] 357 | 358 | coin_spend, signature = c.create_standard_spend(self.sk_, announce_conditions) 359 | destroyed_coin_spends.append(coin_spend) 360 | signatures.append(signature) 361 | 362 | final_coin_creation: list[list] = [ 363 | [ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, final_coin.name()], 364 | [ConditionOpcode.CREATE_COIN, self.puzzle_hash, final_coin.amount], 365 | ] 366 | 367 | coin_spend, signature = coins[-1].create_standard_spend(self.sk_, final_coin_creation) 368 | destroyed_coin_spends.append(coin_spend) 369 | signatures.append(signature) 370 | 371 | signature = AugSchemeMPL.aggregate(signatures) 372 | spend_bundle = SpendBundle(destroyed_coin_spends, signature) 373 | 374 | pushed: dict[str, str | list[Coin]] = await self.parent.push_tx(spend_bundle) 375 | 376 | # We should have the same amount of money. 377 | assert beginning_balance == self.balance() 378 | # We should have shredded n-1 coins and replaced one. 379 | assert len(self.usable_coins) == beginning_coins - (len(coins) - 1) 380 | 381 | return SpendResult(pushed) 382 | 383 | # Find a coin containing amt we can use as a parent. 384 | # Synthesize a coin with sufficient funds if possible. 385 | async def choose_coin(self, amt) -> CoinWrapper | None: 386 | """Given an amount requirement, find a coin that contains at least that much chia""" 387 | start_balance: uint64 = self.balance() 388 | coins_to_spend: list[Coin] | None = self.compute_combine_action(amt, [], dict(self.usable_coins)) 389 | 390 | # Couldn't find a working combination. 391 | if coins_to_spend is None: 392 | return None 393 | 394 | if len(coins_to_spend) == 1: 395 | only_coin: Coin = coins_to_spend[0] 396 | return CoinWrapper( 397 | only_coin.parent_coin_info, 398 | only_coin.amount, 399 | self.puzzle, 400 | ) 401 | 402 | # We receive a timeline of actions to take (indicating that we have a plan) 403 | # Do the first action and start over. 404 | result: SpendResult | None = await self.combine_coins( 405 | [CoinWrapper(coin.parent_coin_info, coin.amount, self.puzzle) for coin in coins_to_spend] 406 | ) 407 | 408 | if result is None: 409 | return None 410 | 411 | assert self.balance() == start_balance 412 | return await self.choose_coin(amt) 413 | 414 | # Create a new smart coin based on a parent coin and return the coin to the user. 415 | # TODO: 416 | # - allow use of more than one coin to launch smart coin 417 | # - ensure input chia = output chia. it'd be dumb to just allow somebody 418 | # to lose their chia without telling them. 419 | async def launch_smart_coin(self, source: Program, **kwargs) -> CoinWrapper | None: 420 | """Create a new smart coin based on a parent coin and return the smart coin's living 421 | coin to the user or None if the spend failed.""" 422 | amt = uint64(1) 423 | found_coin: CoinWrapper | None = None 424 | 425 | if "amt" in kwargs: 426 | amt = kwargs["amt"] 427 | 428 | if "launcher" in kwargs: 429 | found_coin = kwargs["launcher"] 430 | else: 431 | found_coin = await self.choose_coin(amt) 432 | 433 | if found_coin is None: 434 | raise ValueError(f"could not find available coin containing {amt} mojo") 435 | 436 | # Create a puzzle based on the incoming smart coin 437 | cw = SmartCoinWrapper(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, source) 438 | condition_args: list[list] = [ 439 | [ConditionOpcode.CREATE_COIN, cw.puzzle_hash(), amt], 440 | ] 441 | if amt < found_coin.amount: 442 | condition_args.append([ConditionOpcode.CREATE_COIN, self.puzzle_hash, found_coin.amount - amt]) 443 | 444 | delegated_puzzle_solution = Program.to((1, condition_args)) 445 | solution = Program.to([[], delegated_puzzle_solution, []]) 446 | 447 | # Sign the (delegated_puzzle_hash + coin_name) with synthetic secret key 448 | signature: G2Element = AugSchemeMPL.sign( 449 | calculate_synthetic_secret_key(self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH), 450 | ( 451 | delegated_puzzle_solution.get_tree_hash() 452 | + found_coin.name() 453 | + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA 454 | ), 455 | ) 456 | 457 | spend_bundle = SpendBundle( 458 | [make_spend(found_coin.coin, self.puzzle, solution)], 459 | signature, 460 | ) 461 | pushed: dict[str, str | list[Coin]] = await self.parent.push_tx(spend_bundle) 462 | if "error" not in pushed: 463 | return cw.custom_coin(found_coin.coin, amt) 464 | else: 465 | return None 466 | 467 | # Give chia 468 | async def give_chia(self, target: Wallet, amt: uint64) -> CoinWrapper | None: 469 | return await self.launch_smart_coin(target.puzzle, amt=amt) 470 | 471 | # Called each cycle before coins are re-established from the simulator. 472 | def _clear_coins(self): 473 | self.usable_coins = {} 474 | 475 | # Public key of wallet 476 | def pk(self) -> G1Element: 477 | """Return actor's public key""" 478 | return self.pk_ 479 | 480 | # Balance of wallet 481 | def balance(self) -> uint64: 482 | """Return the actor's balance in standard coins as we understand it""" 483 | return uint64(sum(c.amount for c in self.usable_coins.values())) 484 | 485 | # Spend a coin, probably a smart coin. 486 | # Allows the user to specify the arguments for the puzzle solution. 487 | # Automatically takes care of signing, etc. 488 | # Result is an object representing the actions taken when the block 489 | # with this transaction was farmed. 490 | async def spend_coin(self, coin: CoinWrapper, pushtx: bool = True, **kwargs) -> SpendResult | SpendBundle: 491 | """Given a coin object, invoke it on the blockchain, either as a standard 492 | coin if no arguments are given or with custom arguments in args=""" 493 | amt = uint64(1) 494 | if "amt" in kwargs: 495 | amt = kwargs["amt"] 496 | 497 | delegated_puzzle_solution: Program | None = None 498 | if "args" not in kwargs: 499 | target_puzzle_hash: bytes32 = self.puzzle_hash 500 | # Allow the user to 'give this much chia' to another user. 501 | if "to" in kwargs: 502 | target_puzzle_hash = kwargs["to"].puzzle_hash 503 | 504 | # Automatic arguments from the user's intention. 505 | if "custom_conditions" not in kwargs: 506 | solution_list: list[list] = [[ConditionOpcode.CREATE_COIN, target_puzzle_hash, amt]] 507 | else: 508 | solution_list = kwargs["custom_conditions"] 509 | if "remain" in kwargs: 510 | remainer: SmartCoinWrapper | Wallet = kwargs["remain"] 511 | remain_amt = uint64(coin.amount - amt) 512 | if isinstance(remainer, SmartCoinWrapper): 513 | solution_list.append( 514 | [ 515 | ConditionOpcode.CREATE_COIN, 516 | remainer.puzzle_hash(), 517 | remain_amt, 518 | ] 519 | ) 520 | elif isinstance(remainer, Wallet): 521 | solution_list.append([ConditionOpcode.CREATE_COIN, remainer.puzzle_hash, remain_amt]) 522 | else: 523 | raise ValueError("remainer is not a wallet or a smart coin") 524 | 525 | delegated_puzzle_solution = Program.to((1, solution_list)) 526 | # Solution is the solution for the old coin. 527 | solution = Program.to([[], delegated_puzzle_solution, []]) 528 | else: 529 | delegated_puzzle_solution = Program.to(kwargs["args"]) 530 | solution = delegated_puzzle_solution 531 | 532 | solution_for_coin = make_spend(coin.coin, coin.puzzle(), solution) 533 | 534 | # The reason this use of sign_coin_spends exists is that it correctly handles 535 | # the signing for non-standard coins. I don't fully understand the difference but 536 | # this definitely does the right thing. 537 | try: 538 | spend_bundle: SpendBundle = await sign_coin_spends( 539 | [solution_for_coin], 540 | self.pk_to_sk, 541 | self.sk_for_puzzle_hash, 542 | DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA, 543 | DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, 544 | [], 545 | ) 546 | except ValueError: 547 | spend_bundle = SpendBundle( 548 | [solution_for_coin], 549 | G2Element(), 550 | ) 551 | 552 | if pushtx: 553 | pushed: dict[str, str | list[Coin]] = await self.parent.push_tx(spend_bundle) 554 | return SpendResult(pushed) 555 | else: 556 | return spend_bundle 557 | 558 | 559 | # A user oriented (domain specific) view of the chia network. 560 | class Network: 561 | """An object that owns a simulation, responsible for managing Wallet actors, 562 | time and initialization.""" 563 | 564 | time: datetime.timedelta 565 | sim: SpendSim 566 | sim_client: SimClient 567 | wallets: dict[str, Wallet] 568 | nobody: Wallet 569 | 570 | @classmethod 571 | @contextlib.asynccontextmanager 572 | async def managed(cls) -> AsyncIterator[Self]: 573 | self = cls() 574 | self.time = datetime.timedelta(days=18750, seconds=61201) # Past the initial transaction freeze 575 | async with SpendSim.managed() as sim: 576 | self.sim = sim 577 | self.sim_client = SimClient(self.sim) 578 | self.wallets = {} 579 | self.nobody = self.make_wallet("nobody") 580 | self.wallets[str(self.nobody.pk())] = self.nobody 581 | yield self 582 | 583 | # Have the system farm one block with a specific beneficiary (nobody if not specified). 584 | async def farm_block(self, **kwargs) -> tuple[list[Coin], list[Coin]]: 585 | """Given a farmer, farm a block with that actor as the beneficiary of the farm 586 | reward. 587 | 588 | Used for causing chia balance to exist so the system can do things. 589 | """ 590 | farmer: Wallet = self.nobody 591 | if "farmer" in kwargs: 592 | farmer = kwargs["farmer"] 593 | 594 | farm_duration = datetime.timedelta(block_time) 595 | farmed: tuple[list[Coin], list[Coin]] = await self.sim.farm_block(farmer.puzzle_hash) 596 | 597 | for k, w in self.wallets.items(): 598 | w._clear_coins() 599 | 600 | for kw, w in self.wallets.items(): 601 | coin_records: list[CoinRecord] = await self.sim_client.get_coin_records_by_puzzle_hash(w.puzzle_hash) 602 | for coin_record in coin_records: 603 | if coin_record.spent is False: 604 | w.add_coin(CoinWrapper.from_coin(coin_record.coin, w.puzzle)) 605 | 606 | self.time += farm_duration 607 | return farmed 608 | 609 | # Allow the user to create a wallet identity to whom standard coins may be targeted. 610 | # This results in the creation of a wallet that tracks balance and standard coins. 611 | # Public and private key from here are used in signing. 612 | def make_wallet(self, name: str) -> Wallet: 613 | """Create a wallet for an actor. This causes the actor's chia balance in standard 614 | coin to be tracked during the simulation. Wallets have some domain specific methods 615 | that behave in similar ways to other blockchains.""" 616 | key_idx = 1000 * len(self.wallets) 617 | w = Wallet(self, name, key_idx) 618 | self.wallets[str(w.pk())] = w 619 | return w 620 | 621 | # # Skip real time by farming blocks until the target duration is achieved. 622 | async def skip_time(self, target_duration: str, **kwargs): 623 | """Skip a duration of simulated time, causing blocks to be farmed. If a farmer 624 | is specified, they win each block""" 625 | target_time = self.time + datetime.timedelta(pytimeparse.parse(target_duration) / duration_div) 626 | while target_time > self.get_timestamp(): 627 | await self.farm_block(**kwargs) 628 | self.sim.pass_time(uint64(20)) 629 | 630 | # Or possibly aggregate farm_block results. 631 | 632 | def get_timestamp(self) -> datetime.timedelta: 633 | """Return the current simualtion time in seconds.""" 634 | return datetime.timedelta(seconds=float(self.sim.timestamp)) 635 | 636 | # 'peak' is valid 637 | async def get_blockchain_state(self) -> dict: 638 | return {"peak": self.sim.get_height()} 639 | 640 | async def get_block_record_by_height(self, height): 641 | return await self.sim_client.get_block_record_by_height(height) 642 | 643 | async def get_additions_and_removals(self, header_hash): 644 | return await self.sim_client.get_additions_and_removals(header_hash) 645 | 646 | async def get_coin_record_by_name(self, name: bytes32) -> CoinRecord | None: 647 | return await self.sim_client.get_coin_record_by_name(name) 648 | 649 | async def get_coin_records_by_names( 650 | self, 651 | names: list[bytes32], 652 | include_spent_coins: bool = True, 653 | start_height: int | None = None, 654 | end_height: int | None = None, 655 | ) -> list: 656 | result_list = [] 657 | 658 | for n in names: 659 | single_result = await self.sim_client.get_coin_record_by_name(n) 660 | if single_result is not None: 661 | result_list.append(single_result) 662 | 663 | return result_list 664 | 665 | async def get_coin_records_by_parent_ids( 666 | self, 667 | parent_ids: list[bytes32], 668 | include_spent_coins: bool = True, 669 | start_height: int | None = None, 670 | end_height: int | None = None, 671 | ) -> list: 672 | result = [] 673 | 674 | peak_data = await self.get_blockchain_state() 675 | last_block = await self.sim_client.get_block_record_by_height(peak_data["peak"]) 676 | 677 | while last_block.height > 0: 678 | last_block_hash = last_block.header_hash 679 | additions, _removals = await self.sim_client.get_additions_and_removals(last_block_hash) 680 | for new_coin in additions: 681 | if new_coin.coin.parent_coin_info in parent_ids: 682 | result.append(new_coin) 683 | 684 | last_block = await self.sim_client.get_block_record_by_height(uint32(last_block.height - 1)) 685 | 686 | return result 687 | 688 | async def get_puzzle_and_solution(self, coin_id: bytes32, height: uint32) -> CoinSpend | None: 689 | return await self.sim_client.get_puzzle_and_solution(coin_id, height) 690 | 691 | # Given a spend bundle, farm a block and analyze the result. 692 | async def push_tx(self, bundle: SpendBundle) -> dict[str, str | list[Coin]]: 693 | """Given a spend bundle, try to farm a block containing it. If the spend bundle 694 | didn't validate, then a result containing an 'error' key is returned. The reward 695 | for the block goes to Network::nobody""" 696 | 697 | _status, error = await self.sim_client.push_tx(bundle) 698 | if error: 699 | return {"error": str(error)} 700 | 701 | # Common case that we want to farm this right away. 702 | additions, removals = await self.farm_block() 703 | return { 704 | "additions": additions, 705 | "removals": removals, 706 | } 707 | 708 | 709 | @asynccontextmanager 710 | async def setup() -> AsyncIterator[tuple[Network, Wallet, Wallet]]: 711 | async with Network.managed() as network: 712 | alice = network.make_wallet("alice") 713 | bob = network.make_wallet("bob") 714 | yield network, alice, bob 715 | -------------------------------------------------------------------------------- /cdv/cmds/chia_inspect.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import sys 5 | from collections.abc import Callable, Iterable 6 | from pprint import pprint 7 | from secrets import token_bytes 8 | from typing import Any, ClassVar 9 | 10 | import click 11 | from chia._tests.util.get_name_puzzle_conditions import get_name_puzzle_conditions 12 | from chia.consensus.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict 13 | from chia.consensus.cost_calculator import NPCResult 14 | from chia.consensus.default_constants import DEFAULT_CONSTANTS 15 | from chia.full_node.bundle_tools import simple_solution_generator 16 | from chia.types.blockchain_format.coin import Coin 17 | from chia.types.blockchain_format.program import INFINITE_COST, Program 18 | from chia.types.coin_record import CoinRecord 19 | from chia.types.coin_spend import CoinSpend, make_spend 20 | from chia.types.generator_types import BlockGenerator 21 | from chia.util.byte_types import hexstr_to_bytes 22 | from chia.util.config import load_config 23 | from chia.util.default_root import DEFAULT_ROOT_PATH 24 | from chia.util.keychain import bytes_to_mnemonic, mnemonic_to_seed 25 | from chia.wallet.derive_keys import _derive_path 26 | from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( 27 | DEFAULT_HIDDEN_PUZZLE_HASH, 28 | calculate_synthetic_public_key, 29 | calculate_synthetic_secret_key, 30 | ) 31 | from chia.wallet.wallet_spend_bundle import WalletSpendBundle 32 | from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey 33 | from chia_rs.sized_bytes import bytes32 34 | from chia_rs.sized_ints import uint32, uint64 35 | 36 | from cdv.cmds.util import parse_program 37 | 38 | """ 39 | This group of commands is for guessing the types of objects when you don't know what they are, 40 | but also for building them from scratch and for modifying them once you have them completely loaded/built. 41 | """ 42 | 43 | 44 | @click.group("inspect", short_help="Inspect various data structures") 45 | @click.option("-j", "--json", is_flag=True, help="Output the result as JSON") 46 | @click.option("-b", "--bytes", is_flag=True, help="Output the result as bytes") 47 | @click.option("-id", "--id", is_flag=True, help="Output the id of the object") 48 | @click.option("-t", "--type", is_flag=True, help="Output the type of the object") 49 | @click.pass_context 50 | def inspect_cmd(ctx: click.Context, **kwargs) -> None: 51 | ctx.ensure_object(dict) 52 | for key, value in kwargs.items(): 53 | ctx.obj[key] = value 54 | 55 | 56 | # Every inspect command except the key related ones will call this when they're done 57 | # The function allows the flags on "inspect" apply AFTER the objects have been successfully loaded 58 | def inspect_callback( 59 | objs: list[Any], 60 | ctx: click.Context, 61 | id_calc: Callable = (lambda: None), 62 | type: str = "Unknown", 63 | ): 64 | # By default we return JSON 65 | if (not any([value for key, value in ctx.obj.items()])) or ctx.obj["json"]: 66 | if getattr(objs[0], "to_json_dict", None): 67 | print(json.dumps([obj.to_json_dict() for obj in objs])) 68 | else: 69 | pprint(f"Object of type {type} cannot be serialized to JSON") 70 | if ctx.obj["bytes"]: 71 | final_output = [] 72 | for obj in objs: 73 | try: 74 | final_output.append(bytes(obj).hex()) 75 | except AssertionError: 76 | final_output.append("None") # This is for coins since coins overload the __bytes__ method 77 | pprint(final_output) 78 | if ctx.obj["id"]: 79 | pprint([id_calc(obj) for obj in objs]) 80 | if ctx.obj["type"]: 81 | pprint([type for _ in objs]) 82 | 83 | 84 | # Utility functions 85 | 86 | 87 | # If there's only one key, return the data on that key instead (for things like {'spend_bundle': {...}}) 88 | def json_and_key_strip(input: str) -> dict: 89 | json_dict: dict = json.loads(input) 90 | if len(json_dict.keys()) == 1: 91 | result: dict = json_dict[next(iter(json_dict.keys()))] 92 | return result # mypy 93 | else: 94 | return json_dict 95 | 96 | 97 | # Streamable objects can be in either bytes or JSON and we'll take them via CLI or file 98 | def streamable_load(cls: Any, inputs: Iterable[Any]) -> list[Any]: 99 | # If we're receiving a group of objects rather than strings to parse, we're going to return them back as a list 100 | if inputs and not isinstance(next(iter(inputs)), str): 101 | for inst in inputs: 102 | assert isinstance(inst, cls) 103 | return list(inputs) 104 | 105 | input_objs: list[Any] = [] 106 | for input in inputs: 107 | if "{" in input: # If it's a JSON string 108 | json_dict = json_and_key_strip(input) 109 | parsed_obj = cls.from_json_dict(json_dict) 110 | elif "." in input: # If it's a filename 111 | file_string = open(input).read() 112 | if "{" in file_string: # If it's a JSON file 113 | json_dict = json_and_key_strip(file_string) 114 | parsed_obj = cls.from_json_dict(json_dict) 115 | else: # If it's bytes in a file 116 | original_bytes = hexstr_to_bytes(file_string) 117 | parsed_obj = cls.from_bytes(original_bytes) 118 | assert bytes(parsed_obj) == original_bytes # assert the serialization incase it was only a partial read 119 | else: # If it's a byte string 120 | original_bytes = hexstr_to_bytes(input) 121 | parsed_obj = cls.from_bytes(original_bytes) 122 | assert bytes(parsed_obj) == original_bytes 123 | 124 | input_objs.append(parsed_obj) 125 | 126 | return input_objs 127 | 128 | 129 | # Theoretically, every type of data should have it's command called if it's passed through this function 130 | @inspect_cmd.command("any", short_help="Attempt to guess the type of the object before inspecting it") 131 | @click.argument("objects", nargs=-1, required=False) 132 | @click.pass_context 133 | def inspect_any_cmd(ctx: click.Context, objects: tuple[str]): 134 | input_objects = [] 135 | for obj in objects: 136 | in_obj: Any = obj 137 | # Try it as Streamable types 138 | for cls in [Coin, CoinSpend, WalletSpendBundle, CoinRecord]: 139 | try: 140 | in_obj = streamable_load(cls, [obj])[0] 141 | break 142 | except Exception: 143 | pass 144 | else: 145 | # Try it as some key stuff 146 | for cls in [G1Element, G2Element, PrivateKey]: 147 | try: 148 | in_obj = cls.from_bytes(hexstr_to_bytes(obj)) # type: ignore 149 | break 150 | except Exception: 151 | pass 152 | else: 153 | # Try it as a Program 154 | try: 155 | in_obj = parse_program(obj) 156 | except Exception: 157 | pass 158 | 159 | input_objects.append(in_obj) 160 | 161 | for obj in input_objects: 162 | if type(obj) is str: 163 | print(f"Could not guess the type of {obj}") 164 | elif type(obj) is Coin: 165 | assert isinstance(obj, Coin) # mypy otherwise complains that obj is a str 166 | do_inspect_coin_cmd(ctx, [obj]) 167 | elif type(obj) is CoinSpend: 168 | assert isinstance(obj, CoinSpend) 169 | do_inspect_coin_spend_cmd(ctx, [obj]) 170 | elif type(obj) is WalletSpendBundle: 171 | assert isinstance(obj, WalletSpendBundle) 172 | do_inspect_spend_bundle_cmd(ctx, [obj]) 173 | elif type(obj) is CoinRecord: 174 | do_inspect_coin_record_cmd(ctx, [obj]) 175 | elif type(obj) is Program: 176 | assert isinstance(obj, Program) 177 | do_inspect_program_cmd(ctx, [obj]) 178 | elif type(obj) is G1Element: 179 | do_inspect_keys_cmd(ctx, public_key=obj) 180 | elif type(obj) is PrivateKey: 181 | do_inspect_keys_cmd(ctx, secret_key=obj) 182 | elif type(obj) is G2Element: 183 | print("That's a BLS aggregated signature") # This is more helpful than just printing it back to them 184 | 185 | 186 | """ 187 | Most of the following commands are designed to also be available as importable functions and usable by the "any" cmd. 188 | This is why there's the cmd/do_cmd pattern in all of them. 189 | The objects they are inspecting can be a list of strings (from the cmd line), or a list of the object being inspected. 190 | """ 191 | 192 | 193 | @inspect_cmd.command("coins", short_help="Various methods for examining and calculating coin objects") 194 | @click.argument("coins", nargs=-1, required=False) 195 | @click.option("-pid", "--parent-id", help="The parent coin's ID") 196 | @click.option("-ph", "--puzzle-hash", help="The tree hash of the CLVM puzzle that locks this coin") 197 | @click.option("-a", "--amount", help="The amount of the coin") 198 | @click.pass_context 199 | def inspect_coin_cmd(ctx: click.Context, coins: tuple[str], **kwargs): 200 | do_inspect_coin_cmd(ctx, coins, **kwargs) 201 | 202 | 203 | def do_inspect_coin_cmd( 204 | ctx: click.Context, 205 | coins: tuple[str] | list[Coin], 206 | print_results: bool = True, 207 | **kwargs, 208 | ) -> list[Coin]: 209 | # If this is built from the command line and all relevant arguments are there 210 | if kwargs and all([kwargs[key] for key in kwargs.keys()]): 211 | coin_objs: list[Coin] = [ 212 | Coin( 213 | bytes32.from_hexstr(kwargs["parent_id"]), 214 | bytes32.from_hexstr(kwargs["puzzle_hash"]), 215 | uint64(kwargs["amount"]), 216 | ) 217 | ] 218 | elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): 219 | try: 220 | coin_objs = streamable_load(Coin, coins) 221 | except Exception as e: 222 | print(f"One or more of the specified objects was not a coin: {e}") 223 | sys.exit(1) 224 | else: 225 | print("Invalid arguments specified.") 226 | sys.exit(1) 227 | 228 | if print_results: 229 | inspect_callback(coin_objs, ctx, id_calc=(lambda e: e.name().hex()), type="Coin") 230 | 231 | return coin_objs 232 | 233 | 234 | @inspect_cmd.command( 235 | "spends", 236 | short_help="Various methods for examining and calculating CoinSpend objects", 237 | ) 238 | @click.argument("spends", nargs=-1, required=False) 239 | @click.option("-c", "--coin", help="The coin to spend (replaces -pid, -ph, -a)") 240 | @click.option("-pid", "--parent-id", help="The parent coin's ID") 241 | @click.option( 242 | "-ph", 243 | "--puzzle-hash", 244 | help="The tree hash of the CLVM puzzle that locks the coin being spent", 245 | ) 246 | @click.option("-a", "--amount", help="The amount of the coin being spent") 247 | @click.option("-pr", "--puzzle-reveal", help="The program that is hashed into this coin") 248 | @click.option("-s", "--solution", help="The attempted solution to the puzzle") 249 | @click.option("-ec", "--cost", is_flag=True, help="Print the CLVM cost of the spend") 250 | @click.option( 251 | "--ignore-byte-cost", 252 | is_flag=True, 253 | help="Ignore the puzzle reveal cost when examining a spend (mimics potential compression)", 254 | ) 255 | @click.pass_context 256 | def inspect_coin_spend_cmd(ctx: click.Context, spends: tuple[str], **kwargs): 257 | do_inspect_coin_spend_cmd(ctx, spends, **kwargs) 258 | 259 | 260 | def do_inspect_coin_spend_cmd( 261 | ctx: click.Context, 262 | spends: tuple[str] | list[CoinSpend], 263 | print_results: bool = True, 264 | **kwargs, 265 | ) -> list[CoinSpend]: 266 | cost_flag: bool = False 267 | ignore_byte_cost: bool = False 268 | if kwargs: 269 | # These args don't really fit with the logic below so we're going to store and delete them 270 | cost_flag = kwargs["cost"] 271 | ignore_byte_cost = kwargs["ignore_byte_cost"] 272 | del kwargs["cost"] 273 | del kwargs["ignore_byte_cost"] 274 | # If this is being built from the command line and the two required args are there 275 | if kwargs and all([kwargs["puzzle_reveal"], kwargs["solution"]]): 276 | # If they specified the coin components 277 | if (not kwargs["coin"]) and all([kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]]): 278 | coin_spend_objs: list[CoinSpend] = [ 279 | make_spend( 280 | Coin( 281 | bytes32.from_hexstr(kwargs["parent_id"]), 282 | bytes32.from_hexstr(kwargs["puzzle_hash"]), 283 | uint64(kwargs["amount"]), 284 | ), 285 | parse_program(kwargs["puzzle_reveal"]), 286 | parse_program(kwargs["solution"]), 287 | ) 288 | ] 289 | # If they specifed a coin object to parse 290 | elif kwargs["coin"]: 291 | coin_spend_objs = [ 292 | make_spend( 293 | do_inspect_coin_cmd(ctx, [kwargs["coin"]], print_results=False)[0], 294 | parse_program(kwargs["puzzle_reveal"]), 295 | parse_program(kwargs["solution"]), 296 | ) 297 | ] 298 | else: 299 | print("Invalid arguments specified.") 300 | elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): 301 | try: 302 | coin_spend_objs = streamable_load(CoinSpend, spends) 303 | except Exception as e: 304 | print(f"One or more of the specified objects was not a coin spend: {e}") 305 | sys.exit(1) 306 | else: 307 | print("Invalid arguments specified.") 308 | sys.exit(1) 309 | 310 | if print_results: 311 | inspect_callback( 312 | coin_spend_objs, 313 | ctx, 314 | id_calc=(lambda e: e.coin.name().hex()), 315 | type="CoinSpend", 316 | ) 317 | # We're going to print some extra stuff if they wanted to see the cost 318 | if cost_flag: 319 | for coin_spend in coin_spend_objs: 320 | program: BlockGenerator = simple_solution_generator(WalletSpendBundle([coin_spend], G2Element())) 321 | npc_result: NPCResult = get_name_puzzle_conditions( 322 | program, 323 | INFINITE_COST, 324 | height=DEFAULT_CONSTANTS.HARD_FORK_HEIGHT, # so that all opcodes are available 325 | mempool_mode=True, 326 | constants=DEFAULT_CONSTANTS, 327 | ) 328 | cost = int(0 if npc_result.conds is None else npc_result.conds.cost) 329 | if ignore_byte_cost: 330 | cost -= len(bytes(coin_spend.puzzle_reveal)) * DEFAULT_CONSTANTS.COST_PER_BYTE 331 | print(f"Cost: {cost}") 332 | 333 | return coin_spend_objs 334 | 335 | 336 | @inspect_cmd.command( 337 | "spendbundles", 338 | short_help="Various methods for examining and calculating SpendBundle objects", 339 | ) 340 | @click.argument("bundles", nargs=-1, required=False) 341 | @click.option("-s", "--spend", multiple=True, help="A coin spend object to add to the bundle") 342 | @click.option( 343 | "-as", 344 | "--aggsig", 345 | multiple=True, 346 | help="A BLS signature to aggregate into the bundle (can be used more than once)", 347 | ) 348 | @click.option("-db", "--debug", is_flag=True, help="Show debugging information about the bundles") 349 | @click.option( 350 | "-sd", 351 | "--signable_data", 352 | is_flag=True, 353 | help="Print the data that needs to be signed in the bundles", 354 | ) 355 | @click.option( 356 | "-n", 357 | "--network", 358 | default="mainnet", 359 | show_default=True, 360 | help="The network this spend bundle will be pushed to (for AGG_SIG_ME)", 361 | ) 362 | @click.option("-ec", "--cost", is_flag=True, help="Print the CLVM cost of the entire bundle") 363 | @click.option( 364 | "--ignore-byte-cost", 365 | is_flag=True, 366 | help="Ignore the puzzle reveal cost when examining a spend (mimics potential compression)", 367 | ) 368 | @click.pass_context 369 | def inspect_spend_bundle_cmd(ctx: click.Context, bundles: tuple[str], **kwargs): 370 | do_inspect_spend_bundle_cmd(ctx, bundles, **kwargs) 371 | 372 | 373 | def do_inspect_spend_bundle_cmd( 374 | ctx: click.Context, 375 | bundles: tuple[str] | list[WalletSpendBundle], 376 | print_results: bool = True, 377 | **kwargs, 378 | ) -> list[WalletSpendBundle]: 379 | # If this is from the command line and they've specified at lease one spend to parse 380 | if kwargs and (len(kwargs["spend"]) > 0): 381 | if len(kwargs["aggsig"]) > 0: 382 | sig: G2Element = AugSchemeMPL.aggregate( 383 | [G2Element.from_bytes(hexstr_to_bytes(sig)) for sig in kwargs["aggsig"]] 384 | ) 385 | else: 386 | sig = G2Element() 387 | spend_bundle_objs: list[WalletSpendBundle] = [ 388 | WalletSpendBundle( 389 | do_inspect_coin_spend_cmd(ctx, kwargs["spend"], print_results=False), 390 | sig, 391 | ) 392 | ] 393 | else: 394 | try: 395 | spend_bundle_objs = streamable_load(WalletSpendBundle, bundles) 396 | except Exception as e: 397 | print(f"One or more of the specified objects was not a spend bundle: {e}") 398 | sys.exit(1) 399 | 400 | if print_results: 401 | inspect_callback( 402 | spend_bundle_objs, 403 | ctx, 404 | id_calc=(lambda e: e.name().hex()), 405 | type="WalletSpendBundle", 406 | ) 407 | # We're going to print some extra stuff if they've asked for it. 408 | if kwargs: 409 | if kwargs["cost"]: 410 | for spend_bundle in spend_bundle_objs: 411 | program: BlockGenerator = simple_solution_generator(spend_bundle) 412 | npc_result: NPCResult = get_name_puzzle_conditions( 413 | program, 414 | INFINITE_COST, 415 | height=DEFAULT_CONSTANTS.HARD_FORK_HEIGHT, # so that all opcodes are available 416 | mempool_mode=True, 417 | constants=DEFAULT_CONSTANTS, 418 | ) 419 | cost = int(0 if npc_result.conds is None else npc_result.conds.cost) 420 | if kwargs["ignore_byte_cost"]: 421 | for coin_spend in spend_bundle.coin_spends: 422 | cost -= len(bytes(coin_spend.puzzle_reveal)) * DEFAULT_CONSTANTS.COST_PER_BYTE 423 | print(f"Cost: {cost}") 424 | if kwargs["debug"]: 425 | print("") 426 | print("Debugging Information") 427 | print("---------------------") 428 | config: dict = load_config(DEFAULT_ROOT_PATH, "config.yaml") 429 | genesis_challenge: str = config["network_overrides"]["constants"][kwargs["network"]][ 430 | "GENESIS_CHALLENGE" 431 | ] 432 | for bundle in spend_bundle_objs: 433 | bundle.debug(agg_sig_additional_data=bytes32(hexstr_to_bytes(genesis_challenge))) 434 | if kwargs["signable_data"]: 435 | print("") 436 | print("Public Key/Message Pairs") 437 | print("------------------------") 438 | pkm_dict: dict[str, list[bytes]] = {} 439 | for obj in spend_bundle_objs: 440 | for coin_spend in obj.coin_spends: 441 | conditions_dict = conditions_dict_for_solution( 442 | coin_spend.puzzle_reveal, coin_spend.solution, INFINITE_COST 443 | ) 444 | if conditions_dict is None: 445 | print(f"Generating conditions failed, con: {conditions_dict}") 446 | else: 447 | config = load_config(DEFAULT_ROOT_PATH, "config.yaml") 448 | genesis_challenge = config["network_overrides"]["constants"][kwargs["network"]][ 449 | "GENESIS_CHALLENGE" 450 | ] 451 | for pk, msg in pkm_pairs_for_conditions_dict( 452 | conditions_dict, 453 | coin_spend.coin, 454 | hexstr_to_bytes(genesis_challenge), 455 | ): 456 | if str(pk) in pkm_dict: 457 | pkm_dict[str(pk)].append(msg) 458 | else: 459 | pkm_dict[str(pk)] = [msg] 460 | # This very deliberately prints identical messages multiple times 461 | for pk_str, msgs in pkm_dict.items(): 462 | print(f"{pk_str}: ") 463 | for msg in msgs: 464 | print(f"\t- {msg.hex()}") 465 | 466 | return spend_bundle_objs 467 | 468 | 469 | @inspect_cmd.command( 470 | "coinrecords", 471 | short_help="Various methods for examining and calculating CoinRecord objects", 472 | ) 473 | @click.argument("records", nargs=-1, required=False) 474 | @click.option("-c", "--coin", help="The coin to spend (replaces -pid, -ph, -a)") 475 | @click.option("-pid", "--parent-id", help="The parent coin's ID") 476 | @click.option( 477 | "-ph", 478 | "--puzzle-hash", 479 | help="The tree hash of the CLVM puzzle that locks the coin being spent", 480 | ) 481 | @click.option("-a", "--amount", help="The amount of the coin being spent") 482 | @click.option( 483 | "-cb", 484 | "--coinbase", 485 | is_flag=True, 486 | help="Is this coin generated as a farming reward?", 487 | ) 488 | @click.option( 489 | "-ci", 490 | "--confirmed-block-index", 491 | help="The block index in which this coin was created", 492 | ) 493 | @click.option( 494 | "-si", 495 | "--spent-block-index", 496 | default=0, 497 | show_default=True, 498 | type=int, 499 | help="The block index in which this coin was spent", 500 | ) 501 | @click.option( 502 | "-t", 503 | "--timestamp", 504 | help="The timestamp of the block in which this coin was created", 505 | ) 506 | @click.pass_context 507 | def inspect_coin_record_cmd(ctx: click.Context, records: tuple[str], **kwargs): 508 | do_inspect_coin_record_cmd(ctx, records, **kwargs) 509 | 510 | 511 | def do_inspect_coin_record_cmd( 512 | ctx: click.Context, 513 | records: tuple[str] | list[CoinRecord], 514 | print_results: bool = True, 515 | **kwargs, 516 | ) -> list[CoinRecord]: 517 | # If we're building this from the command line and we have the two arguements we forsure need 518 | if kwargs and all([kwargs["confirmed_block_index"], kwargs["timestamp"]]): 519 | # If they've specified the components of a coin 520 | if (not kwargs["coin"]) and all([kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]]): 521 | coin_record_objs: list[CoinRecord] = [ 522 | CoinRecord( 523 | Coin( 524 | bytes32.from_hexstr(kwargs["parent_id"]), 525 | bytes32.from_hexstr(kwargs["puzzle_hash"]), 526 | uint64(kwargs["amount"]), 527 | ), 528 | kwargs["confirmed_block_index"], 529 | kwargs["spent_block_index"], 530 | kwargs["coinbase"], 531 | kwargs["timestamp"], 532 | ) 533 | ] 534 | # If they've specified a coin to parse 535 | elif kwargs["coin"]: 536 | coin_record_objs = [ 537 | CoinRecord( 538 | do_inspect_coin_cmd(ctx, (kwargs["coin"],), print_results=False)[0], 539 | kwargs["confirmed_block_index"], 540 | kwargs["spent_block_index"], 541 | kwargs["coinbase"], 542 | kwargs["timestamp"], 543 | ) 544 | ] 545 | else: 546 | print("Invalid arguments specified.") 547 | elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): 548 | try: 549 | coin_record_objs = streamable_load(CoinRecord, records) 550 | except Exception as e: 551 | print(f"One or more of the specified objects was not a coin record: {e}") 552 | sys.exit(1) 553 | else: 554 | print("Invalid arguments specified.") 555 | sys.exit(1) 556 | 557 | if print_results: 558 | inspect_callback( 559 | coin_record_objs, 560 | ctx, 561 | id_calc=(lambda e: e.coin.name().hex()), 562 | type="CoinRecord", 563 | ) 564 | 565 | return coin_record_objs 566 | 567 | 568 | @inspect_cmd.command("programs", short_help="Various methods for examining CLVM Program objects") 569 | @click.argument("programs", nargs=-1, required=False) 570 | @click.pass_context 571 | def inspect_program_cmd(ctx: click.Context, programs: tuple[str], **kwargs): 572 | do_inspect_program_cmd(ctx, programs, **kwargs) 573 | 574 | 575 | def do_inspect_program_cmd( 576 | ctx: click.Context, 577 | programs: tuple[str] | list[Program], 578 | print_results: bool = True, 579 | **kwargs, 580 | ) -> list[Program]: 581 | try: 582 | program_objs: list[Program] = [parse_program(prog) for prog in programs] 583 | except Exception: 584 | print("One or more of the specified objects was not a Program") 585 | sys.exit(1) 586 | 587 | if print_results: 588 | inspect_callback( 589 | program_objs, 590 | ctx, 591 | id_calc=(lambda e: e.get_tree_hash().hex()), 592 | type="Program", 593 | ) 594 | 595 | return program_objs 596 | 597 | 598 | @inspect_cmd.command("keys", short_help="Various methods for examining and generating BLS Keys") 599 | @click.option("-pk", "--public-key", help="A BLS public key") 600 | @click.option("-sk", "--secret-key", help="The secret key from which to derive the public key") 601 | @click.option("-m", "--mnemonic", help="A 24 word mnemonic from which to derive the secret key") 602 | @click.option("-r", "--random", is_flag=True, help="Generate a random set of keys") 603 | @click.option("-hd", "--hd-path", help="Enter the HD path in the form 'm/12381/8444/n/n'") 604 | @click.option( 605 | "-t", 606 | "--key-type", 607 | type=click.Choice(["farmer", "pool", "wallet", "local", "backup", "owner", "auth"]), 608 | help="Automatically use a chia defined HD path for a specific service", 609 | ) 610 | @click.option( 611 | "-sy", 612 | "--synthetic", 613 | is_flag=True, 614 | help="Use a hidden puzzle hash (-ph) to calculate a synthetic secret/public key", 615 | ) 616 | @click.option( 617 | "-ph", 618 | "--hidden-puzhash", 619 | default=DEFAULT_HIDDEN_PUZZLE_HASH.hex(), 620 | show_default=False, 621 | help="The hidden puzzle to use when calculating a synthetic key", 622 | ) 623 | @click.pass_context 624 | def inspect_keys_cmd(ctx: click.Context, **kwargs): 625 | do_inspect_keys_cmd(ctx, **kwargs) 626 | 627 | 628 | def do_inspect_keys_cmd(ctx: click.Context, print_results: bool = True, **kwargs): 629 | sk: PrivateKey | None = None 630 | pk: G1Element = G1Element() 631 | path: str = "m" 632 | # If we're receiving this from the any command 633 | if len(kwargs) == 1: 634 | if "secret_key" in kwargs: 635 | sk = kwargs["secret_key"] 636 | assert sk is not None 637 | pk = sk.get_g1() 638 | elif "public_key" in kwargs: 639 | pk = kwargs["public_key"] 640 | else: 641 | condition_list = [ 642 | kwargs["public_key"], 643 | kwargs["secret_key"], 644 | kwargs["mnemonic"], 645 | kwargs["random"], 646 | ] 647 | 648 | # This a construct to ensure there is exactly one of these conditions set 649 | def one_or_zero(value): 650 | return 1 if value else 0 651 | 652 | if sum([one_or_zero(condition) for condition in condition_list]) == 1: 653 | if kwargs["public_key"]: 654 | sk = None 655 | pk = G1Element.from_bytes(hexstr_to_bytes(kwargs["public_key"])) 656 | elif kwargs["secret_key"]: 657 | sk = PrivateKey.from_bytes(hexstr_to_bytes(kwargs["secret_key"])) 658 | pk = sk.get_g1() 659 | elif kwargs["mnemonic"]: 660 | seed = mnemonic_to_seed(kwargs["mnemonic"]) 661 | sk = AugSchemeMPL.key_gen(seed) 662 | pk = sk.get_g1() 663 | elif kwargs["random"]: 664 | sk = AugSchemeMPL.key_gen(mnemonic_to_seed(bytes_to_mnemonic(token_bytes(32)))) 665 | pk = sk.get_g1() 666 | 667 | list_path: list[int] = [] 668 | if kwargs["hd_path"] and (kwargs["hd_path"] != "m"): 669 | list_path = [uint32(int(i)) for i in kwargs["hd_path"].split("/") if i != "m"] 670 | elif kwargs["key_type"]: 671 | case = kwargs["key_type"] 672 | if case == "farmer": 673 | list_path = [12381, 8444, 0, 0] 674 | if case == "pool": 675 | list_path = [12381, 8444, 1, 0] 676 | if case == "wallet": 677 | list_path = [12381, 8444, 2, 0] 678 | if case == "local": 679 | list_path = [12381, 8444, 3, 0] 680 | if case == "backup": 681 | list_path = [12381, 8444, 4, 0] 682 | if case == "owner": 683 | list_path = [12381, 8444, 5, 0] 684 | if case == "auth": 685 | list_path = [12381, 8444, 6, 0] 686 | if list_path: 687 | assert sk is not None 688 | sk = _derive_path(sk, list_path) 689 | pk = sk.get_g1() 690 | path = "m/" + "/".join([str(e) for e in path]) 691 | 692 | if kwargs["synthetic"]: 693 | if sk: 694 | sk = calculate_synthetic_secret_key(sk, bytes32.from_hexstr(kwargs["hidden_puzhash"])) 695 | pk = calculate_synthetic_public_key(pk, bytes32.from_hexstr(kwargs["hidden_puzhash"])) 696 | else: 697 | print("Invalid arguments specified.") 698 | 699 | if sk: 700 | print(f"Secret Key: {bytes(sk).hex()}") 701 | print(f"Public Key: {pk!s}") 702 | print(f"Fingerprint: {pk.get_fingerprint()!s}") 703 | print(f"HD Path: {path}") 704 | 705 | 706 | # This class is necessary for being able to handle parameters in order, rather than grouped by name 707 | class OrderedParamsCommand(click.Command): 708 | _options: ClassVar[list] = [] 709 | 710 | def parse_args(self, ctx, args): 711 | # run the parser for ourselves to preserve the passed order 712 | parser = self.make_parser(ctx) 713 | opts, _, param_order = parser.parse_args(args=list(args)) 714 | for param in param_order: 715 | if param.name != "help": 716 | type(self)._options.append((param, opts[param.name].pop(0))) 717 | 718 | # return "normal" parse results 719 | return super().parse_args(ctx, args) 720 | 721 | 722 | @inspect_cmd.command( 723 | "signatures", 724 | cls=OrderedParamsCommand, 725 | short_help="Various methods for examining and creating BLS aggregated signatures", 726 | ) 727 | @click.option("-sk", "--secret-key", multiple=True, help="A secret key to sign a message with") 728 | @click.option( 729 | "-t", 730 | "--utf-8", 731 | multiple=True, 732 | help="A UTF-8 message to be signed with the specified secret key", 733 | ) 734 | @click.option( 735 | "-b", 736 | "--bytes", 737 | multiple=True, 738 | help="A hex message to be signed with the specified secret key", 739 | ) 740 | @click.option("-sig", "--aggsig", multiple=True, help="A signature to be aggregated") 741 | @click.pass_context 742 | def inspect_sigs_cmd(ctx: click.Context, **kwargs): 743 | do_inspect_sigs_cmd(ctx, **kwargs) 744 | 745 | 746 | # This command sort of works like a script: 747 | # Whenever you use a parameter, it changes some state, 748 | # at the end it returns the result of running those parameters in that order. 749 | def do_inspect_sigs_cmd(ctx: click.Context, print_results: bool = True, **kwargs) -> G2Element: 750 | base = G2Element() 751 | sk: PrivateKey | None = None 752 | for param, value in OrderedParamsCommand._options: 753 | if param.name == "secret_key": 754 | sk = PrivateKey.from_bytes(hexstr_to_bytes(value)) 755 | elif param.name == "aggsig": 756 | new_sig = G2Element.from_bytes(hexstr_to_bytes(value)) 757 | base = AugSchemeMPL.aggregate([base, new_sig]) 758 | elif sk: 759 | if param.name == "utf_8": 760 | new_sig = AugSchemeMPL.sign(sk, bytes(value, "utf-8")) 761 | base = AugSchemeMPL.aggregate([base, new_sig]) 762 | if param.name == "bytes": 763 | new_sig = AugSchemeMPL.sign(sk, hexstr_to_bytes(value)) 764 | base = AugSchemeMPL.aggregate([base, new_sig]) 765 | 766 | if print_results: 767 | print(str(base)) 768 | 769 | return base 770 | --------------------------------------------------------------------------------