├── .env ├── .flake8 ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── README.md ├── checkcontractverify.md ├── contracts.md └── matt.md ├── examples ├── fund.sh ├── game256 │ ├── README.md │ └── game256_contracts.py ├── init.sh ├── ram │ ├── README.md │ ├── ram.py │ ├── ram_contracts.py │ └── requirements.txt ├── rps │ ├── README.md │ ├── requirements.txt │ ├── rps.py │ └── rps_contracts.py └── vault │ ├── README.md │ ├── minivault_contracts.py │ ├── requirements.txt │ ├── scripts │ ├── normal-1-input-3-outputs.txt │ ├── normal-with-revault-3-inputs.txt │ ├── recover-from-trigger-1-input.txt │ └── recover-from-vault-1-input.txt │ ├── vault.py │ └── vault_contracts.py ├── matt ├── __init__.py ├── argtypes.py ├── btctools │ ├── __init__.py │ ├── _base58.py │ ├── _script.py │ ├── _serialize.py │ ├── auth_proxy.py │ ├── block.py │ ├── common.py │ ├── errors.py │ ├── key.py │ ├── messages.py │ ├── psbt.py │ ├── ripemd160.py │ ├── script.py │ ├── segwit_addr.py │ └── tx.py ├── contracts.py ├── environment.py ├── hub │ ├── README.md │ ├── __init__.py │ └── fraud.py ├── manager.py ├── merkle.py ├── script_helpers.py └── utils.py ├── pyproject.toml ├── pytest.ini ├── requirements-dev.txt ├── setup.py └── tests ├── conftest.py ├── test_fraud.py ├── test_minivault.py ├── test_ram.py ├── test_rps.py ├── test_utils ├── README.md ├── __init__.py └── utxograph.py └── test_vault.py /.env: -------------------------------------------------------------------------------- 1 | RPC_HOST = "localhost" 2 | RPC_USER = "rpcuser" 3 | RPC_PASSWORD = "rpcpass" 4 | RPC_PORT = "18443" -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Python Test Suite 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-24.04 14 | container: bigspider/bitcoin_matt 15 | steps: 16 | - name: Prepare Configuration File 17 | run: | 18 | mkdir -p /github/home/.bitcoin 19 | cp /root/.bitcoin/bitcoin.conf /github/home/.bitcoin/bitcoin.conf 20 | - name: Run MATT-enabled bitcoind 21 | run: | 22 | bitcoind -regtest --daemon 23 | - name: Set up dependencies 24 | run: | 25 | apt-get update 26 | apt-get install -y libssl-dev libffi-dev 27 | apt-get install -y python3-venv 28 | - name: Set up Python 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: '3.10' 32 | - name: Clone 33 | uses: actions/checkout@v4 34 | - name: Install dependencies 35 | run: | 36 | python -m venv venv 37 | source venv/bin/activate 38 | pip install --upgrade pip 39 | pip install -r requirements-dev.txt 40 | pip install . 41 | shell: bash 42 | - name: Create test wallet 43 | run: bash ./examples/init.sh 44 | - name: Run tests and capture output 45 | run: | 46 | source venv/bin/activate 47 | pytest -vv 48 | shell: bash 49 | - name: Upload test output as artifact 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: test-output 53 | path: test_output.txt 54 | - name: Upload markdown report as artifact 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: report.md 58 | path: report.md 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | examples/**/.cli-history 163 | 164 | tests/graphs/** 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WIP Python framework for MATT smart contracts 2 | 3 | This repository contains a (very much Work In Progress) framework to create and test smart contracts using the `OP_CHECKCONTRACTVERIFY` opcode of MATT. 4 | 5 | # Prerequisites 6 | ## Installing the library 7 | 8 | Optionally, create a python environment: 9 | 10 | ```bash 11 | $ python -m venv venv 12 | $ source venv/bin/activate 13 | ``` 14 | 15 | Install the library with: 16 | 17 | ```bash 18 | $ pip install . 19 | ``` 20 | 21 | If you're also modifying the framework, you might want to install the package in editable mode: 22 | 23 | ```bash 24 | $ pip install -e . 25 | ``` 26 | 27 | ## Run bitcoin-inquisition MATT in regtest mode 28 | 29 | The fastest way to get started is [this docker container](https://github.com/Merkleize/docker): 30 | 31 | ```bash 32 | $ docker pull bigspider/bitcoin_matt 33 | 34 | $ docker run -d -p 18443:18443 bigspider/bitcoin_matt 35 | ``` 36 | 37 | All the examples use the `RPC_USER`, `RPC_PASSWORD`, `RPC_HOST`, `RPC_PORT` environment variables to set up a connection with the regtest bitcoin node; the default values are the same as set in the container. 38 | 39 | If they differ in your system, make sure to set them appropriately, or create a `.env` file similar to the following: 40 | 41 | ``` 42 | RPC_HOST = "localhost" 43 | RPC_USER = "rpcuser" 44 | RPC_PASSWORD = "rpcpass" 45 | RPC_PORT = "18443" 46 | ``` 47 | 48 | NOTE: the examples do not handle fee management and will send transactions with 0 fees; those are rejected with the default settings of bitcoin-core. 49 | 50 | If not using the container above, please see an [example of custom bitcoin.conf](https://github.com/Merkleize/docker/blob/master/bitcoin.conf) to work with the scripts in this repository. 51 | 52 | # Docs 53 | 54 | See the [docs](./docs) folder for high-level documentation on how to design smart contracts using MATT. 55 | 56 | As the framework is still in development, we recommend looking at the code examples below for developer documentation on using pymatt. 57 | 58 | # Case studies 59 | 60 | The `examples` folder contains some utility scripts to work with regtest bitcoin-core: 61 | - [init.sh](examples/init.sh) creates/loads and funds a wallet named `testwallet`. Run it once before the examples and you're good to go. 62 | - [fund.sh](examples/fund.sh) that allows to fund a certain address. 63 | 64 | The following examples are currently implemented 65 | 66 | - [Vault](examples/vault) [cli]: an implementation of a vault, largely compatible with [OP_VAULT BIP-0345](https://github.com/bitcoin/bips/pull/1421). 67 | - [Rock-Paper-Scissors](examples/rps) [cli]: play Rock-Paper-Scissors on bitcoin. 68 | - [RAM](examples/ram) [cli]: a a contract that uses a Merkle tree to store a vector of arbitrary length in size, with transitions that allow to modify one element of the vector. 69 | - [game256](examples/game256): Implements an end-2-end execution of the toy example for fraud proofs [drafted in bitcoin-dev](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-November/021205.html). 70 | 71 | For the ones marked with [cli], a simple interactive tool allows to play with the contract. More code examples can be found in the test suite. 72 | 73 | # Tests 74 | 75 | This project uses `pytest` to run automated tests. Install the dependencies with: 76 | 77 | ```bash 78 | $ pip install -r requirements-dev.txt 79 | ``` 80 | 81 | The test suite requires a running instance of the MATT-enabled bitcoin-inquisition, for example using the container above. The [init.sh](examples/init.sh) script makes sure that a funded test wallet is loaded. 82 | 83 | ```bash 84 | $ docker run -d -p 18443:18443 bigspider/bitcoin_matt 85 | $ bash ./examples/init.sh 86 | ``` 87 | 88 | Then, run the tests with 89 | 90 | ```bash 91 | $ pytest 92 | ``` 93 | 94 | Refer to the [pytest documentation](https://docs.pytest.org/) for more advanced options. 95 | 96 | ## Report 97 | 98 | Some tests produce additional illustrative info about the transactions produced during the contract execution, in a Markdown report called `report.md`. 99 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This folder contains high level documentation on the MATT framework, and the core concepts used in the examples of smart contracts implemented in this repository. 2 | 3 | - [matt](./matt.md)
Start here for a general introduction to MATT. 4 | - [contracts](./contracts.md)
How to build smart contracts based on state machines. 5 | - [checkcontractverify](./checkcontractverify.md)
Draft specs for the `OP_CHECKCONTRACTVERIFY` opcode. -------------------------------------------------------------------------------- /docs/checkcontractverify.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The content of this page describes the current semantics of `OP_CHECKCONTRACTVERIFY`. 4 | 5 | Together with `OP_CAT`, it is a straightforward implementation of MATT in bitcoin. 6 | 7 | ## P2TR and _augmented_ P2TR 8 | 9 | In a P2TR scriptPubKey, the output key is computed from an _internal pubkey_ `pk` and a `taptree`, as: 10 | 11 | ``` 12 | output_key = taproot_tweak(pk, taptree) 13 | ``` 14 | 15 | We call an _augmented_ P2TR any P2TR where the _internal pubkey_ is, in turn, computed from a _naked pubkey_ `naked_pk`, tweaked with some embedded `data`: 16 | 17 | ``` 18 | pk = tweak(naked_pk, data) 19 | ``` 20 | 21 | `OP_CHECKCONTRACTVERIFY` allows to verify that the `scriptPubkey` of an input or an output is a certain P2TR Script, possibly _augmented_ with some embedded data. 22 | 23 | The embedded data is a 32-byte value. 24 | 25 | 26 | ## `OP_CHECKCONTRACTVERIFY` 27 | 28 | This section describes the semantics of the `OP_CHECKCONTRACTVERIFY` opcode, as currently implemented in the [docker container for MATT](https://github.com/Merkleize/docker). 29 | 30 | ### Description 31 | 32 | `OP_CHECKCONTRACTVERIFY` is only active for scripts spending a Segwit version 1 input. 33 | 34 | Get `data`, `index`, `pk`, `taptree`, `flags` from the stack (bottom-to-top). 35 | 36 | `OP_CHECKCONTRACTVERIFY` verifies that the scriptPubKey of the input/output with the given `index` is a P2TR script with a pubkey obtained by the x-only pubkey `pk`, optionally tweaked with `data`, optionally taptweaked with `taptree`. The `CIOCV_FLAG_CHECK_INPUT` determines if the `index` refers to an input or an output. Special values for the parameters, are listed below. 37 | 38 | The `flags` parameter alters the behaviour of the opcode. If negative, the opcode checks the `scriptPubkey` of an input; otherwise, it checks the `scriptPubkey` of an output. The following value for the `flags` is currently the only one defined for inputs: 39 | 40 | - `CCV_FLAG_CHECK_INPUT = -1`: makes the opcode check an input. 41 | 42 | Non-negative values make the opcode check an output, and different values have different behaviour in the way the output's amount (`nValue`) is checked. The following values for the `flags` are currently defined for checking an output: 43 | 44 | - `0`: default behavior, the (possibly residual) amount of this input must be present in the output. This amount 45 | - `CCV_FLAG_IGNORE_OUTPUT_AMOUNT = 1`: For outputs, disables the default deferred checks on amounts defined below. Undefined when `CCV_FLAG_CHECK_INPUT` is present. 46 | - `CCV_FLAG_DEDUCT_OUTPUT_AMOUNT = 2`: Fail if the amount of the output is larger than the amount of the input; otherwise, subtracts the value of the output from the value of the current input in future calls top `OP_CHECKCONTRACTVERIFY`. 47 | 48 | The following values of the parameters are special values: 49 | - If `pk` is empty, it is replaced with the NUMS x-only pubkey `0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0` defined in [BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). 50 | - If `pk` is `-1`, it is replaced with the current input's internal key. 51 | - If `index` is `-1`, it is replaced with the current input index. 52 | - If `data` is empty, the data tweak is skipped. 53 | - If `taptree` is empty, the taptweak is skipped. 54 | - If `taptree` is `-1`, the taptree of the current input is used for the taptweak. 55 | 56 | The following additional deferred checks are performed after the validation of all inputs is completed: 57 | - The amount of each output must be at least equal to the sum of the amount of all the inputs that have a `CCV` for that output with the default flag (equal to `0`). 58 | - No output that is a target of `CCV_FLAG_DEDUCT_OUTPUT_AMOUNT` can also be the target of another `OP_CHECKCONTRACTVERIFY`, unless it's with the `CCV_FLAG_IGNORE_OUTPUT_AMOUNT`. 59 | 60 | ### Pseudocode 61 | 62 | Semantics (initialization before input evaluation): 63 | ```python 64 | for in_index in range(n_inputs) 65 | in_ccv_amount[in_index] = inputs[in_index].amount 66 | 67 | for out_index in range(n_outputs) 68 | out_min_amount[out_index] = 0 69 | ``` 70 | 71 | 72 | Semantics (per input): 73 | 74 | ```python 75 | if flags < CCV_FLAG_CHECK_INPUT or flags > CCV_FLAG_DEDUCT_OUTPUT_AMOUNT: 76 | return success() # undefined flags are OP_SUCCESS 77 | 78 | if index == -1: 79 | index = current_input_index 80 | 81 | if flags == CCV_FLAG_CHECK_INPUT: 82 | if index < 0 or index >= n_inputs: 83 | return fail() 84 | 85 | target = inputs[index].scriptPubKey 86 | else: 87 | if index < 0 or index >= n_outputs: 88 | return fail() 89 | 90 | target = outputs[index].scriptPubKey 91 | 92 | if taptree == <-1>: 93 | taptree = current_input_taptree 94 | 95 | if pk == <0>: 96 | result = BIP340_NUMS_KEY 97 | elif pk == <-1>: 98 | result = current_input_internal_key 99 | elif len(pk) == 32: 100 | result = pk 101 | else: 102 | return fail() 103 | 104 | if data != <0>: 105 | if len(data) != 32: 106 | return fail() 107 | 108 | result = tweak(result, data) 109 | 110 | if len(taptree) != 0: 111 | if len(taptree) != 32: 112 | return fail() 113 | 114 | result = taptweak(result, taptree) 115 | 116 | if target != P2TR(result) 117 | return fail() 118 | 119 | if flags == 0: 120 | out_min_amount[index] += in_ccv_amount[current_input_index] 121 | elif flags == CCV_FLAG_DEDUCT_OUTPUT_AMOUNT: 122 | if in_ccv_amount[current_input_index] > outputs[index].amount: 123 | return fail() 124 | in_ccv_amount[current_input_index] -= outputs[index].amount 125 | 126 | stack.pop(5) # drop all 5 stack elements 127 | ``` 128 | 129 | Semantics (deferred, checks after all inputs are validated successfully): 130 | 131 | ```python 132 | 133 | for out_index in range(n_outputs): 134 | if outputs[out_index].amount < out_min_amount[out_index]: 135 | return fail() 136 | 137 | if an_output_was_used_both_with_default_behavior_and_with_DEDUCT_OUTPUT_AMOUNT_semantics(): 138 | return fail() 139 | ``` 140 | 141 | ## Common patterns 142 | 143 | Here are some examples for the most common combination of parameters. 144 | 145 | ### Check that some data is embedded in the current input 146 | 147 | This is used to check data that was typically committed to in an output from a covenant-encumbered spend that produced the current input. 148 | 149 | ``` 150 | CCV 151 | ``` 152 | 153 | ### Check that a certain output with index `out_i` is a certain contract with specified data, preserving input amount 154 | 155 | This might be used for a 1-to-1 or many-to-1 covenant-encumbered spend: one or several inputs are spent to an output with certain code and data. 156 | 157 | ``` 158 | CCV 159 | ``` 160 | 161 | ### Check that the output with the same index as the current input is a certain contract with specified data, preserving input amount 162 | 163 | This is a common pattern for 1-input-1-output contracts, as it allows flexibility when creating the transaction. Typically, this would be one after checking the current input's data using the [standard pattern](#check-that-some-data-is-embedded-in-the-current-input). 164 | 165 | Many spends of this kind could easily be batched in the same transaction, possibly together with other unencumbered inputs/outputs. 166 | 167 | ``` 168 | CCV 169 | ``` 170 | 171 | ### Check that a certain output with index `out_i` is a a P2TR with a pubkey `output_pk`, preserving amount: 172 | 173 | A simpler case where we just want the output to be a certain P2TR output, without any embedded data. 174 | 175 | ``` 176 | > > CCV 177 | ``` 178 | 179 | 180 | ## Advanced patterns 181 | 182 | The examples in this section use some less common use cases of `OP_CHECKCONTRACTVERIFY`. 183 | 184 | ### Check that some other input with index `in_i` is a specific contract with embedded data: 185 | 186 | This allows to "read" the data of another input. 187 | 188 | ``` 189 | CCV 190 | ``` 191 | 192 | ### Subtract the amount of output `out_i` from the current input 193 | 194 | This checks the _data_ and _program_ of an output, an subtracts the value of this output from the value of the current input. The residual value of the current input will be used in further calls to `OP_CHECKCONTRACTVERIFY`. 195 | 196 | This allows the pattern of sending some amount to one or more specified destination, and then separately decide where to send any residual value. 197 | 198 | ``` 199 | CCV 200 | ``` 201 | 202 | ### Check that a certain output with index `out_i` is a certain contract with specified data; don't check amount 203 | 204 | This could be used to check _data_ and _program_ of an output, but not its amount (which might be either irrelevant, or is checked via a different introspection opcode). 205 | 206 | ``` 207 | CCV 208 | ``` 209 | 210 | ### Check that the input is sent exactly to the same scriptPubKey 211 | This requires that the output with the same index as the current input is exactly the same script, and with the same amount. 212 | 213 | ``` 214 | > CCV 215 | ``` 216 | -------------------------------------------------------------------------------- /docs/contracts.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | MATT allows to define on-chain protocols in the UTXO model by using the covenant encumbrance. 4 | 5 | In such protocols, the UTXOs themselves contain the _state_ of the contract. The state is updated by spending some UTXOs and producing new UTXOs with a different state - with rules that are encoded in the UTXO itself. 6 | 7 | This page documents the general framework that is used in the demos. 8 | 9 | See [checkcontractverify.md](checkcontractverify.md) for the semantics of `OP_CHECKCONTRACTVERIFY` and the concept of _augmented_ UTXOs. 10 | 11 | ## Contracts, programs and clauses 12 | 13 | The internal pubkey (or the _naked_ pubkey for an augmented P2TR), together with the taptree, constitutes the ___program___ of the contract, which encodes all the spending conditions of the contract. 14 | 15 | An actual UTXO whose `scriptPubKey` is a program, possibly with some specified embedded _data_, is a ___contract instance___. 16 | 17 | We call ___clause___ each of the spending conditions in the taptree of. Each clause might also specify the state transition rules, by defining the program of one or more of the outputs.
18 | The keypath, if not a NUMS (Nothing-Up-My-Sleeve) point, can also be considered an additional special clause with no condition on the outputs. 19 | 20 | ### Merklelized data 21 | 22 | While the embedded _data_ of the contract is a 32-byte value, it is always possible to represent arbitrary collections of value and use the 32-byte slot to store a _commitment_ to the data. 23 | 24 | We can think as a contract instance as a UTXO that _stores_ some arbitrary state. The exact representation of the state is an implementation detail, but here are some rules of thumb: 25 | 26 | - A single 32-byte value is stored as-is. 27 | - For a single value that is not 32-bytes long, the SHA256 hash is the embedded data. 28 | - If multiple values are part of the contract state, they can be encoded as the leaves of a [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree), and only the root hash is stored. 29 | 30 | ___Remark___: other ways of committing to a collection of values are possible, and sometimes more efficient. For example, _the SHA256-hash of the concatenation of the SHA256-hashes of the elements_ is indeed more efficient than Merkle trees if all the values need to be revealed anyway. Care is necessary as __not all ways of concatenating/hashing a collection of elements are safe__, as some are prone to collisions. 31 | 32 | ## Smart contracts as finite state machines 33 | 34 | Clauses of a contract can specify that certain outputs must be certain other contracts, and their embedded data. 35 | 36 | This allows to represent UTXO-based smart contract protocols as finite state machines, where each node represents a contract, and its clauses specify the transitions to 0 or more other contracts. 37 | 38 | For many constructions, spending the UTXO of a contract produces one or more pre-determined contracts as its output UTXOs. In this case, the resulting diagram is acyclic and is (or can be reduced to) a *Directed Acyclic Graph* (DAG) 39 | 40 | Some contracts might have an output with the _same_ contract as the input being spent. In that case, the diagram is not a DAG, but only loops from a node to itself are allowed; this avoids impossible hash cycles. 41 | 42 | Here's an example of a [vault](https://github.com/Merkleize/pymatt/tree/master/examples/vault): 43 | 44 | ```mermaid 45 | graph LR 46 | VAULT -->|recover| R["recovery
address"] 47 | VAULT -->|"trigger
(ctv_hash)"| UNVAULTING 48 | VAULT ---|"trigger_and_revault
(ctv_hash)"| X( ) 49 | X --> VAULT 50 | X --> UNVAULTING 51 | style X display: none; 52 | 53 | UNVAULTING("UNVAULTING
[ctv_hash]") 54 | UNVAULTING -->|recover| R["recovery
address"] 55 | UNVAULTING -->|withdraw| D["ctv outputs
(possibly several)"] 56 | 57 | classDef contract stroke:#333,stroke-width:2px; 58 | class VAULT,UNVAULTING contract 59 | ``` 60 | 61 | ***Remark***: this diagram represents the possible states and transitions of each individual UTXO. For some construction, the entire smart contract resides in a single UTXO; however, other constructions might require the existence of multiple UTXOs, which could interact in some spending conditions. 62 | 63 | ## Definitions 64 | 65 | In this section we will define a pseudocode notation to describe MATT contracts. 66 | 67 | Naming conventions: 68 | - _parameters_: decided at contract creation time, hardcoded in the Script. 69 | - _variables_: data stored in the UTXO instance, accessible to Script via `OP_CHECKCONTRACTVERIFY`. 70 | - _arguments_: passed via the witness during transitions (script spending paths) 71 | 72 | 73 | We represent a contract with the following notation: 74 | 75 | ``` 76 | ContractName{params}[vars] 77 | ``` 78 | 79 | where: 80 | - `ContractName`, in camelcase, is the name of the contract 81 | - `params`: the compile-time list of parameters of the contract 82 | - `vars`: the list of variables (concretely stored in the data commitment of the covenant, aka the _state_ of the contract) 83 | 84 | `params` and `vars` should be omitted if empty. Moreover, for notational simplicity we prefer to omit (and list separately) the *global* parameters that are unchanged for all the contracts in the diagram. 85 | 86 | We call *clause* each spending condition of a contract. Each clause has a name (in lowercase, in snake_case if multiple words) 87 | 88 | Transition notation: 89 | ``` 90 | clause_name(args) => out_i: Contract{contract_params}[contract_vars] 91 | ``` 92 | if only a single output contract is produced by this clause, or: 93 | 94 | ``` 95 | clause_name(args) => [ 96 | out1_i: Contract1{contract1_params}[contract1_vars], 97 | out2_i: Contract2{contract2_params}[contract2_vars] 98 | ] 99 | ``` 100 | 101 | `out_i` is the index of the output that must match the contract. If omitted (allowed for at most one of the outputs), it must be equal to the input index being spent. 102 | 103 | 104 | where: 105 | - `args`: the arguments of the clause, passed via the witness stack. 106 | - `=> Contract...` the destination contract of this clause. Omitted if the spending condition is not encumbered by the covenant. `contract_params` can only depend on the `params` of the current contract. `contract_vars` can depend on the `params` and the `vars` of the current contract, and also on the argument `args`. 107 | 108 | The spending condition can be any predicate that can be expressed in Script, with access to all the `params`, `vars` and `args`. 109 | 110 | _Note_: this ignores the technical details of how to encode/decode the state variables to/from a single hash; that is an implementation detail that can safely be left out when discussing the semantic of a smart contract. 111 | 112 | ### Default contract 113 | 114 | The contract `P2TR{pk}` is equal to the output script descriptor `tr(pk)`. 115 | 116 | ### Example: Vault 117 | 118 | With the above conventions, we can model the Vault contract drawn above as follows: 119 | 120 | Global parameters: 121 | - `unvault_pk`: a public key that can start trigger a withdrawal 122 | - `spend_delay`: the number of blocks triggered coins have to wait before the final withdrawal 123 | - `recover_pk`: a public key for a P2TR address that coins will be sent to if the *recover* clause is used. 124 | 125 | 126 | ``` 127 | global unvault_pk 128 | global recover_pk 129 | global spend_delay 130 | 131 | 132 | Vault: 133 | trigger(ctv_hash, out_i) => [out_i: Unvaulting[ctv_hash]]: 134 | checksig(unvault_pk) 135 | 136 | trigger_and_revault(ctv_hash, revault_out_i, trigger_out_i) => [ 137 | deduct revault_out_i: Vault, 138 | trigger_out_i: Unvaulting[ctv_hash] 139 | ]: 140 | checksig(unvault_pk) 141 | 142 | recover => P2TR{recover_pk}: 143 | pass 144 | 145 | 146 | Unvaulting[ctv_hash]: 147 | withdraw: 148 | older(spend_delay) 149 | ctv(ctv_hash) 150 | 151 | recover => P2TR{recover_pk}: 152 | pass 153 | ``` 154 | 155 | A matching Python implementation can be found in [vault_contracts.py](../examples/vault/vault_contracts.py). 156 | -------------------------------------------------------------------------------- /docs/matt.md: -------------------------------------------------------------------------------- 1 | MATT is an acronym for _Merkleize All The Things_, and is a research project for an approach to bitcoin smart contracts that only require relatively minimal changes to bitcoin's Script, while allowing very general constructions. 2 | 3 | This page introduces the general idea of the framework. 4 | 5 | 6 | ### Key concept: Covenants 7 | 8 | In Bitcoin, coins are locked in UTXOs (short for Unspent Transaction Outputs). A UTXO contains the Script that specifies the conditions to spend those coins ("Alice can spend", or "Alice can spend, or Bob can spend after a month", etc.). 9 | 10 | Today, there is no way in Bitcoin to add constraints on where coins can be spent, if the conditions in the Script are satisfied. 11 | 12 | A script that adds such restriction is called a covenant, and that is not possible in Bitcoin today, at least not within the Script language. Adding the capability to do so in Script is an increasingly active area of research. 13 | 14 | ## The covenant introduced in MATT 15 | The core idea in MATT is to introduce the following capability to be accessible within Script: 16 | 17 | - force an output to have a certain Script (and their amounts) 18 | - attach a piece of data to an output 19 | - read the data of the current input (or another one) 20 | 21 | The first is common to many other covenant proposals, for example [OP_CHECKTEMPLATEVERIFY](https://github.com/bitcoin/bips/blob/master/bip-0119.mediawiki) is a long-discussed proposal that can constrain all the outputs at the same time. 22 | 23 | The part relative to the data is more specific: this data can be as short as a 32-byte hash, but the key is that the data of an output is not decided when the UTXO is first created, but it is dynamically computed in Script (and therefore it can depend on "parameters" that are passed by the spender). This is extremely powerful, as it allows to create some sort of "state machines" where the execution can decide: 24 | 25 | - what is the next "state" of the state machine (by constraining the Script of the outputs) 26 | - what is the "data" attached to the next state 27 | 28 | There are many ways to introduce these capabilities in bitcoin Script. This repository is based on the [OP_CHECKCONTRACTVERIFY](./checkcontractverify.md) opcode, which is tailored specifically to smart contract using the ideas of MATT; it would be easy to port the code of the examples in this repository to other approaches. 29 | 30 | ## Merkleize All The Things 31 | 32 | The rest of this page goes into more details into what the framework 33 | 34 | ### (1) Merkleizing the data 35 | Here we come to the second core idea: if we can only attach a single piece of data (32 bytes), how can we execute more complex "contracts" that require accessing/storing more data? 36 | 37 | The solution is to use the 32-byte data as a commitment to a larger collection containing all the required data of the contract. This can be done with [Merkle trees](https://en.wikipedia.org/wiki/Merkle_tree), which are not currently possible in Script, but become possible by adding a simple opcode like `OP_CAT`, that takes two stack elements and concatenates them. 38 | 39 | It is not difficult to convince oneself that the capabilities of the covenant described above, together with the ability to compress arbitrary data in a single hash, allows chains of transactions to be programmed to perform arbitrary computation. More on this below. 40 | 41 | ### (2) Merkleizing the Script 42 | The concept of this section is not really anything new in MATT, as it was introduced in the Taproot soft fork, which is active in bitcoin since November 2021. 43 | 44 | When you represent a contract as a Finite State Machine, you often have situations where a certain state can transition to multiple other states of the FSM. 45 | 46 | For example, if the smart contract is encoding a game of Tic-Tac-Toe between Alice and Bob, and it's Alice's turn, one transition encodes "Alice plays her move". However, Alice might stop playing, so you likely want to allow Bob to automatically win the game if Alice doesn't play her move within 1 day. So a second "state transition" in the node that represents Alice's turn could be "After 1 day, Bob can take the money". 47 | 48 | More complicated contracts can have many possible transitions from the same node, and taproot makes it possible by using - you guessed it - a Merkle tree of all the possible transition. Each leaf of this Merkle tree contains a bitcoin Script, as usual. 49 | 50 | ### (3) Merkleizing the Execution 51 | This section describes some more advanced applications of the ideas described above; unavoidably, this section will be the hardest to read. 52 | 53 | What we said above is already enough to represent some very interesting smart contracts, like [vaults](../examples/vault/), [Rock-Paper-Scissors](../examples/rps), and a lot more. 54 | 55 | However, there are smart contracts that are way too expensive to execute in the way described above, simply because bitcoin Script is not powerful enough to perform complex computations (this is by design, as it helps to keep the validation efficient and cheap, which is crucial for people to be able to run bitcoin full nodes!). 56 | 57 | Sure, one could decompose the computation in a chain of hundreds, or thousands of little state-machine updates − but this certainly does not scale! 58 | 59 | MATT allows an interesting solution to this problem, by using an idea known as ___fraud proofs___. 60 | 61 | It goes like this: suppose that a transition from a certain state to another state is only allowed if Alice produces an input x that satisfies a certain complicated condition. For example, _x_ must be a prime number. Script does not have any opcode to check if a number is prime! 62 | 63 | Therefore, we modify the contract as follows: 64 | 65 | - Alice posts the number _x_ (unconditionally), and the contract moves to a "Challenge phase" 66 | - During the challenge phase, if Bob verifies that _x_ is not prime, Bob can challenge the assertion. 67 | - Otherwise, after some time (say, 1 day), Alice can continue as normal: she produced an _x_ that Bob did not challenge, so it is probably a prime! 68 | 69 | What happens if Bob does start a challenge? In that case, the contract enters a different stage: a fraud proof protocol. The protocol involves multiple transaction from both parties, but it guarantees the following: if Alice was lying, she will be exposed and lose her money; vice-versa, if she wasn't lying, Bob will lose his money. Lying is not profitable! 70 | 71 | The execution of the fraud proof protocol requires yet another Merkle tree, this time built off-chain by the participants while they execute the computation. See [fraud.py](../matt/hub/fraud.py) for a detailed description and implementation of the fraud proof protocol. -------------------------------------------------------------------------------- /examples/fund.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if at least one argument (Bitcoin address) was provided 4 | if [ "$#" -lt 1 ]; then 5 | echo "Usage: ./fund.sh [amount]" 6 | exit 1 7 | fi 8 | 9 | # Set the address to the provided argument 10 | ADDRESS=$1 11 | 12 | # Check if an amount was provided; if not, default to 0.002 13 | AMOUNT=${2:-0.00002} 14 | 15 | # Send the specified amount (or 0.0002 if none specified) to the provided address 16 | bitcoin-cli -regtest -rpcwallet=testwallet sendtoaddress $ADDRESS $AMOUNT 17 | 18 | # Generate a block to confirm the transaction 19 | bitcoin-cli -regtest -rpcwallet=testwallet -generate 1 20 | -------------------------------------------------------------------------------- /examples/game256/README.md: -------------------------------------------------------------------------------- 1 | # 256 game 2 | 3 | `game256_contracts.py` implements the game of doubling 8 times, the toy example used in [this post](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-November/021205.html) to the bitcoin-dev mailing list. 4 | 5 | There is no interactive tool to play with these contracts, but they are [tested](../../tests/test_fraud.py) in the pytest test suite. 6 | 7 | The actual code of the bisection protocol smart contract, which is independent from the specific computation, is in [hub/fraud.py](../../matt/hub/fraud.py). 8 | -------------------------------------------------------------------------------- /examples/game256/game256_contracts.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from dataclasses import dataclass 4 | from matt import NUMS_KEY 5 | from matt.argtypes import BytesType, IntType, SignerType 6 | from matt.btctools.common import sha256 7 | from matt.btctools.script import OP_ADD, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_FROMALTSTACK, OP_NOT, OP_PICK, OP_ROT, OP_SHA256, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, CScript 8 | from matt.contracts import ClauseOutput, StandardClause, StandardAugmentedP2TR, StandardP2TR, ContractState 9 | from matt.hub.fraud import Bisect_1, Computer, Leaf 10 | from matt.merkle import MerkleTree 11 | from matt.script_helpers import check_input_contract, check_output_contract, dup, merkle_root, older 12 | from matt.utils import encode_wit_element 13 | 14 | # Note: for simplicity, this contract does not yet implement bonds, nor slashing part of it after the fraud proof protocol. 15 | 16 | # TODO: add forfait clauses whenever needed 17 | 18 | # TODO: how to generalize what the contract does after the leaf? We should be able to compose clauses with some external code. 19 | # Do we need "clause" algebra? 20 | 21 | 22 | class G256_S0(StandardP2TR): 23 | def __init__(self, alice_pk: bytes, bob_pk: bytes, forfait_timeout: int = 10): 24 | self.alice_pk = alice_pk 25 | self.bob_pk = bob_pk 26 | self.forfait_timeout = forfait_timeout 27 | 28 | g256_s1 = G256_S1(alice_pk, bob_pk, forfait_timeout) 29 | # witness: 30 | choose = StandardClause( 31 | name="choose", 32 | script=CScript([ 33 | *g256_s1.State.encoder_script(), 34 | *check_output_contract(g256_s1), 35 | 36 | bob_pk, 37 | OP_CHECKSIG 38 | ]), 39 | arg_specs=[ 40 | ('bob_sig', SignerType(bob_pk)), 41 | ('x', IntType()), 42 | ], 43 | next_outputs_fn=lambda args, _: [ClauseOutput( 44 | n=-1, 45 | next_contract=g256_s1, 46 | next_state=g256_s1.State(x=args['x']) 47 | )] 48 | ) 49 | 50 | super().__init__(NUMS_KEY, choose) 51 | 52 | 53 | class G256_S1(StandardAugmentedP2TR): 54 | @dataclass 55 | class State(ContractState): 56 | x: int 57 | 58 | def encode(self): 59 | return sha256(encode_wit_element(self.x)) 60 | 61 | def encoder_script(): 62 | return CScript([OP_SHA256]) 63 | 64 | def __init__(self, alice_pk: bytes, bob_pk: bytes, forfait_timeout): 65 | self.alice_pk = alice_pk 66 | self.bob_pk = bob_pk 67 | self.forfait_timeout = forfait_timeout 68 | 69 | g256_s2 = G256_S2(alice_pk, bob_pk, forfait_timeout) 70 | 71 | # reveal: 72 | reveal = StandardClause( 73 | name="reveal", 74 | script=CScript([ 75 | OP_DUP, 76 | 77 | # check that the top of the stack is the embedded data 78 | *self.State.encoder_script(), 79 | *check_input_contract(), 80 | 81 | # 82 | *g256_s2.State.encoder_script(), 83 | *check_output_contract(g256_s2), 84 | 85 | alice_pk, 86 | OP_CHECKSIG 87 | ]), 88 | arg_specs=[ 89 | ('alice_sig', SignerType(alice_pk)), 90 | ('t_a', BytesType()), 91 | ('y', IntType()), 92 | ('x', IntType()), 93 | ], 94 | next_outputs_fn=lambda args, _: [ClauseOutput( 95 | n=-1, 96 | next_contract=g256_s2, 97 | next_state=g256_s2.State(t_a=args['t_a'], y=args['y'], x=args['x']) 98 | )] 99 | ) 100 | 101 | super().__init__(NUMS_KEY, reveal) 102 | 103 | 104 | Compute2x = Computer( 105 | encoder=CScript([OP_SHA256]), 106 | func=CScript([OP_DUP, OP_ADD]), 107 | specs=[('x', IntType())], 108 | ) 109 | 110 | 111 | NopInt = Computer( 112 | encoder=CScript([]), 113 | func=CScript([]), 114 | specs=[('x', IntType())], 115 | ) 116 | 117 | 118 | class G256_S2(StandardAugmentedP2TR): 119 | @dataclass 120 | class State(ContractState): 121 | t_a: bytes 122 | y: int 123 | x: bytes 124 | 125 | def encode(self): 126 | return MerkleTree([self.t_a, sha256(encode_wit_element(self.y)), sha256(encode_wit_element(self.x))]).root 127 | 128 | def encoder_script(): 129 | return CScript([ 130 | OP_TOALTSTACK, OP_SHA256, OP_FROMALTSTACK, OP_SHA256, 131 | *merkle_root(3) 132 | ]) 133 | 134 | def __init__(self, alice_pk: bytes, bob_pk: bytes, forfait_timeout: int = 10): 135 | self.alice_pk = alice_pk 136 | self.bob_pk = bob_pk 137 | self.forfait_timeout = forfait_timeout 138 | 139 | # reveal: 140 | withdraw = StandardClause( 141 | name="withdraw", 142 | script=CScript([ 143 | *older(forfait_timeout), 144 | 145 | alice_pk, 146 | OP_CHECKSIG 147 | ]), 148 | arg_specs=[('alice_sig', SignerType(alice_pk))] 149 | ) 150 | 151 | def leaf_factory(i: int): return Leaf(alice_pk, bob_pk, Compute2x) 152 | 153 | bisectg256_0 = Bisect_1(alice_pk, bob_pk, 0, 7, leaf_factory, forfait_timeout) 154 | # start_challenge: 155 | start_challenge = StandardClause( 156 | name="start_challenge", 157 | script=CScript([ 158 | OP_TOALTSTACK, 159 | 160 | # check that y != z 161 | OP_DUP, 3, OP_PICK, OP_EQUAL, OP_NOT, OP_VERIFY, 162 | 163 | OP_TOALTSTACK, 164 | 165 | # --- 166 | 167 | *dup(3), 168 | 169 | # verify the embedded data 170 | *self.State.encoder_script(), 171 | *check_input_contract(), 172 | 173 | # --- 174 | OP_SHA256, OP_SWAP, OP_SHA256, 175 | # --- 176 | OP_ROT, 177 | # --- 178 | 179 | OP_FROMALTSTACK, OP_SHA256, 180 | # --- 181 | OP_SWAP, 182 | # --- 183 | 184 | OP_FROMALTSTACK, 185 | 186 | # 187 | 188 | *bisectg256_0.State.encoder_script(), 189 | *check_output_contract(bisectg256_0), 190 | 191 | bob_pk, 192 | OP_CHECKSIG 193 | ]), 194 | arg_specs=[ 195 | ('bob_sig', SignerType(bob_pk)), 196 | ('t_a', BytesType()), 197 | ('y', IntType()), 198 | ('x', IntType()), 199 | ('z', IntType()), 200 | ('t_b', BytesType()), 201 | ], 202 | next_outputs_fn=lambda args, _: [ClauseOutput( 203 | n=-1, 204 | next_contract=bisectg256_0, 205 | next_state=bisectg256_0.State( 206 | h_start=sha256(encode_wit_element(args['x'])), 207 | h_end_a=sha256(encode_wit_element(args['y'])), 208 | h_end_b=sha256(encode_wit_element(args['z'])), 209 | trace_a=args['t_a'], 210 | trace_b=args['t_b'], 211 | ) 212 | )] 213 | ) 214 | 215 | super().__init__(NUMS_KEY, [withdraw, start_challenge]) 216 | -------------------------------------------------------------------------------- /examples/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set bitcoin-cli with regtest option as a variable for simplicity 4 | BITCOIN_CLI="bitcoin-cli -regtest" 5 | 6 | # Try to create the wallet "testwallet" 7 | WALLET_CREATE_RESULT=$($BITCOIN_CLI createwallet "testwallet" 2>&1) 8 | 9 | # If the wallet already exists, load it 10 | if [[ $WALLET_CREATE_RESULT == *"Database already exists"* ]]; then 11 | echo "Wallet 'testwallet' already exists. Loading it..." 12 | $BITCOIN_CLI loadwallet "testwallet" 13 | fi 14 | 15 | # Get a new address from "testwallet" 16 | NEW_ADDRESS=$($BITCOIN_CLI -rpcwallet=testwallet getnewaddress) 17 | 18 | # Check for valid address before proceeding 19 | if [[ -z $NEW_ADDRESS ]]; then 20 | echo "Failed to get a new address." 21 | exit 1 22 | fi 23 | 24 | # Generate 300 blocks, sending the block reward to the new address 25 | $BITCOIN_CLI generatetoaddress 300 $NEW_ADDRESS 26 | 27 | echo "Generated 300 blocks to address: $NEW_ADDRESS" 28 | -------------------------------------------------------------------------------- /examples/ram/README.md: -------------------------------------------------------------------------------- 1 | # RAM 2 | 3 | `ram.py` is a simple contract that allows a Script to commit to some memory, and modify it in successive executions. 4 | 5 | It is a building block for more complex smart contracts that require "memory" access. 6 | 7 | ## Prerequisites 8 | 9 | After following the [root prerequisites](../..#prerequisites), make sure to install the additional requirements: 10 | 11 | ```bash 12 | $ pip install -r requirements.txt 13 | ``` 14 | 15 | ## How to Run 16 | 17 | `ram.py` is a command line tool that allows to create, manage and spend the Vault UTXOs. 18 | 19 | To run the script, navigate to the directory containing `vault.py` and use the following command: 20 | 21 | ```bash 22 | $ python ram.py -m 23 | ``` 24 | 25 | ## Command-line Arguments 26 | 27 | - `--mine-automatically` or `-m`: Enables automatic mining any time transactions are broadcast (assuming a wallet is loaded in bitcoin-core). 28 | - `--script` or `-s`: Executes commands from a specified script file, instead of running the interactive CLI interface. Some examples are in the (script)[scripts] folder. 29 | 30 | ## Interactive Commands 31 | 32 | While typing commands in interactive mode, the script offers auto-completion features to assist you. 33 | 34 | You can use the following commands to work with regtest: 35 | - `fund`: Funds the vault with a specified amount. 36 | - `mine [n]`: mines 1 or `n` blocks. 37 | 38 | The following commands allows to inspect the current state and history of known UTXOs: 39 | 40 | - `list`: Lists available UTXOs known to the ContractManager. 41 | - `printall`: Prints in a nice formats for Markdown all the transactions from known UTXOs. 42 | 43 | The following commands implement specific features of the vault UTXOs (trigger, recover, withdraw). Autocompletion can help 44 | 45 | - `withdraw`: Given the proof for the value of an element, withdraw from the contract. 46 | - `write i value`: Given a valid proof for the value of the `i`-th element, updates the state but replacing it with `value`. -------------------------------------------------------------------------------- /examples/ram/ram.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | 4 | import os 5 | 6 | import logging 7 | import shlex 8 | import traceback 9 | from typing import Dict, List, Tuple 10 | 11 | from dotenv import load_dotenv 12 | 13 | from prompt_toolkit import prompt 14 | from prompt_toolkit.completion import Completer, Completion 15 | from prompt_toolkit.history import FileHistory 16 | 17 | from matt.btctools.auth_proxy import AuthServiceProxy 18 | 19 | from matt.btctools import key 20 | from matt.btctools.messages import CTransaction, CTxOut, sha256 21 | from matt.environment import Environment 22 | from matt.manager import ContractManager 23 | from matt.merkle import MerkleTree 24 | from matt.utils import addr_to_script, format_tx_markdown 25 | 26 | from ram_contracts import RAM 27 | 28 | logging.basicConfig(filename='matt-cli.log', level=logging.DEBUG) 29 | 30 | 31 | class ActionArgumentCompleter(Completer): 32 | ACTION_ARGUMENTS = { 33 | "fund": ["amount="], 34 | "list": [], 35 | "printall": [], 36 | "withdraw": ["item=", "leaf_index=", "outputs=\"["], 37 | "write": ["item=", "leaf_index=", "new_value="], 38 | } 39 | 40 | def get_completions(self, document, complete_event): 41 | word_before_cursor = document.get_word_before_cursor(WORD=True) 42 | 43 | if ' ' not in document.text: 44 | # user is typing the action 45 | for action in self.ACTION_ARGUMENTS.keys(): 46 | if action.startswith(word_before_cursor): 47 | yield Completion(action, start_position=-len(word_before_cursor)) 48 | else: 49 | # user is typing an argument, find which are valid 50 | action = document.text.split()[0] 51 | for argument in self.ACTION_ARGUMENTS.get(action, []): 52 | if argument not in document.text and argument.startswith(word_before_cursor): 53 | yield Completion(argument, start_position=-len(word_before_cursor)) 54 | 55 | 56 | load_dotenv() 57 | 58 | rpc_user = os.getenv("RPC_USER", "rpcuser") 59 | rpc_password = os.getenv("RPC_PASSWORD", "rpcpass") 60 | rpc_host = os.getenv("RPC_HOST", "localhost") 61 | rpc_port = os.getenv("RPC_PORT", 18443) 62 | rpc_wallet_name = os.getenv("RPC_WALLET_NAME", "testwallet") 63 | 64 | 65 | def parse_outputs(output_strings: List[str]) -> List[Tuple[str, int]]: 66 | """Parses a list of strings in the form "address:amount" into a list of (address, amount) tuples. 67 | 68 | Args: 69 | - output_strings (list of str): List of strings in the form "address:amount". 70 | 71 | Returns: 72 | - list of (str, int): List of (address, amount) tuples. 73 | """ 74 | outputs = [] 75 | for output_str in output_strings: 76 | address, amount_str = output_str.split(":") 77 | amount = int(amount_str) 78 | if amount <= 0: 79 | raise ValueError(f"Invalid amount for address {address}: {amount_str}") 80 | outputs.append((address, amount)) 81 | return outputs 82 | 83 | 84 | def execute_command(input_line: str): 85 | # consider lines starting with '#' (possibly prefixed with whitespaces) as comments 86 | if input_line.strip().startswith("#"): 87 | return 88 | 89 | # Split into a command and the list of arguments 90 | try: 91 | input_line_list = shlex.split(input_line) 92 | except ValueError as e: 93 | print(f"Invalid command: {str(e)}") 94 | return 95 | 96 | # Ensure input_line_list is not empty 97 | if input_line_list: 98 | action = input_line_list[0].strip() 99 | else: 100 | return 101 | 102 | # Get the necessary arguments from input_command_list 103 | args_dict = {} 104 | pos_count = 0 # count of positional arguments 105 | for item in input_line_list[1:]: 106 | parts = item.strip().split('=', 1) 107 | if len(parts) == 2: 108 | param, value = parts 109 | args_dict[param] = value 110 | else: 111 | # record positional arguments with keys @0, @1, ... 112 | args_dict['@' + str(pos_count)] = parts[0] 113 | pos_count += 1 114 | 115 | if action == "": 116 | return 117 | elif action not in actions: 118 | print("Invalid action") 119 | return 120 | elif action == "list": 121 | for i, instance in enumerate(manager.instances): 122 | print(i, instance.status.name, 123 | f"{instance.contract} data={None if instance.data is None else instance.data.hex()} value={instance.get_value()} outpoint={(instance.outpoint.hash).to_bytes(32, byteorder='big').hex()}:{instance.outpoint.n}") 124 | elif action == "mine": 125 | if '@0' in args_dict: 126 | n_blocks = int(args_dict['@0']) 127 | else: 128 | n_blocks = 1 129 | print(repr(manager._mine_blocks(n_blocks))) 130 | elif action == "printall": 131 | all_txs = {} 132 | for i, instance in enumerate(manager.instances): 133 | if instance.spending_tx is not None: 134 | all_txs[instance.spending_tx.hash] = (instance.contract.__class__.__name__, instance.spending_tx) 135 | 136 | for msg, tx in all_txs.values(): 137 | print(format_tx_markdown(tx, msg)) 138 | elif action == "withdraw": 139 | item_index = int(args_dict["item"]) 140 | leaf_index = int(args_dict["leaf_index"]) 141 | outputs_amounts = parse_outputs(json.loads(args_dict["outputs"])) 142 | 143 | if item_index not in range(len(manager.instances)): 144 | raise ValueError("Invalid item") 145 | 146 | R_inst = manager.instances[item_index] 147 | assert isinstance(R_inst.data_expanded, RAM.State) 148 | 149 | mt = MerkleTree(R_inst.data_expanded.leaves) 150 | 151 | if leaf_index not in range(len(R_inst.data_expanded.leaves)): 152 | raise ValueError("Invalid leaf index") 153 | 154 | outputs = [] 155 | for address, amount in outputs_amounts: 156 | outputs.append(CTxOut( 157 | nValue=amount, 158 | scriptPubKey=addr_to_script(address) 159 | ) 160 | ) 161 | 162 | R_inst("withdraw", outputs=outputs)( 163 | merkle_root=mt.root, 164 | merkle_proof=mt.prove_leaf(leaf_index) 165 | ) 166 | 167 | print("Done") 168 | elif action == "write": 169 | item_index = int(args_dict["item"]) 170 | leaf_index = int(args_dict["leaf_index"]) 171 | new_value = sha256(bytes.fromhex(args_dict["new_value"])) 172 | 173 | if item_index not in range(len(manager.instances)): 174 | raise ValueError("Invalid item") 175 | 176 | R_inst = manager.instances[item_index] 177 | 178 | assert isinstance(R_inst.data_expanded, RAM.State) 179 | 180 | mt = MerkleTree(R_inst.data_expanded.leaves) 181 | 182 | if leaf_index not in range(len(R_inst.data_expanded.leaves)): 183 | raise ValueError("Invalid leaf index") 184 | 185 | result = R_inst("write")( 186 | merkle_root=mt.root, 187 | new_value=new_value, 188 | merkle_proof=mt.prove_leaf(leaf_index) 189 | ) 190 | 191 | assert len(result) == 1 192 | 193 | print("Done") 194 | elif action == "fund": 195 | amount = int(args_dict["amount"]) 196 | content = [sha256(i.to_bytes(1, byteorder='little')) for i in range(8)] 197 | 198 | R = RAM(len(content)) 199 | R_inst = manager.fund_instance(R, amount, data=R.State(content)) 200 | 201 | print(R_inst.funding_tx) 202 | 203 | 204 | def cli_main(): 205 | completer = ActionArgumentCompleter() 206 | # Create a history object 207 | history = FileHistory('.cli-history') 208 | 209 | while True: 210 | try: 211 | input_line = prompt("₿ ", history=history, completer=completer) 212 | execute_command(input_line) 213 | except (KeyboardInterrupt, EOFError): 214 | raise # exit 215 | except Exception as err: 216 | print(f"Error: {err}") 217 | print(traceback.format_exc()) 218 | 219 | 220 | def script_main(script_filename: str): 221 | with open(script_filename, "r") as script_file: 222 | for input_line in script_file: 223 | try: 224 | # Assuming each command can be executed in a similar manner to the CLI 225 | # This will depend on the structure of the main() function and may need adjustments 226 | execute_command(input_line) 227 | except Exception as e: 228 | print(f"Error executing command: {input_line.strip()} - Error: {str(e)}") 229 | break 230 | 231 | 232 | if __name__ == "__main__": 233 | parser = argparse.ArgumentParser() 234 | 235 | # Mine automatically option 236 | parser.add_argument("--mine-automatically", "-m", action="store_true", help="Mine automatically") 237 | 238 | # Script file option 239 | parser.add_argument("--script", "-s", type=str, help="Execute commands from script file") 240 | 241 | args = parser.parse_args() 242 | 243 | actions = ["fund", "mine", "list", "printall", "withdraw", "write"] 244 | 245 | unvault_priv_key = key.ExtendedKey.deserialize( 246 | "tprv8ZgxMBicQKsPdpwA4vW8DcSdXzPn7GkS2RdziGXUX8k86bgDQLKhyXtB3HMbJhPFd2vKRpChWxgPe787WWVqEtjy8hGbZHqZKeRrEwMm3SN") 247 | recover_priv_key = key.ExtendedKey.deserialize( 248 | "tprv8ZgxMBicQKsPeDvaW4xxmiMXxqakLgvukT8A5GR6mRwBwjsDJV1jcZab8mxSerNcj22YPrusm2Pz5oR8LTw9GqpWT51VexTNBzxxm49jCZZ") 249 | 250 | rpc = AuthServiceProxy(f"http://{rpc_user}:{rpc_password}@{rpc_host}:{rpc_port}/wallet/{rpc_wallet_name}") 251 | 252 | manager = ContractManager(rpc, mine_automatically=args.mine_automatically) 253 | environment = Environment(rpc, manager, None, None, False) 254 | 255 | # map from known ctv hashes to the corresponding template (used for withdrawals) 256 | ctv_templates: Dict[bytes, CTransaction] = {} 257 | 258 | if args.script: 259 | script_main(args.script) 260 | else: 261 | try: 262 | cli_main() 263 | except (KeyboardInterrupt, EOFError): 264 | pass # exit 265 | -------------------------------------------------------------------------------- /examples/ram/ram_contracts.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | from matt import CCV_FLAG_CHECK_INPUT, NUMS_KEY 5 | from matt.argtypes import BytesType, MerkleProofType 6 | from matt.btctools.script import OP_CAT, OP_CHECKCONTRACTVERIFY, OP_DUP, OP_ELSE, OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_IF, OP_NOTIF, OP_PICK, OP_ROLL, OP_ROT, OP_SHA256, OP_SWAP, OP_TOALTSTACK, OP_TRUE, CScript 7 | from matt.contracts import ClauseOutput, StandardClause, StandardAugmentedP2TR, ContractState 8 | from matt.merkle import MerkleTree, is_power_of_2, floor_lg 9 | from matt.script_helpers import merkle_root 10 | 11 | 12 | class RAM(StandardAugmentedP2TR): 13 | @dataclass 14 | class State(ContractState): 15 | leaves: List[bytes] 16 | 17 | def encode(self): 18 | return MerkleTree(self.leaves).root 19 | 20 | def encoder_script(size: int): 21 | return merkle_root(size) 22 | 23 | def __init__(self, size: int): 24 | assert is_power_of_2(size) 25 | 26 | self.size = size 27 | 28 | n = floor_lg(size) 29 | self.n = n 30 | 31 | # witness: ... 32 | withdraw = StandardClause( 33 | name="withdraw", 34 | script=CScript([ 35 | OP_DUP, 36 | OP_TOALTSTACK, 37 | 38 | # check that the top of the stack is the embedded data 39 | -1, # index 40 | 0, # pk 41 | -1, # taptree 42 | CCV_FLAG_CHECK_INPUT, 43 | OP_CHECKCONTRACTVERIFY, 44 | 45 | # stack: ... 46 | # alt : 47 | 48 | # repeat until the root is computed 49 | # TODO: we could save an opcode by modifying the order of witness elements 50 | *([ 51 | OP_SWAP, # put direction on top 52 | # TODO: should we check that it's either exactly 0 or exactly 1? 53 | 54 | OP_NOTIF, 55 | # left child; swap, as we want x || h_i 56 | OP_SWAP, 57 | OP_ENDIF, 58 | 59 | OP_CAT, 60 | OP_SHA256 61 | ] * n), 62 | 63 | OP_FROMALTSTACK, 64 | OP_EQUAL 65 | ]), 66 | arg_specs=[ 67 | ("merkle_proof", MerkleProofType(n)), 68 | ('merkle_root', BytesType()), 69 | ] 70 | ) 71 | 72 | def next_outputs_fn(args: dict, state: RAM.State): 73 | i: int = args["merkle_proof"].get_leaf_index() 74 | 75 | return [ 76 | ClauseOutput( 77 | n=-1, 78 | next_contract=self, 79 | next_state=self.State( 80 | leaves=state.leaves[:i] + [args["new_value"]] + state.leaves[i+1:] 81 | ) 82 | ) 83 | ] 84 | 85 | # witness: ... 86 | write = StandardClause( 87 | name="write", 88 | script=CScript([ 89 | OP_DUP, 90 | OP_TOALTSTACK, 91 | 92 | # stack: ... 93 | # alt : 94 | 95 | # check that the top of the stack is the embedded data 96 | -1, # index 97 | 0, # pk 98 | -1, # taptree 99 | CCV_FLAG_CHECK_INPUT, 100 | OP_CHECKCONTRACTVERIFY, 101 | 102 | # stack: ... 103 | # alt : 104 | 105 | # repeat until both the old and new roots are computed 106 | *([ 107 | 2, OP_ROLL, # put direction on top 108 | 109 | # TODO: should we check that the direction is either exactly 0 or exactly 1? 110 | 111 | # TODO: seems too verbose, there should be a way of optimizing it 112 | # top of stack is now: 113 | OP_IF, 114 | # top of stack is now: 115 | # right child: we want h || x 116 | 2, OP_PICK, 117 | # top of stack is now: 118 | OP_SWAP, 119 | OP_CAT, 120 | OP_SHA256, 121 | # top of stack is now: 122 | 123 | OP_SWAP, 124 | # top of stack is now: 125 | OP_ROT, 126 | # top of stack is now: 127 | OP_SWAP, 128 | # OP_CAT, 129 | # OP_SHA256, 130 | # # top of stack is now: 131 | 132 | # OP_SWAP, 133 | # # top of stack is now: 134 | OP_ELSE, 135 | # top of stack is now: 136 | 2, OP_PICK, 137 | # top of stack is now: 138 | OP_CAT, 139 | OP_SHA256, 140 | # top of stack is now: 141 | 142 | OP_SWAP, 143 | OP_ROT, 144 | # top of stack is now: 145 | 146 | # OP_CAT, 147 | # OP_SHA256, 148 | # # top of stack is now: 149 | 150 | # OP_SWAP, 151 | # # top of stack is now: 152 | OP_ENDIF, 153 | 154 | # this is in common between the two branches, so we can put it here 155 | OP_CAT, 156 | OP_SHA256, 157 | OP_SWAP, 158 | 159 | ] * n), 160 | 161 | # stack: 162 | # alt : 163 | 164 | # check that ineed old_root_computed == root as expected 165 | OP_SWAP, 166 | OP_FROMALTSTACK, 167 | OP_EQUALVERIFY, 168 | 169 | # stack: 170 | 171 | # Check that new_root is committed in the next output, 172 | -1, # index 173 | 0, # NUMS 174 | -1, # keep current taptree 175 | 0, # default, preserve amount 176 | OP_CHECKCONTRACTVERIFY, 177 | 178 | OP_TRUE 179 | ]), 180 | arg_specs=[ 181 | ("merkle_proof", MerkleProofType(n)), 182 | # the new value of the element (its index is specified by the directions in the merkle proof) 183 | ('new_value', BytesType()), 184 | ('merkle_root', BytesType()), 185 | ], 186 | next_outputs_fn=next_outputs_fn 187 | ) 188 | 189 | super().__init__(NUMS_KEY, [withdraw, write]) 190 | -------------------------------------------------------------------------------- /examples/ram/requirements.txt: -------------------------------------------------------------------------------- 1 | prompt-toolkit>=3.0.31,<3.1 2 | python-dotenv==0.13.0 3 | -------------------------------------------------------------------------------- /examples/rps/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Rock, Paper, Scissors (RPS) with Bitcoin Protocol 3 | 4 | This script implements the Rock, Paper, Scissors game based on a protocol described [here](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2023-May/021599.html). 5 | 6 | 7 | ## Prerequisites 8 | 9 | After following the [root prerequisites](../..#prerequisites), make sure to install the additional requirements: 10 | 11 | ```bash 12 | $ pip install -r requirements.txt 13 | ``` 14 | 15 | ## How to Run: 16 | 17 | The game can be played as either Alice or Bob, and you can specify your move (rock, paper, scissors). Additionally, other options like non-interactive mode and automatic mining can be set. 18 | 19 | ```bash 20 | python rps.py --alice/--bob [--rock/--paper/--scissors] [--non-interactive] [--mine-automatically] [--host HOST] [--port PORT] 21 | ``` 22 | 23 | In order to play a game, run `python rps.py --alice` on a shell, and `python rps.py --bob` on a separate shell. 24 | 25 | The two scripts will establish a socket to communicate and negotiate a game UTXO. 26 | 27 | Once the UTXO is funded (NOTE: it must be funded externally), the two scripts proceed to play the game. 28 | 29 | ### Arguments: 30 | 31 | - `--alice` / `--bob`: Specify the player you want to play as. 32 | - `--rock` / `--paper` / `--scissors`: Specify your move. If ommitted, a random move is chosen. 33 | - `--non-interactive`: Run in non-interactive mode (if not enabled, the user has to confirm each action). 34 | - `--mine-automatically`: Enable automatic mining when transactions are broadcast. 35 | - `--host`: Specify the host address (default is `localhost`). 36 | - `--port`: Specify the port number (default is `12345`). 37 | -------------------------------------------------------------------------------- /examples/rps/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv==0.13.0 2 | -------------------------------------------------------------------------------- /examples/rps/rps.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements the protocol described in https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2023-May/021599.html 3 | 4 | ### Setup 5 | 6 | Alice has a pk_a, and bob has a pk_b. 7 | 8 | 9 | Alice Bob 10 | 11 | choose m_a <-- {0, 1, 2} 12 | r_a <$-- {0, 1}^256 13 | c_a = SHA256(m_a || r_a) 14 | 15 | pk_a, c_a 16 | |------------------------> 17 | 18 | Compute the RPS(c_a, pk_a, pk_b) UTXO 19 | Create a PSBTv2 psbt_game with his inputs, his change output, and the contract output 20 | pk_b, psbt_game_partial 21 | <------------------------| 22 | 23 | Verify that RPS(c_a, pk_a, pk_b) is in the psbt. 24 | Add her own inputs and change output, obtaining psbt_game. 25 | Sign psbt_game. 26 | 27 | psbt_game 28 | |------------------------> 29 | Sign psbt_game. 30 | Finalize the psbt, broadcast the transaction. 31 | 32 | 33 | ### Gameplay 34 | 35 | Once the transaction is confirmed, both parties monitor the UTXO containing the game instance, 36 | and play the moves when it's their turn, as per the rules. 37 | 38 | """ 39 | 40 | import argparse 41 | import socket 42 | import json 43 | import random 44 | import os 45 | 46 | from dotenv import load_dotenv 47 | 48 | from matt.btctools.auth_proxy import AuthServiceProxy 49 | 50 | import matt.btctools.key as key 51 | from matt.btctools.messages import sha256 52 | import matt.btctools.script as script 53 | from matt.environment import Environment 54 | from matt.manager import ContractInstance, ContractManager, SchnorrSigner 55 | 56 | from rps_contracts import DEFAULT_STAKE, RPS, RPSGameS0 57 | 58 | 59 | load_dotenv() 60 | 61 | rpc_user = os.getenv("RPC_USER", "rpcuser") 62 | rpc_password = os.getenv("RPC_PASSWORD", "rpcpass") 63 | rpc_host = os.getenv("RPC_HOST", "localhost") 64 | rpc_port = os.getenv("RPC_PORT", 18443) 65 | rpc_wallet_name = os.getenv("RPC_WALLET_NAME", "testwallet") 66 | 67 | 68 | class AliceGame: 69 | def __init__(self, env: Environment, args: dict): 70 | self.env = env 71 | self.args = args 72 | 73 | self.priv_key = key.ExtendedKey.deserialize( 74 | "tprv8ZgxMBicQKsPdpwA4vW8DcSdXzPn7GkS2RdziGXUX8k86bgDQLKhyXtB3HMbJhPFd2vKRpChWxgPe787WWVqEtjy8hGbZHqZKeRrEwMm3SN") 75 | 76 | def start_session(self, m_a: int): 77 | assert 0 <= m_a <= 2 78 | 79 | # Beginning of the protocol: exchange of pubkeys 80 | 81 | print(f"Alice's move: {m_a} ({RPS.move_str(m_a)})") 82 | 83 | r_a = os.urandom(32) 84 | c_a = RPS.calculate_hash(m_a, r_a) 85 | 86 | print("Waiting for Bob...") 87 | 88 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 89 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 90 | s.bind((self.env.host, self.env.port)) 91 | s.listen(1) 92 | conn, _ = s.accept() 93 | 94 | pk_a = self.priv_key.pubkey[1:] # x-only pubkey 95 | 96 | conn.send(json.dumps({'c_a': c_a.hex(), 'pk_a': pk_a.hex()}).encode()) 97 | 98 | bob_msg = json.loads(conn.recv(1024).decode()) 99 | 100 | pk_b = bytes.fromhex(bob_msg['pk_b']) 101 | print(f"Alice's state: m_a={m_a}, r_a={r_a.hex()}, c_a={c_a.hex()}, pk_a={pk_a.hex()}, pk_b={pk_b.hex()}") 102 | 103 | M = self.env.manager 104 | 105 | # Create initial smart contract UTXO 106 | S0 = RPSGameS0(pk_a, pk_b, c_a) 107 | 108 | if self.args.mine_automatically: 109 | C = manager.fund_instance(S0, 2 * DEFAULT_STAKE) 110 | else: 111 | C = ContractInstance(S0) 112 | M.add_instance(C) 113 | M.wait_for_outpoint(C) 114 | 115 | # Wait for bob to spend it 116 | 117 | print("Waiting for Bob's move...") 118 | 119 | print(f"Outpoint: {hex(C.outpoint.hash)}:{C.outpoint.n}") 120 | [C2] = M.wait_for_spend(C) 121 | 122 | # Decode bob's move 123 | m_b: int = C.spending_args['m_b'] 124 | assert 0 <= m_b <= 2 125 | 126 | print(f"Bob's move: {m_b} ({RPS.move_str(m_b)}).") 127 | 128 | outcome = RPS.adjudicate(m_a, m_b) 129 | print(f"Game result: {outcome}") 130 | 131 | self.env.prompt("Broadcasting adjudication transaction") 132 | C2(outcome)(m_a=m_a, m_b=m_b, r_a=r_a) 133 | 134 | s.close() 135 | 136 | 137 | class BobGame: 138 | def __init__(self, env: Environment, args: dict): 139 | self.env = env 140 | self.args = args 141 | 142 | self.priv_key = key.ExtendedKey.deserialize( 143 | "tprv8ZgxMBicQKsPeDvaW4xxmiMXxqakLgvukT8A5GR6mRwBwjsDJV1jcZab8mxSerNcj22YPrusm2Pz5oR8LTw9GqpWT51VexTNBzxxm49jCZZ") 144 | 145 | def join_session(self, m_b: int): 146 | assert 0 <= m_b <= 2 147 | 148 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 149 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 150 | s.connect((self.env.host, self.env.port)) 151 | 152 | alice_message = json.loads(s.recv(1024).decode()) 153 | 154 | c_a = bytes.fromhex(alice_message['c_a']) 155 | pk_a = bytes.fromhex(alice_message['pk_a']) 156 | pk_b = self.priv_key.pubkey[1:] # x-only pubkey 157 | 158 | print(f"Bob's state: c_a={c_a.hex()}, pk_a={pk_a.hex()}, pk_b={pk_b.hex()}") 159 | 160 | s.send(json.dumps({'pk_b': pk_b.hex()}).encode()) 161 | 162 | # Create initial smart contract UTXO 163 | 164 | S0 = RPSGameS0(pk_a, pk_b, c_a) 165 | C = ContractInstance(S0) 166 | M = self.env.manager 167 | M.add_instance(C) 168 | 169 | print(f"Bob waiting for output: {C.get_address()}") 170 | 171 | M.wait_for_outpoint(C) 172 | 173 | # Make move 174 | 175 | m_b_hash = sha256(script.bn2vch(m_b)) 176 | 177 | print(f"Bob's move: {m_b} ({RPS.move_str(m_b)})") 178 | print(f"Bob's move's hash: {m_b_hash.hex()}") 179 | 180 | self.env.prompt("Broadcasting Bob's move transaction") 181 | 182 | [C2] = C("bob_move", SchnorrSigner(self.priv_key))(m_b=m_b) 183 | 184 | txid = C.spending_tx.hash 185 | print(f"Bob's move broadcasted: {m_b}. txid: {txid}") 186 | 187 | print("Waiting for adjudication") 188 | 189 | # Wait for Alice to adjudicate 190 | M.wait_for_spend(C2) 191 | 192 | print(f"Outcome: {C2.spending_clause}") 193 | 194 | s.close() 195 | 196 | 197 | if __name__ == "__main__": 198 | parser = argparse.ArgumentParser(description="Final command line arguments parser.", 199 | usage="%(prog)s [-a | -b] [-n] [-m] [--host HOST] [--port PORT]", 200 | epilog="Ensure that either --alice or --bob is provided.") 201 | 202 | # Group for mutually exclusive options: alice and bob 203 | group_player = parser.add_mutually_exclusive_group(required=True) 204 | group_player.add_argument("--alice", "-A", action="store_true", help="Play as Alice") 205 | group_player.add_argument("--bob", "-B", action="store_true", help="Play as Bob") 206 | 207 | group_move = parser.add_mutually_exclusive_group(required=False) 208 | group_move.add_argument("--rock", action="store_true", help="Play Rock") 209 | group_move.add_argument("--paper", action="store_true", help="Play Paper") 210 | group_move.add_argument("--scissors", action="store_true", help="Play Scissors") 211 | 212 | # Non-interactive option 213 | parser.add_argument("--non-interactive", "-n", action="store_true", help="Run in non-interactive mode") 214 | 215 | # Mine automatically option 216 | parser.add_argument("--mine-automatically", "-m", action="store_true", help="Mine automatically") 217 | 218 | # Host option 219 | parser.add_argument("--host", default="localhost", type=str, help="Host address (default: localhost)") 220 | 221 | # Port option 222 | parser.add_argument("--port", default=12345, type=int, help="Port number (default: 12345)") 223 | 224 | args = parser.parse_args() 225 | 226 | move = None 227 | if args.rock: 228 | move = 0 229 | elif args.paper: 230 | move = 1 231 | elif args.scissors: 232 | move = 2 233 | 234 | rpc = AuthServiceProxy(f"http://{rpc_user}:{rpc_password}@{rpc_host}:{rpc_port}/wallet/{rpc_wallet_name}") 235 | 236 | manager = ContractManager(rpc, mine_automatically=args.mine_automatically) 237 | environment = Environment(rpc, manager, args.host, args.port, not args.non_interactive) 238 | 239 | if args.alice: 240 | a = AliceGame(environment, args) 241 | m_a = move if move is not None else random.SystemRandom().randint(0, 2) 242 | a.start_session(m_a) 243 | else: 244 | b = BobGame(environment, args) 245 | m_b = move if move is not None else random.SystemRandom().randint(0, 2) 246 | b.join_session(m_b) 247 | -------------------------------------------------------------------------------- /examples/rps/rps_contracts.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import hashlib 3 | 4 | from matt import NUMS_KEY 5 | from matt.argtypes import BytesType, IntType, SignerType 6 | from matt.btctools.messages import sha256 7 | from matt.btctools import script 8 | from matt.btctools.script import OP_ADD, OP_CAT, OP_CHECKSIG, OP_CHECKTEMPLATEVERIFY, OP_DUP, OP_ENDIF, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_IF, OP_LESSTHAN, OP_OVER, OP_SHA256, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN, CScript 9 | from matt.contracts import P2TR, ClauseOutput, StandardClause, StandardP2TR, StandardAugmentedP2TR, ContractState 10 | from matt.script_helpers import check_input_contract, check_output_contract 11 | from matt.utils import encode_wit_element, make_ctv_template 12 | 13 | DEFAULT_STAKE: int = 1000 # amount of sats that the players bet 14 | 15 | 16 | class RPS: 17 | @staticmethod 18 | def move_str(move: int) -> str: 19 | assert 0 <= move <= 2 20 | if move == 0: 21 | return "rock" 22 | elif move == 1: 23 | return "paper" 24 | else: 25 | return "scissors" 26 | 27 | @staticmethod 28 | def adjudicate(move_alice, move_bob): 29 | assert 0 <= move_alice <= 2 and 0 <= move_bob <= 2 30 | if move_bob == move_alice: 31 | return "tie" 32 | elif (move_bob - move_alice) % 3 == 2: 33 | return "alice_wins" 34 | else: 35 | return "bob_wins" 36 | 37 | @staticmethod 38 | def calculate_hash(move: int, r: bytes) -> bytes: 39 | assert 0 <= move <= 2 and len(r) == 32 40 | 41 | m = hashlib.sha256() 42 | m.update(script.bn2vch(move) + r) 43 | return m.digest() 44 | 45 | 46 | # params: 47 | # - alice_pk 48 | # - bob_pk 49 | # - c_a 50 | # spending conditions: 51 | # - bob_pk (m_b) => RPSGameS1[m_b] 52 | class RPSGameS0(StandardP2TR): 53 | def __init__(self, alice_pk: bytes, bob_pk: bytes, c_a: bytes, stake: int = DEFAULT_STAKE): 54 | assert len(alice_pk) == 32 and len(bob_pk) == 32 and len(c_a) == 32 55 | 56 | self.alice_pk = alice_pk 57 | self.bob_pk = bob_pk 58 | self.c_a = c_a 59 | self.stake = stake 60 | 61 | S1 = RPSGameS1(alice_pk, bob_pk, c_a, stake) 62 | 63 | # witness: 64 | bob_move = StandardClause( 65 | name="bob_move", 66 | script=CScript([ 67 | bob_pk, 68 | OP_CHECKSIG, 69 | OP_SWAP, 70 | 71 | # stack on successful signature check: <1> 72 | 73 | OP_DUP, 0, 3, OP_WITHIN, OP_VERIFY, # check that m_b is 0, 1 or 2 74 | 75 | *S1.State.encoder_script(), 76 | *check_output_contract(S1, index=0), 77 | ]), 78 | arg_specs=[ 79 | ('m_b', IntType()), 80 | ('bob_sig', SignerType(bob_pk)), 81 | ], 82 | next_outputs_fn=lambda args, _: [ 83 | ClauseOutput( 84 | n=0, 85 | next_contract=S1, 86 | next_state=S1.State(m_b=args["m_b"]) 87 | )] 88 | ) 89 | 90 | super().__init__(NUMS_KEY, bob_move) 91 | 92 | 93 | # params: 94 | # - alice_pk 95 | # - bob_pk 96 | # - c_a 97 | # variables: 98 | # - m_b 99 | # spending conditions: 100 | # - alice_pk, reveal winning move => ctv(alice wins) 101 | # - alice_pk, reveal losing move => ctv(bob wins) 102 | # - alice_pk, reveal tie move => ctv(tie) 103 | class RPSGameS1(StandardAugmentedP2TR): 104 | @dataclass 105 | class State(ContractState): 106 | m_b: int 107 | 108 | def encode(self): 109 | return sha256(encode_wit_element(self.m_b)) 110 | 111 | def encoder_script(): 112 | return CScript([OP_SHA256]) 113 | 114 | def __init__(self, alice_pk: bytes, bob_pk: bytes, c_a: bytes, stake: int): 115 | self.alice_pk = alice_pk 116 | self.bob_pk = bob_pk 117 | self.c_a = c_a 118 | self.stake = stake 119 | 120 | def make_script(diff: int, ctv_hash: bytes): 121 | # diff is (m_b - m_a) % 3 122 | assert 0 <= diff <= 2 123 | # witness: [ ] 124 | 125 | return CScript([ 126 | OP_OVER, OP_DUP, OP_TOALTSTACK, # save m_a 127 | 0, 3, OP_WITHIN, OP_VERIFY, # check that m_a is 0, 1 or 2 128 | 129 | # stack: altstack: 130 | 131 | # check that SHA256(m_a || r_a) equals c_a 132 | OP_CAT, OP_SHA256, 133 | self.c_a, 134 | OP_EQUALVERIFY, 135 | 136 | OP_DUP, 137 | # stack: altstack: 138 | 139 | *self.State.encoder_script(), 140 | *check_input_contract(), 141 | 142 | # stack: altstack: 143 | 144 | OP_FROMALTSTACK, 145 | OP_SUB, 146 | 147 | # stack: 148 | 149 | OP_DUP, # if the result is negative, add 3 150 | 0, OP_LESSTHAN, 151 | OP_IF, 152 | 3, 153 | OP_ADD, 154 | OP_ENDIF, 155 | 156 | diff, # draw / Bob wins / Alice wins, respectively 157 | OP_EQUALVERIFY, 158 | 159 | ctv_hash, 160 | OP_CHECKTEMPLATEVERIFY 161 | ]) 162 | 163 | alice_spk = P2TR(self.alice_pk, []).get_tr_info().scriptPubKey 164 | bob_spk = P2TR(self.bob_pk, []).get_tr_info().scriptPubKey 165 | 166 | tmpl_alice_wins = make_ctv_template([(alice_spk, 2*self.stake)]) 167 | tmpl_bob_wins = make_ctv_template([(bob_spk, 2*self.stake)]) 168 | tmpl_tie = make_ctv_template([ 169 | (alice_spk, self.stake), 170 | (bob_spk, self.stake), 171 | ]) 172 | 173 | arg_specs = [ 174 | ('m_b', IntType()), 175 | ('m_a', IntType()), 176 | ('r_a', BytesType()), 177 | ] 178 | tie = StandardClause("tie", make_script( 179 | 0, tmpl_tie.get_standard_template_hash(0)), arg_specs, lambda _, __: tmpl_tie) 180 | bob_wins = StandardClause("bob_wins", make_script( 181 | 1, tmpl_bob_wins.get_standard_template_hash(0)), arg_specs, lambda _, __: tmpl_bob_wins) 182 | alice_wins = StandardClause("alice_wins", make_script( 183 | 2, tmpl_alice_wins.get_standard_template_hash(0)), arg_specs, lambda _, __: tmpl_alice_wins) 184 | 185 | super().__init__(NUMS_KEY, [alice_wins, [bob_wins, tie]]) 186 | -------------------------------------------------------------------------------- /examples/vault/README.md: -------------------------------------------------------------------------------- 1 | # Vault 2 | 3 | The `vault.py` script provides a command-line and interactive interface for Bitcoin vault operations, similar to the one described in the linked [bitcoin-dev mailing list post](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2023-April/021588.html). 4 | 5 | Compared to that prototype, this version uses the `CCV_FLAG_DEDUCT_OUTPUT_AMOUNT` to allow partial revaulting of a Vault UTXO. This was the only missing feature as compared to the implementation using `OP_VAULT` (albeit with some differences in the revaulting semantics). 6 | 7 | It uses the `OP_CHECKCONTRACTVERIFY` and `OP_CHECKTEMPLATEVERIFY` opcodes. 8 | 9 | ## Prerequisites 10 | 11 | After following the [root prerequisites](../..#prerequisites), make sure to install the additional requirements: 12 | 13 | ```bash 14 | $ pip install -r requirements.txt 15 | ``` 16 | 17 | ## How to Run 18 | 19 | `vault.py` is a command line tool that allows to create, manage and spend the Vault UTXOs. 20 | 21 | To run the script, navigate to the directory containing `vault.py` and use the following command: 22 | 23 | ```bash 24 | $ python vault.py -m 25 | ``` 26 | 27 | ## Command-line Arguments 28 | 29 | - `--mine-automatically` or `-m`: Enables automatic mining any time transactions are broadcast (assuming a wallet is loaded in bitcoin-core). 30 | - `--script` or `-s`: Executes commands from a specified script file, instead of running the interactive CLI interface. Some examples are in the [scripts](scripts) folder. 31 | 32 | ## Interactive Commands 33 | 34 | While typing commands in interactive mode, the script offers auto-completion features to assist you. 35 | 36 | You can use the following commands to work with regtest: 37 | - `fund`: Funds the vault with a specified amount. 38 | - `mine [n]`: mines 1 or `n` blocks. 39 | 40 | The following commands allows to inspect the current state and history of known UTXOs: 41 | 42 | - `list`: Lists available UTXOs known to the ContractManager. 43 | - `printall`: Prints in a nice formats for Markdown all the transactions from known UTXOs. 44 | 45 | The following commands implement specific features of the vault UTXOs (trigger, recover, withdraw). Autocompletion can help 46 | 47 | - `trigger`: Triggers an action with specified items and outputs. 48 | - `recover`: Recovers an item. Can be applied to one or more Vault, or Unvaulting (triggered) UTXO. 49 | - `withdraw`: Completes the withdrawal from the vault; will fail if the timelock of 10 blocks is not satisfied. 50 | 51 | The `scripts` folder has some example of interactions with the vault. 52 | 53 | ## Minivault 54 | 55 | A simplified construction that only uses `OP_CHECKCONTRACTVERIFY` (without using `OP_CHECKTEMPLATEVERIFY`) is implemented in [minivault_contracts.py](minivault_contracts.py). It has all the same features of the full construction, except that the final withdrawal must necessarily go entirely to a single P2TR address. 56 | 57 | Check out [test_minivault.py](../../tests/test_minivault.py) to see how it would be used. 58 | -------------------------------------------------------------------------------- /examples/vault/minivault_contracts.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from matt import CCV_FLAG_DEDUCT_OUTPUT_AMOUNT, NUMS_KEY 5 | from matt.argtypes import BytesType, IntType, SignerType 6 | from matt.btctools.script import OP_CHECKCONTRACTVERIFY, OP_CHECKSIG, OP_DUP, OP_PICK, OP_SWAP, OP_TRUE, CScript 7 | from matt.contracts import ClauseOutput, ClauseOutputAmountBehaviour, OpaqueP2TR, StandardClause, StandardP2TR, StandardAugmentedP2TR, ContractState 8 | from matt.script_helpers import check_input_contract, older 9 | 10 | 11 | class Vault(StandardP2TR): 12 | def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes, unvault_pk: bytes, *, has_partial_revault=True, has_early_recover=True): 13 | assert (alternate_pk is None or len(alternate_pk) == 32) and len(recover_pk) == 32 and len(unvault_pk) == 32 14 | 15 | self.alternate_pk = alternate_pk 16 | self.spend_delay = spend_delay 17 | self.recover_pk = recover_pk 18 | 19 | unvaulting = Unvaulting(alternate_pk, spend_delay, recover_pk) 20 | 21 | self.has_partial_revault = has_partial_revault 22 | self.has_early_recover = has_early_recover 23 | 24 | # witness: 25 | trigger = StandardClause( 26 | name="trigger", 27 | script=CScript([ 28 | # data and index already on the stack 29 | 0 if alternate_pk is None else alternate_pk, # pk 30 | unvaulting.get_taptree_merkle_root(), # taptree 31 | 0, # standard flags 32 | OP_CHECKCONTRACTVERIFY, 33 | 34 | unvault_pk, 35 | OP_CHECKSIG 36 | ]), 37 | arg_specs=[ 38 | ('sig', SignerType(unvault_pk)), 39 | ('withdrawal_pk', BytesType()), 40 | ('out_i', IntType()), 41 | ], 42 | next_outputs_fn=lambda args, _: [ClauseOutput( 43 | n=args['out_i'], 44 | next_contract=unvaulting, 45 | next_state=unvaulting.State(withdrawal_pk=args["withdrawal_pk"]) 46 | )] 47 | ) 48 | 49 | # witness: 50 | trigger_and_revault = StandardClause( 51 | name="trigger_and_revault", 52 | script=CScript([ 53 | 0, OP_SWAP, # no data tweak 54 | # from the witness 55 | -1, # current input's internal key 56 | -1, # current input's taptweak 57 | CCV_FLAG_DEDUCT_OUTPUT_AMOUNT, # revault output 58 | OP_CHECKCONTRACTVERIFY, 59 | 60 | # data and index already on the stack 61 | 0 if alternate_pk is None else alternate_pk, # pk 62 | unvaulting.get_taptree_merkle_root(), # taptree 63 | 0, # standard flags 64 | OP_CHECKCONTRACTVERIFY, 65 | 66 | unvault_pk, 67 | OP_CHECKSIG 68 | ]), 69 | arg_specs=[ 70 | ('sig', SignerType(unvault_pk)), 71 | ('withdrawal_pk', BytesType()), 72 | ('out_i', IntType()), 73 | ('revault_out_i', IntType()), 74 | ], 75 | next_outputs_fn=lambda args, _: [ 76 | ClauseOutput(n=args['revault_out_i'], next_contract=self, 77 | next_amount=ClauseOutputAmountBehaviour.DEDUCT_OUTPUT), 78 | ClauseOutput( 79 | n=args['out_i'], 80 | next_contract=unvaulting, 81 | next_state=unvaulting.State(withdrawal_pk=args["withdrawal_pk"])), 82 | ] 83 | ) 84 | 85 | # witness: 86 | recover = StandardClause( 87 | name="recover", 88 | script=CScript([ 89 | 0, # data 90 | OP_SWAP, # (from witness) 91 | recover_pk, # pk 92 | 0, # taptree 93 | 0, # flags 94 | OP_CHECKCONTRACTVERIFY, 95 | OP_TRUE 96 | ]), 97 | arg_specs=[ 98 | ('out_i', IntType()), 99 | ], 100 | next_outputs_fn=lambda args, _: [ClauseOutput(n=args['out_i'], next_contract=OpaqueP2TR(recover_pk))] 101 | ) 102 | 103 | if self.has_partial_revault: 104 | if self.has_early_recover: 105 | clauses = [trigger, [trigger_and_revault, recover]] 106 | else: 107 | clauses = [trigger, trigger_and_revault] 108 | else: 109 | if self.has_early_recover: 110 | clauses = [trigger, recover] 111 | else: 112 | clauses = trigger 113 | 114 | super().__init__(NUMS_KEY if alternate_pk is None else alternate_pk, clauses) 115 | 116 | 117 | class Unvaulting(StandardAugmentedP2TR): 118 | @dataclass 119 | class State(ContractState): 120 | withdrawal_pk: bytes 121 | 122 | def encode(self): 123 | return self.withdrawal_pk 124 | 125 | def encoder_script(): 126 | return CScript([]) 127 | 128 | def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes): 129 | assert (alternate_pk is None or len(alternate_pk) == 32) and len(recover_pk) == 32 130 | 131 | self.alternate_pk = alternate_pk 132 | self.spend_delay = spend_delay 133 | self.recover_pk = recover_pk 134 | 135 | # witness: 136 | withdrawal = StandardClause( 137 | name="withdraw", 138 | script=CScript([ 139 | OP_DUP, 140 | 141 | *check_input_contract(-1, alternate_pk), 142 | 143 | # Check timelock 144 | *older(self.spend_delay), 145 | 146 | # Check that the transaction output is as expected 147 | 0, # no data 148 | 0, # output index 149 | 2, OP_PICK, # withdrawal_pk 150 | 0, # no taptweak 151 | 0, # default flags 152 | OP_CHECKCONTRACTVERIFY, 153 | 154 | # withdrawal_pk is left on the stack on success 155 | ]), 156 | arg_specs=[ 157 | ('withdrawal_pk', BytesType()) 158 | ], 159 | next_outputs_fn=lambda args, _: [ClauseOutput(n=0, next_contract=OpaqueP2TR(args['withdrawal_pk']))] 160 | ) 161 | 162 | # witness: 163 | recover = StandardClause( 164 | name="recover", 165 | script=CScript([ 166 | 0, # data 167 | OP_SWAP, # (from witness) 168 | recover_pk, # pk 169 | 0, # taptree 170 | 0, # flags 171 | OP_CHECKCONTRACTVERIFY, 172 | OP_TRUE 173 | ]), 174 | arg_specs=[ 175 | ('out_i', IntType()), 176 | ], 177 | next_outputs_fn=lambda args, _: [ClauseOutput(n=args['out_i'], next_contract=OpaqueP2TR(recover_pk))] 178 | ) 179 | 180 | super().__init__(NUMS_KEY if alternate_pk is None else alternate_pk, [withdrawal, recover]) 181 | -------------------------------------------------------------------------------- /examples/vault/requirements.txt: -------------------------------------------------------------------------------- 1 | prompt-toolkit>=3.0.31,<3.1 2 | python-dotenv==0.13.0 3 | -------------------------------------------------------------------------------- /examples/vault/scripts/normal-1-input-3-outputs.txt: -------------------------------------------------------------------------------- 1 | fund amount=49999900 2 | 3 | trigger items="[0]" outputs="[\"bcrt1qqy0kdmv0ckna90ap6efd6z39wcdtpfa3a27437:1666663333\",\"bcrt1qpnpjyzkfe7n5eppp2ktwpvuxfw5qfn2zjdum83:1666663333\",\"bcrt1q6vqduw24yjjll6nfkxlfy2twwt52w58tnvnd46:16663334\"]" 4 | 5 | # make sure the timelock expires 6 | mine 10 7 | 8 | withdraw item=1 9 | 10 | printall 11 | 12 | -------------------------------------------------------------------------------- /examples/vault/scripts/normal-with-revault-3-inputs.txt: -------------------------------------------------------------------------------- 1 | fund amount=49999900 2 | fund amount=49999900 3 | fund amount=49999900 4 | 5 | trigger items="[0,1,2]" outputs="[\"bcrt1qqy0kdmv0ckna90ap6efd6z39wcdtpfa3a27437:49999900\",\"bcrt1qpnpjyzkfe7n5eppp2ktwpvuxfw5qfn2zjdum83:49999900\",\"bcrt1q6vqduw24yjjll6nfkxlfy2twwt52w58tnvnd46:29999900\"]" 6 | 7 | printall 8 | 9 | -------------------------------------------------------------------------------- /examples/vault/scripts/recover-from-trigger-1-input.txt: -------------------------------------------------------------------------------- 1 | fund amount=49999900 2 | 3 | trigger items="[0]" outputs="[\"bcrt1qqy0kdmv0ckna90ap6efd6z39wcdtpfa3a27437:49999900\"]" 4 | 5 | # make sure the timelock expires 6 | mine 10 7 | 8 | recover item=1 9 | 10 | printall 11 | 12 | -------------------------------------------------------------------------------- /examples/vault/scripts/recover-from-vault-1-input.txt: -------------------------------------------------------------------------------- 1 | fund amount=49999900 2 | 3 | recover item=0 4 | 5 | printall 6 | 7 | -------------------------------------------------------------------------------- /examples/vault/vault_contracts.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from matt import CCV_FLAG_DEDUCT_OUTPUT_AMOUNT, NUMS_KEY 5 | from matt.argtypes import BytesType, IntType, SignerType 6 | from matt.btctools.script import OP_CHECKCONTRACTVERIFY, OP_CHECKSIG, OP_CHECKTEMPLATEVERIFY, OP_DUP, OP_SWAP, OP_TRUE, CScript 7 | from matt.contracts import ClauseOutput, ClauseOutputAmountBehaviour, OpaqueP2TR, StandardClause, StandardP2TR, StandardAugmentedP2TR, ContractState 8 | from matt.script_helpers import check_input_contract, older 9 | 10 | 11 | class Vault(StandardP2TR): 12 | def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes, unvault_pk: bytes, *, has_partial_revault=True, has_early_recover=True): 13 | assert (alternate_pk is None or len(alternate_pk) == 32) and len(recover_pk) == 32 and len(unvault_pk) == 32 14 | 15 | self.alternate_pk = alternate_pk 16 | self.spend_delay = spend_delay 17 | self.recover_pk = recover_pk 18 | 19 | unvaulting = Unvaulting(alternate_pk, spend_delay, recover_pk) 20 | 21 | self.has_partial_revault = has_partial_revault 22 | self.has_early_recover = has_early_recover 23 | 24 | # witness: 25 | trigger = StandardClause( 26 | name="trigger", 27 | script=CScript([ 28 | # data and index already on the stack 29 | 0 if alternate_pk is None else alternate_pk, # pk 30 | unvaulting.get_taptree_merkle_root(), # taptree 31 | 0, # standard flags 32 | OP_CHECKCONTRACTVERIFY, 33 | 34 | unvault_pk, 35 | OP_CHECKSIG 36 | ]), 37 | arg_specs=[ 38 | ('sig', SignerType(unvault_pk)), 39 | ('ctv_hash', BytesType()), 40 | ('out_i', IntType()), 41 | ], 42 | next_outputs_fn=lambda args, _: [ClauseOutput( 43 | n=args['out_i'], 44 | next_contract=unvaulting, 45 | next_state=unvaulting.State(ctv_hash=args["ctv_hash"]) 46 | )] 47 | ) 48 | 49 | # witness: 50 | trigger_and_revault = StandardClause( 51 | name="trigger_and_revault", 52 | script=CScript([ 53 | 0, OP_SWAP, # no data tweak 54 | # from the witness 55 | -1, # current input's taptweak 56 | -1, # taptree 57 | CCV_FLAG_DEDUCT_OUTPUT_AMOUNT, # revault output 58 | OP_CHECKCONTRACTVERIFY, 59 | 60 | # data and index already on the stack 61 | 0 if alternate_pk is None else alternate_pk, # pk 62 | unvaulting.get_taptree_merkle_root(), # taptree 63 | 0, # standard flags 64 | OP_CHECKCONTRACTVERIFY, 65 | 66 | unvault_pk, 67 | OP_CHECKSIG 68 | ]), 69 | arg_specs=[ 70 | ('sig', SignerType(unvault_pk)), 71 | ('ctv_hash', BytesType()), 72 | ('out_i', IntType()), 73 | ('revault_out_i', IntType()), 74 | ], 75 | next_outputs_fn=lambda args, _: [ 76 | ClauseOutput(n=args['revault_out_i'], next_contract=self, 77 | next_amount=ClauseOutputAmountBehaviour.DEDUCT_OUTPUT), 78 | ClauseOutput( 79 | n=args['out_i'], 80 | next_contract=unvaulting, 81 | next_state=unvaulting.State(ctv_hash=args["ctv_hash"])), 82 | ] 83 | ) 84 | 85 | # witness: 86 | recover = StandardClause( 87 | name="recover", 88 | script=CScript([ 89 | 0, # data 90 | OP_SWAP, # (from witness) 91 | recover_pk, # pk 92 | 0, # taptree 93 | 0, # flags 94 | OP_CHECKCONTRACTVERIFY, 95 | OP_TRUE 96 | ]), 97 | arg_specs=[ 98 | ('out_i', IntType()), 99 | ], 100 | next_outputs_fn=lambda args, _: [ClauseOutput(n=args['out_i'], next_contract=OpaqueP2TR(recover_pk))] 101 | ) 102 | 103 | if self.has_partial_revault: 104 | if self.has_early_recover: 105 | clauses = [trigger, [trigger_and_revault, recover]] 106 | else: 107 | clauses = [trigger, trigger_and_revault] 108 | else: 109 | if self.has_early_recover: 110 | clauses = [trigger, recover] 111 | else: 112 | clauses = trigger 113 | 114 | super().__init__(NUMS_KEY if alternate_pk is None else alternate_pk, clauses) 115 | 116 | 117 | class Unvaulting(StandardAugmentedP2TR): 118 | @dataclass 119 | class State(ContractState): 120 | ctv_hash: bytes 121 | 122 | def encode(self): 123 | return self.ctv_hash 124 | 125 | def encoder_script(): 126 | return CScript([]) 127 | 128 | def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes): 129 | assert (alternate_pk is None or len(alternate_pk) == 32) and len(recover_pk) == 32 130 | 131 | self.alternate_pk = alternate_pk 132 | self.spend_delay = spend_delay 133 | self.recover_pk = recover_pk 134 | 135 | # witness: 136 | withdrawal = StandardClause( 137 | name="withdraw", 138 | script=CScript([ 139 | OP_DUP, 140 | 141 | *check_input_contract(-1, alternate_pk), 142 | 143 | # Check timelock 144 | *older(self.spend_delay), 145 | 146 | # Check that the transaction output is as expected 147 | OP_CHECKTEMPLATEVERIFY 148 | ]), 149 | arg_specs=[ 150 | ('ctv_hash', BytesType()) 151 | ] 152 | ) 153 | 154 | # witness: 155 | recover = StandardClause( 156 | name="recover", 157 | script=CScript([ 158 | 0, # data 159 | OP_SWAP, # (from witness) 160 | recover_pk, # pk 161 | 0, # taptree 162 | 0, # flags 163 | OP_CHECKCONTRACTVERIFY, 164 | OP_TRUE 165 | ]), 166 | arg_specs=[ 167 | ('out_i', IntType()), 168 | ], 169 | next_outputs_fn=lambda args, _: [ClauseOutput(n=args['out_i'], next_contract=OpaqueP2TR(recover_pk))] 170 | ) 171 | 172 | super().__init__(NUMS_KEY if alternate_pk is None else alternate_pk, [withdrawal, recover]) 173 | -------------------------------------------------------------------------------- /matt/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Flags for OP_CHECKCONTRACTVERIFY 3 | CCV_FLAG_CHECK_INPUT: int = -1 4 | CCV_FLAG_IGNORE_OUTPUT_AMOUNT: int = 1 5 | CCV_FLAG_DEDUCT_OUTPUT_AMOUNT: int = 2 6 | 7 | # point with provably unknown private key 8 | NUMS_KEY: bytes = bytes.fromhex("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0") 9 | -------------------------------------------------------------------------------- /matt/argtypes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Tuple 2 | from abc import ABC, abstractmethod 3 | 4 | from .merkle import MerkleProof 5 | from .utils import encode_wit_element, vch2bn 6 | 7 | 8 | class ArgType(ABC): 9 | """ 10 | Abstract base class for argument types in script serialization and deserialization. 11 | 12 | Subclasses implements the serialization and deserialization of argument used in 13 | contract clauses. 14 | 15 | Methods: 16 | serialize_to_wit(self, value: Any) -> List[bytes]: 17 | Serializes the provided value into a format suitable for inclusion 18 | in a witness stack. This method must be overridden in subclasses. 19 | 20 | Args: 21 | value: The value to be serialized. The type of this value depends 22 | on the specific implementation in the subclass. 23 | 24 | Returns: 25 | A list of one or more witness arguments, serialized in the format of 26 | the witness stack. 27 | 28 | deserialize_from_wit(self, wit_stack: List[bytes]) -> Tuple[int, Any]: 29 | Deserializes data from a witness stack into a Python object. This 30 | method must be overridden in subclasses. 31 | 32 | Args: 33 | wit_stack: A list of bytes representing the witness stack. This is not 34 | the full witness stack, as it does not includes the elements 35 | that were already consumed. 36 | 37 | Returns: 38 | A tuple containing two elements: 39 | - An int indicating the number of elements consumed from the wit_stack. 40 | - The deserialized value as a Python object. The exact type of this 41 | object depends on the subclass implementation. 42 | """ 43 | @abstractmethod 44 | def serialize_to_wit(self, value: Any) -> List[bytes]: 45 | raise NotImplementedError() 46 | 47 | @abstractmethod 48 | def deserialize_from_wit(self, wit_stack: List[bytes]) -> Tuple[int, Any]: 49 | raise NotImplementedError() 50 | 51 | 52 | class IntType(ArgType): 53 | def serialize_to_wit(self, value: int) -> List[bytes]: 54 | return [encode_wit_element(value)] 55 | 56 | def deserialize_from_wit(self, wit_stack: List[bytes]) -> Tuple[int, int]: 57 | return 1, vch2bn(wit_stack[0]) 58 | 59 | 60 | class BytesType(ArgType): 61 | def serialize_to_wit(self, value: int) -> List[bytes]: 62 | return [encode_wit_element(value)] 63 | 64 | def deserialize_from_wit(self, wit_stack: List[bytes]) -> Tuple[int, bytes]: 65 | return 1, wit_stack[0] 66 | 67 | 68 | class SignerType(BytesType): 69 | """ 70 | This is a special type for arguments that represent signatures in tapscripts. 71 | It is encoded as bytes, but labeling it allows the ContractManager to get the correct 72 | signatures by calling SchnorrSigner object instances. 73 | """ 74 | 75 | def __init__(self, pubkey: bytes): 76 | if len(pubkey) != 32: 77 | raise ValueError("pubkey must be an x-only pubkey") 78 | self.pubkey = pubkey 79 | 80 | 81 | class MerkleProofType(ArgType): 82 | def __init__(self, depth: int): 83 | self.depth = depth 84 | 85 | def serialize_to_wit(self, value: MerkleProof) -> List[bytes]: 86 | return value.to_wit_stack() 87 | 88 | def deserialize_from_wit(self, wit_stack: List[bytes]) -> Tuple[int, MerkleProof]: 89 | n_proof_elements = 2 * self.depth + 1 90 | if len(wit_stack) < n_proof_elements: 91 | raise ValueError("Witness stack too short") 92 | return (n_proof_elements, MerkleProof.from_wit_stack(wit_stack[:n_proof_elements])) 93 | -------------------------------------------------------------------------------- /matt/btctools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkleize/pymatt/89de499399bb8592650b5d78a7d8baef1b3ae1a9/matt/btctools/__init__.py -------------------------------------------------------------------------------- /matt/btctools/_base58.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base 58 conversion utilities 3 | **************************** 4 | """ 5 | 6 | # 7 | # base58.py 8 | # Original source: git://github.com/joric/brutus.git 9 | # which was forked from git://github.com/samrushing/caesure.git 10 | # 11 | # Distributed under the MIT/X11 software license, see the accompanying 12 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 13 | # 14 | 15 | from binascii import hexlify, unhexlify 16 | from typing import List 17 | 18 | from .common import hash256 19 | from .errors import BadArgumentError 20 | 21 | 22 | b58_digits: str = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 23 | 24 | 25 | def encode(b: bytes) -> str: 26 | """ 27 | Encode bytes to a base58-encoded string 28 | 29 | :param b: Bytes to encode 30 | :return: Base58 encoded string of ``b`` 31 | """ 32 | 33 | # Convert big-endian bytes to integer 34 | n: int = int('0x0' + hexlify(b).decode('utf8'), 16) 35 | 36 | # Divide that integer into base58 37 | temp: List[str] = [] 38 | while n > 0: 39 | n, r = divmod(n, 58) 40 | temp.append(b58_digits[r]) 41 | res: str = ''.join(temp[::-1]) 42 | 43 | # Encode leading zeros as base58 zeros 44 | czero: int = 0 45 | pad: int = 0 46 | for c in b: 47 | if c == czero: 48 | pad += 1 49 | else: 50 | break 51 | return b58_digits[0] * pad + res 52 | 53 | def decode(s: str) -> bytes: 54 | """ 55 | Decode a base58-encoding string, returning bytes 56 | 57 | :param s: Base48 string to decode 58 | :return: Bytes encoded by ``s`` 59 | """ 60 | if not s: 61 | return b'' 62 | 63 | # Convert the string to an integer 64 | n: int = 0 65 | for c in s: 66 | n *= 58 67 | if c not in b58_digits: 68 | raise BadArgumentError('Character %r is not a valid base58 character' % c) 69 | digit = b58_digits.index(c) 70 | n += digit 71 | 72 | # Convert the integer to bytes 73 | h: str = '%x' % n 74 | if len(h) % 2: 75 | h = '0' + h 76 | res = unhexlify(h.encode('utf8')) 77 | 78 | # Add padding back. 79 | pad = 0 80 | for c in s[:-1]: 81 | if c == b58_digits[0]: 82 | pad += 1 83 | else: 84 | break 85 | return b'\x00' * pad + res 86 | 87 | def decode_check(s: str) -> bytes: 88 | """ 89 | Decode a Base58Check encoded string, returning bytes 90 | 91 | :param s: Base58 string to decode 92 | :return: Bytes encoded by ``s`` 93 | """ 94 | data = decode(s) 95 | payload = data[:-4] 96 | checksum = data[-4:] 97 | calc_checksum = hash256(payload) 98 | if checksum != calc_checksum: 99 | raise ValueError("Invalid checksum") 100 | return payload 101 | 102 | def encode_check(b: bytes) -> str: 103 | checksum = hash256(b)[0:4] 104 | data = b + checksum 105 | return encode(data) 106 | 107 | def get_xpub_fingerprint(s: str) -> bytes: 108 | """ 109 | Get the parent fingerprint from an extended public key 110 | 111 | :param s: The extended pubkey 112 | :return: The parent fingerprint bytes 113 | """ 114 | data = decode(s) 115 | fingerprint = data[5:9] 116 | return fingerprint 117 | 118 | def get_xpub_fingerprint_hex(xpub: str) -> str: 119 | """ 120 | Get the parent fingerprint as a hex string from an extended public key 121 | 122 | :param s: The extended pubkey 123 | :return: The parent fingerprint as a hex string 124 | """ 125 | data = decode(xpub) 126 | fingerprint = data[5:9] 127 | return hexlify(fingerprint).decode() 128 | 129 | def to_address(b: bytes, version: bytes) -> str: 130 | """ 131 | Base58 Check Encode the data with the version number. 132 | Used to encode legacy style addresses. 133 | 134 | :param b: The data to encode 135 | :param version: The version number to encode with 136 | :return: The Base58 Check Encoded string 137 | """ 138 | data = version + b 139 | checksum = hash256(data)[0:4] 140 | data += checksum 141 | return encode(data) 142 | 143 | def xpub_to_pub_hex(xpub: str) -> str: 144 | """ 145 | Get the public key as a string from the extended public key. 146 | 147 | :param xpub: The extended pubkey 148 | :return: The pubkey hex string 149 | """ 150 | data = decode(xpub) 151 | pubkey = data[-37:-4] 152 | return hexlify(pubkey).decode() 153 | 154 | 155 | def xpub_to_xonly_pub_hex(xpub: str) -> str: 156 | """ 157 | Get the public key as a string from the extended public key. 158 | 159 | :param xpub: The extended pubkey 160 | :return: The pubkey hex string 161 | """ 162 | data = decode(xpub) 163 | pubkey = data[-36:-4] 164 | return hexlify(pubkey).decode() 165 | 166 | 167 | def xpub_main_2_test(xpub: str) -> str: 168 | """ 169 | Convert an extended pubkey from mainnet version to testnet version. 170 | 171 | :param xpub: The extended pubkey 172 | :return: The extended pubkey re-encoded using testnet version bytes 173 | """ 174 | data = decode(xpub) 175 | test_data = b'\x04\x35\x87\xCF' + data[4:-4] 176 | checksum = hash256(test_data)[0:4] 177 | return encode(test_data + checksum) 178 | -------------------------------------------------------------------------------- /matt/btctools/_script.py: -------------------------------------------------------------------------------- 1 | # from https://github.com/bitcoin-core/HWI 2 | 3 | """ 4 | Bitcoin Script utilities 5 | ************************ 6 | """ 7 | 8 | from typing import ( 9 | Optional, 10 | Sequence, 11 | Tuple, 12 | ) 13 | 14 | 15 | def is_opreturn(script: bytes) -> bool: 16 | """ 17 | Determine whether a script is an OP_RETURN output script. 18 | 19 | :param script: The script 20 | :returns: Whether the script is an OP_RETURN output script 21 | """ 22 | return script[0] == 0x6a 23 | 24 | 25 | def is_p2sh(script: bytes) -> bool: 26 | """ 27 | Determine whether a script is a P2SH output script. 28 | 29 | :param script: The script 30 | :returns: Whether the script is a P2SH output script 31 | """ 32 | return len(script) == 23 and script[0] == 0xa9 and script[1] == 0x14 and script[22] == 0x87 33 | 34 | 35 | def is_p2pkh(script: bytes) -> bool: 36 | """ 37 | Determine whether a script is a P2PKH output script. 38 | 39 | :param script: The script 40 | :returns: Whether the script is a P2PKH output script 41 | """ 42 | return len(script) == 25 and script[0] == 0x76 and script[1] == 0xa9 and script[2] == 0x14 and script[23] == 0x88 and script[24] == 0xac 43 | 44 | 45 | def is_p2pk(script: bytes) -> bool: 46 | """ 47 | Determine whether a script is a P2PK output script. 48 | 49 | :param script: The script 50 | :returns: Whether the script is a P2PK output script 51 | """ 52 | return (len(script) == 35 or len(script) == 67) and (script[0] == 0x21 or script[0] == 0x41) and script[-1] == 0xac 53 | 54 | 55 | def is_witness(script: bytes) -> Tuple[bool, int, bytes]: 56 | """ 57 | Determine whether a script is a segwit output script. 58 | If so, also returns the witness version and witness program. 59 | 60 | :param script: The script 61 | :returns: A tuple of a bool indicating whether the script is a segwit output script, 62 | an int representing the witness version, 63 | and the bytes of the witness program. 64 | """ 65 | if len(script) < 4 or len(script) > 42: 66 | return (False, 0, b"") 67 | 68 | if script[0] != 0 and (script[0] < 81 or script[0] > 96): 69 | return (False, 0, b"") 70 | 71 | if script[1] + 2 == len(script): 72 | return (True, script[0] - 0x50 if script[0] else 0, script[2:]) 73 | 74 | return (False, 0, b"") 75 | 76 | 77 | def is_p2wpkh(script: bytes) -> bool: 78 | """ 79 | Determine whether a script is a P2WPKH output script. 80 | 81 | :param script: The script 82 | :returns: Whether the script is a P2WPKH output script 83 | """ 84 | is_wit, wit_ver, wit_prog = is_witness(script) 85 | if not is_wit: 86 | return False 87 | elif wit_ver != 0: 88 | return False 89 | return len(wit_prog) == 20 90 | 91 | 92 | def is_p2wsh(script: bytes) -> bool: 93 | """ 94 | Determine whether a script is a P2WSH output script. 95 | 96 | :param script: The script 97 | :returns: Whether the script is a P2WSH output script 98 | """ 99 | is_wit, wit_ver, wit_prog = is_witness(script) 100 | if not is_wit: 101 | return False 102 | elif wit_ver != 0: 103 | return False 104 | return len(wit_prog) == 32 105 | 106 | def is_p2tr(script: bytes) -> bool: 107 | """ 108 | Determine whether a script is a P2TR output script. 109 | 110 | :param script: The script 111 | :returns: Whether the script is a P2TR output script 112 | """ 113 | is_wit, wit_ver, wit_prog = is_witness(script) 114 | if not is_wit: 115 | return False 116 | elif wit_ver != 1: 117 | return False 118 | return len(wit_prog) == 32 119 | 120 | 121 | # Only handles up to 15 of 15. Returns None if this script is not a 122 | # multisig script. Returns (m, pubkeys) otherwise. 123 | def parse_multisig(script: bytes) -> Optional[Tuple[int, Sequence[bytes]]]: 124 | """ 125 | Determine whether a script is a multisig script. If so, determine the parameters of that multisig. 126 | 127 | :param script: The script 128 | :returns: ``None`` if the script is not multisig. 129 | If multisig, returns a tuple of the number of signers required, 130 | and a sequence of public key bytes. 131 | """ 132 | # Get m 133 | m = script[0] - 80 134 | if m < 1 or m > 15: 135 | return None 136 | 137 | # Get pubkeys 138 | pubkeys = [] 139 | offset = 1 140 | while True: 141 | pubkey_len = script[offset] 142 | if pubkey_len != 33: 143 | break 144 | offset += 1 145 | pubkeys.append(script[offset:offset + 33]) 146 | offset += 33 147 | 148 | # Check things at the end 149 | n = script[offset] - 80 150 | if n != len(pubkeys): 151 | return None 152 | offset += 1 153 | op_cms = script[offset] 154 | if op_cms != 174: 155 | return None 156 | 157 | return (m, pubkeys) 158 | -------------------------------------------------------------------------------- /matt/btctools/_serialize.py: -------------------------------------------------------------------------------- 1 | # from https://github.com/bitcoin-core/HWI 2 | 3 | #!/usr/bin/env python3 4 | # Copyright (c) 2010 ArtForz -- public domain half-a-node 5 | # Copyright (c) 2012 Jeff Garzik 6 | # Copyright (c) 2010-2016 The Bitcoin Core developers 7 | # Distributed under the MIT software license, see the accompanying 8 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 9 | """ 10 | Bitcoin Object Python Serializations 11 | ************************************ 12 | 13 | Modified from the test/test_framework/mininode.py file from the 14 | Bitcoin repository 15 | """ 16 | 17 | import struct 18 | 19 | from typing import ( 20 | List, 21 | Sequence, 22 | TypeVar, 23 | Callable, 24 | ) 25 | from typing_extensions import Protocol 26 | 27 | class Readable(Protocol): 28 | def read(self, n: int = -1) -> bytes: 29 | ... 30 | 31 | class Deserializable(Protocol): 32 | def deserialize(self, f: Readable) -> None: 33 | ... 34 | 35 | class Serializable(Protocol): 36 | def serialize(self) -> bytes: 37 | ... 38 | 39 | 40 | # Serialization/deserialization tools 41 | def ser_compact_size(size: int) -> bytes: 42 | """ 43 | Serialize an integer using Bitcoin's compact size unsigned integer serialization. 44 | 45 | :param size: The int to serialize 46 | :returns: The int serialized as a compact size unsigned integer 47 | """ 48 | r = b"" 49 | if size < 253: 50 | r = struct.pack("B", size) 51 | elif size < 0x10000: 52 | r = struct.pack(" int: 60 | """ 61 | Deserialize a compact size unsigned integer from the beginning of the byte stream. 62 | 63 | :param f: The byte stream 64 | :returns: The integer that was serialized 65 | """ 66 | nit: int = struct.unpack(" bytes: 76 | """ 77 | Deserialize a variable length byte string serialized with Bitcoin's variable length string serialization from a byte stream. 78 | 79 | :param f: The byte stream 80 | :returns: The byte string that was serialized 81 | """ 82 | nit = deser_compact_size(f) 83 | return f.read(nit) 84 | 85 | def ser_string(s: bytes) -> bytes: 86 | """ 87 | Serialize a byte string with Bitcoin's variable length string serialization. 88 | 89 | :param s: The byte string to be serialized 90 | :returns: The serialized byte string 91 | """ 92 | return ser_compact_size(len(s)) + s 93 | 94 | def deser_uint256(f: Readable) -> int: 95 | """ 96 | Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte stream. 97 | 98 | :param f: The byte stream. 99 | :returns: The integer that was serialized 100 | """ 101 | r = 0 102 | for i in range(8): 103 | t = struct.unpack(" bytes: 109 | """ 110 | Serialize a 256 bit integer with Bitcoin's 256 bit integer serialization. 111 | 112 | :param u: The integer to serialize 113 | :returns: The serialized 256 bit integer 114 | """ 115 | rs = b"" 116 | for _ in range(8): 117 | rs += struct.pack(">= 32 119 | return rs 120 | 121 | 122 | def uint256_from_str(s: bytes) -> int: 123 | """ 124 | Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte string. 125 | 126 | :param s: The byte string 127 | :returns: The integer that was serialized 128 | """ 129 | r = 0 130 | t = struct.unpack(" List[D]: 138 | """ 139 | Deserialize a vector of objects with Bitcoin's object vector serialization from a byte stream. 140 | 141 | :param f: The byte stream 142 | :param c: The class of object to deserialize for each object in the vector 143 | :returns: A list of objects that were serialized 144 | """ 145 | nit = deser_compact_size(f) 146 | r = [] 147 | for _ in range(nit): 148 | t = c() 149 | t.deserialize(f) 150 | r.append(t) 151 | return r 152 | 153 | 154 | def ser_vector(v: Sequence[Serializable]) -> bytes: 155 | """ 156 | Serialize a vector of objects with Bitcoin's object vector serialzation. 157 | 158 | :param v: The list of objects to serialize 159 | :returns: The serialized objects 160 | """ 161 | r = ser_compact_size(len(v)) 162 | for i in v: 163 | r += i.serialize() 164 | return r 165 | 166 | 167 | def deser_string_vector(f: Readable) -> List[bytes]: 168 | """ 169 | Deserialize a vector of byte strings from a byte stream. 170 | 171 | :param f: The byte stream 172 | :returns: The list of byte strings that were serialized 173 | """ 174 | nit = deser_compact_size(f) 175 | r = [] 176 | for _ in range(nit): 177 | t = deser_string(f) 178 | r.append(t) 179 | return r 180 | 181 | 182 | def ser_string_vector(v: List[bytes]) -> bytes: 183 | """ 184 | Serialize a list of byte strings as a vector of byte strings. 185 | 186 | :param v: The list of byte strings to serialize 187 | :returns: The serialized list of byte strings 188 | """ 189 | r = ser_compact_size(len(v)) 190 | for sv in v: 191 | r += ser_string(sv) 192 | return r 193 | 194 | def ser_sig_der(r: bytes, s: bytes) -> bytes: 195 | """ 196 | Serialize the ``r`` and ``s`` values of an ECDSA signature using DER. 197 | 198 | :param r: The ``r`` value bytes 199 | :param s: The ``s`` value bytes 200 | :returns: The DER encoded signature 201 | """ 202 | sig = b"\x30" 203 | 204 | # Make r and s as short as possible 205 | ri = 0 206 | for b in r: 207 | if b == 0: 208 | ri += 1 209 | else: 210 | break 211 | r = r[ri:] 212 | si = 0 213 | for b in s: 214 | if b == 0: 215 | si += 1 216 | else: 217 | break 218 | s = s[si:] 219 | 220 | # Make positive of neg 221 | first = r[0] 222 | if first & (1 << 7) != 0: 223 | r = b"\x00" + r 224 | first = s[0] 225 | if first & (1 << 7) != 0: 226 | s = b"\x00" + s 227 | 228 | # Write total length 229 | total_len = len(r) + len(s) + 4 230 | sig += struct.pack("B", total_len) 231 | 232 | # write r 233 | sig += b"\x02" 234 | sig += struct.pack("B", len(r)) 235 | sig += r 236 | 237 | # write s 238 | sig += b"\x02" 239 | sig += struct.pack("B", len(s)) 240 | sig += s 241 | 242 | sig += b"\x01" 243 | return sig 244 | 245 | def ser_sig_compact(r: bytes, s: bytes, recid: bytes) -> bytes: 246 | """ 247 | Serialize the ``r`` and ``s`` values of an ECDSA signature using the compact signature serialization scheme. 248 | 249 | :param r: The ``r`` value bytes 250 | :param s: The ``s`` value bytes 251 | :returns: The compact signature 252 | """ 253 | rec = struct.unpack("B", recid)[0] 254 | prefix = struct.pack("B", 27 + 4 + rec) 255 | 256 | sig = b"" 257 | sig += prefix 258 | sig += r + s 259 | 260 | return sig 261 | 262 | 263 | def uint256_from_compact(c): 264 | nbytes = (c >> 24) & 0xFF 265 | v = (c & 0xFFFFFF) << (8 * (nbytes - 3)) 266 | return v 267 | -------------------------------------------------------------------------------- /matt/btctools/auth_proxy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Jeff Garzik 2 | # 3 | # Previous copyright, from python-jsonrpc/jsonrpc/proxy.py: 4 | # 5 | # Copyright (c) 2007 Jan-Klaas Kollhof 6 | # 7 | # This file is part of jsonrpc. 8 | # 9 | # jsonrpc is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU Lesser General Public License as published by 11 | # the Free Software Foundation; either version 2.1 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This software is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Lesser General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Lesser General Public License 20 | # along with this software; if not, write to the Free Software 21 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | 23 | """HTTP proxy for opening RPC connection to bitcoind. 24 | 25 | AuthServiceProxy has the following improvements over python-jsonrpc's 26 | ServiceProxy class: 27 | 28 | - HTTP connections persist for the life of the AuthServiceProxy object 29 | (if server supports HTTP/1.1) 30 | - sends protocol 'version', per JSON-RPC 1.1 31 | - sends proper, incrementing 'id' 32 | - sends Basic HTTP authentication headers 33 | - parses all JSON numbers that look like floats as Decimal 34 | - uses standard Python json lib 35 | """ 36 | 37 | # bitcoin_rpc auth proxy does not handle broken pipes. Using Bitcoin Core's one which is more complete. 38 | # Taken from https://github.com/bitcoin/bitcoin/blob/master/test/functional/test_framework/authproxy.py 39 | 40 | import base64 41 | import decimal 42 | from http import HTTPStatus 43 | import http.client 44 | import json 45 | import logging 46 | import os 47 | import socket 48 | import time 49 | import urllib.parse 50 | 51 | HTTP_TIMEOUT = 30 52 | USER_AGENT = "AuthServiceProxy/0.1" 53 | 54 | log = logging.getLogger("BitcoinRPC") 55 | 56 | 57 | class JSONRPCException(Exception): 58 | def __init__(self, rpc_error, http_status=None): 59 | try: 60 | errmsg = "%(message)s (%(code)i)" % rpc_error 61 | except (KeyError, TypeError): 62 | errmsg = "" 63 | super().__init__(errmsg) 64 | self.error = rpc_error 65 | self.http_status = http_status 66 | 67 | 68 | def EncodeDecimal(o): 69 | if isinstance(o, decimal.Decimal): 70 | return str(o) 71 | raise TypeError(repr(o) + " is not JSON serializable") 72 | 73 | 74 | class AuthServiceProxy: 75 | __id_count = 0 76 | 77 | # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps 78 | def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True): 79 | self.__service_url = service_url 80 | self._service_name = service_name 81 | self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests 82 | self.__url = urllib.parse.urlparse(service_url) 83 | user = None if self.__url.username is None else self.__url.username.encode("utf8") 84 | passwd = None if self.__url.password is None else self.__url.password.encode("utf8") 85 | authpair = user + b":" + passwd 86 | self.__auth_header = b"Basic " + base64.b64encode(authpair) 87 | self.timeout = timeout 88 | self._set_conn(connection) 89 | 90 | def __getattr__(self, name): 91 | if name.startswith("__") and name.endswith("__"): 92 | # Python internal stuff 93 | raise AttributeError 94 | if self._service_name is not None: 95 | name = "%s.%s" % (self._service_name, name) 96 | return AuthServiceProxy(self.__service_url, name, connection=self.__conn) 97 | 98 | def _request(self, method, path, postdata): 99 | """ 100 | Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout). 101 | This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5. 102 | """ 103 | headers = { 104 | "Host": self.__url.hostname, 105 | "User-Agent": USER_AGENT, 106 | "Authorization": self.__auth_header, 107 | "Content-type": "application/json", 108 | } 109 | if os.name == "nt": 110 | # Windows somehow does not like to re-use connections 111 | # TODO: Find out why the connection would disconnect occasionally and make it reusable on Windows 112 | self._set_conn() 113 | try: 114 | self.__conn.request(method, path, postdata, headers) 115 | return self._get_response() 116 | except http.client.BadStatusLine as e: 117 | if e.line == "''": # if connection was closed, try again 118 | self.__conn.close() 119 | self.__conn.request(method, path, postdata, headers) 120 | return self._get_response() 121 | else: 122 | raise 123 | except (BrokenPipeError, ConnectionResetError): 124 | # Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset 125 | # ConnectionResetError happens on FreeBSD with Python 3.4 126 | self.__conn.close() 127 | self.__conn.request(method, path, postdata, headers) 128 | return self._get_response() 129 | 130 | def get_request(self, *args, **argsn): 131 | AuthServiceProxy.__id_count += 1 132 | 133 | log.debug( 134 | "-{}-> {} {}".format( 135 | AuthServiceProxy.__id_count, 136 | self._service_name, 137 | json.dumps(args or argsn, default=EncodeDecimal, ensure_ascii=self.ensure_ascii), 138 | ) 139 | ) 140 | if args and argsn: 141 | raise ValueError("Cannot handle both named and positional arguments") 142 | return { 143 | "version": "1.1", 144 | "method": self._service_name, 145 | "params": args or argsn, 146 | "id": AuthServiceProxy.__id_count, 147 | } 148 | 149 | def __call__(self, *args, **argsn): 150 | postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) 151 | response, status = self._request("POST", self.__url.path, postdata.encode("utf-8")) 152 | if response["error"] is not None: 153 | raise JSONRPCException(response["error"], status) 154 | elif "result" not in response: 155 | raise JSONRPCException({"code": -343, "message": "missing JSON-RPC result"}, status) 156 | elif status != HTTPStatus.OK: 157 | raise JSONRPCException({"code": -342, "message": "non-200 HTTP status code but no JSON-RPC error"}, status) 158 | else: 159 | return response["result"] 160 | 161 | def batch(self, rpc_call_list): 162 | postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) 163 | log.debug("--> " + postdata) 164 | response, status = self._request("POST", self.__url.path, postdata.encode("utf-8")) 165 | if status != HTTPStatus.OK: 166 | raise JSONRPCException({"code": -342, "message": "non-200 HTTP status code but no JSON-RPC error"}, status) 167 | return response 168 | 169 | def _get_response(self): 170 | req_start_time = time.time() 171 | try: 172 | http_response = self.__conn.getresponse() 173 | except socket.timeout: 174 | raise JSONRPCException( 175 | { 176 | "code": -344, 177 | "message": "%r RPC took longer than %f seconds. Consider " 178 | "using larger timeout for calls that take " 179 | "longer to return." % (self._service_name, self.__conn.timeout), 180 | } 181 | ) 182 | if http_response is None: 183 | raise JSONRPCException({"code": -342, "message": "missing HTTP response from server"}) 184 | 185 | content_type = http_response.getheader("Content-Type") 186 | if content_type != "application/json": 187 | raise JSONRPCException( 188 | { 189 | "code": -342, 190 | "message": "non-JSON HTTP response with '%i %s' from server" 191 | % (http_response.status, http_response.reason), 192 | }, 193 | http_response.status, 194 | ) 195 | 196 | responsedata = http_response.read().decode("utf8") 197 | response = json.loads(responsedata, parse_float=decimal.Decimal) 198 | elapsed = time.time() - req_start_time 199 | if "error" in response and response["error"] is None: 200 | log.debug( 201 | "<-%s- [%.6f] %s" 202 | % ( 203 | response["id"], 204 | elapsed, 205 | json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii), 206 | ) 207 | ) 208 | else: 209 | log.debug("<-- [%.6f] %s" % (elapsed, responsedata)) 210 | return response, http_response.status 211 | 212 | def __truediv__(self, relative_uri): 213 | return AuthServiceProxy( 214 | "{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn 215 | ) 216 | 217 | def _set_conn(self, connection=None): 218 | port = 80 if self.__url.port is None else self.__url.port 219 | if connection: 220 | self.__conn = connection 221 | self.timeout = connection.timeout 222 | elif self.__url.scheme == "https": 223 | self.__conn = http.client.HTTPSConnection(self.__url.hostname, port, timeout=self.timeout) 224 | else: 225 | self.__conn = http.client.HTTPConnection(self.__url.hostname, port, timeout=self.timeout) 226 | -------------------------------------------------------------------------------- /matt/btctools/block.py: -------------------------------------------------------------------------------- 1 | # from bitcoin-core's test framework 2 | 3 | import struct 4 | from .common import hash256 5 | from .tx import CTransaction 6 | from ._serialize import deser_uint256, deser_vector, ser_uint256, ser_vector, uint256_from_compact, uint256_from_str 7 | 8 | 9 | class CBlockHeader: 10 | __slots__ = ("hash", "hashMerkleRoot", "hashPrevBlock", "nBits", "nNonce", 11 | "nTime", "nVersion", "sha256") 12 | 13 | def __init__(self, header=None): 14 | if header is None: 15 | self.set_null() 16 | else: 17 | self.nVersion = header.nVersion 18 | self.hashPrevBlock = header.hashPrevBlock 19 | self.hashMerkleRoot = header.hashMerkleRoot 20 | self.nTime = header.nTime 21 | self.nBits = header.nBits 22 | self.nNonce = header.nNonce 23 | self.sha256 = header.sha256 24 | self.hash = header.hash 25 | self.calc_sha256() 26 | 27 | def set_null(self): 28 | self.nVersion = 4 29 | self.hashPrevBlock = 0 30 | self.hashMerkleRoot = 0 31 | self.nTime = 0 32 | self.nBits = 0 33 | self.nNonce = 0 34 | self.sha256 = None 35 | self.hash = None 36 | 37 | def deserialize(self, f): 38 | self.nVersion = struct.unpack(" 1: 103 | newhashes = [] 104 | for i in range(0, len(hashes), 2): 105 | i2 = min(i+1, len(hashes)-1) 106 | newhashes.append(hash256(hashes[i] + hashes[i2])) 107 | hashes = newhashes 108 | return uint256_from_str(hashes[0]) 109 | 110 | def calc_merkle_root(self): 111 | hashes = [] 112 | for tx in self.vtx: 113 | tx.calc_sha256() 114 | hashes.append(ser_uint256(tx.sha256)) 115 | return self.get_merkle_root(hashes) 116 | 117 | def calc_witness_merkle_root(self): 118 | # For witness root purposes, the hash of the 119 | # coinbase, with witness, is defined to be 0...0 120 | hashes = [ser_uint256(0)] 121 | 122 | for tx in self.vtx[1:]: 123 | # Calculate the hashes with witness data 124 | hashes.append(ser_uint256(tx.calc_sha256(True))) 125 | 126 | return self.get_merkle_root(hashes) 127 | 128 | def is_valid(self): 129 | self.calc_sha256() 130 | target = uint256_from_compact(self.nBits) 131 | if self.sha256 > target: 132 | return False 133 | for tx in self.vtx: 134 | if not tx.is_valid(): 135 | return False 136 | if self.calc_merkle_root() != self.hashMerkleRoot: 137 | return False 138 | return True 139 | 140 | def solve(self): 141 | self.rehash() 142 | target = uint256_from_compact(self.nBits) 143 | while self.sha256 > target: 144 | self.nNonce += 1 145 | self.rehash() 146 | 147 | def __repr__(self): 148 | return "CBlock(nVersion=%i hashPrevBlock=%064x hashMerkleRoot=%064x nBits=%08x nNonce=%08x vtx=%s)" \ 149 | % (self.nVersion, self.hashPrevBlock, self.hashMerkleRoot, self.nBits, self.nNonce, repr(self.vtx)) -------------------------------------------------------------------------------- /matt/btctools/common.py: -------------------------------------------------------------------------------- 1 | # from https://github.com/bitcoin-core/HWI 2 | 3 | """ 4 | Common Classes and Utilities 5 | **************************** 6 | """ 7 | 8 | import hashlib 9 | 10 | from enum import Enum 11 | 12 | from typing import Union 13 | 14 | 15 | class Chain(Enum): 16 | """ 17 | The blockchain network to use 18 | """ 19 | MAIN = 0 #: Bitcoin Main network 20 | TEST = 1 #: Bitcoin Test network 21 | REGTEST = 2 #: Bitcoin Core Regression Test network 22 | SIGNET = 3 #: Bitcoin Signet 23 | 24 | def __str__(self) -> str: 25 | return str(self.name).lower() 26 | 27 | def __repr__(self) -> str: 28 | return str(self) 29 | 30 | @staticmethod 31 | def argparse(s: str) -> Union['Chain', str]: 32 | try: 33 | return Chain[s.upper()] 34 | except KeyError: 35 | return s 36 | 37 | 38 | class AddressType(Enum): 39 | """ 40 | The type of address to use 41 | """ 42 | LEGACY = 1 #: Legacy address type. P2PKH for single sig, P2SH for scripts. 43 | WIT = 2 #: Native segwit v0 address type. P2WPKH for single sig, P2WPSH for scripts. 44 | SH_WIT = 3 #: Nested segwit v0 address type. P2SH-P2WPKH for single sig, P2SH-P2WPSH for scripts. 45 | TAP = 4 #: Segwit v1 Taproot address type. P2TR always. 46 | 47 | def __str__(self) -> str: 48 | return str(self.name).lower() 49 | 50 | def __repr__(self) -> str: 51 | return str(self) 52 | 53 | @staticmethod 54 | def argparse(s: str) -> Union['AddressType', str]: 55 | try: 56 | return AddressType[s.upper()] 57 | except KeyError: 58 | return s 59 | 60 | 61 | def sha256(s: bytes) -> bytes: 62 | """ 63 | Perform a single SHA256 hash. 64 | 65 | :param s: Bytes to hash 66 | :return: The hash 67 | """ 68 | return hashlib.new('sha256', s).digest() 69 | 70 | 71 | def ripemd160(s: bytes) -> bytes: 72 | """ 73 | Perform a single RIPEMD160 hash. 74 | 75 | :param s: Bytes to hash 76 | :return: The hash 77 | """ 78 | return hashlib.new('ripemd160', s).digest() 79 | 80 | 81 | def hash256(s: bytes) -> bytes: 82 | """ 83 | Perform a double SHA256 hash. 84 | A SHA256 is performed on the input, and then a second 85 | SHA256 is performed on the result of the first SHA256 86 | 87 | :param s: Bytes to hash 88 | :return: The hash 89 | """ 90 | return sha256(sha256(s)) 91 | 92 | 93 | def hash160(s: bytes) -> bytes: 94 | """ 95 | perform a single SHA256 hash followed by a single RIPEMD160 hash on the result of the SHA256 hash. 96 | 97 | :param s: Bytes to hash 98 | :return: The hash 99 | """ 100 | return ripemd160(sha256(s)) 101 | -------------------------------------------------------------------------------- /matt/btctools/errors.py: -------------------------------------------------------------------------------- 1 | # from https://github.com/bitcoin-core/HWI 2 | 3 | """ 4 | Errors and Error Codes 5 | ********************** 6 | 7 | HWI has several possible Exceptions with corresponding error codes. 8 | 9 | :class:`~hwilib.hwwclient.HardwareWalletClient` functions and :mod:`~hwilib.commands` functions will generally raise an exception that is a subclass of :class:`HWWError`. 10 | The HWI command line tool will convert these exceptions into a dictionary containing the error message and error code. 11 | These look like ``{"error": "", "code": }``. 12 | """ 13 | 14 | from typing import Any, Dict, Iterator, Optional 15 | from contextlib import contextmanager 16 | 17 | # Error codes 18 | NO_DEVICE_TYPE = -1 #: Device type was not specified 19 | MISSING_ARGUMENTS = -2 #: Arguments are missing 20 | DEVICE_CONN_ERROR = -3 #: Error connecting to the device 21 | UNKNWON_DEVICE_TYPE = -4 #: Device type is unknown 22 | INVALID_TX = -5 #: Transaction is invalid 23 | NO_PASSWORD = -6 #: No password provided, but one is needed 24 | BAD_ARGUMENT = -7 #: Bad, malformed, or conflicting argument was provided 25 | NOT_IMPLEMENTED = -8 #: Function is not implemented 26 | UNAVAILABLE_ACTION = -9 #: Function is not available for this device 27 | DEVICE_ALREADY_INIT = -10 #: Device is already initialized 28 | DEVICE_ALREADY_UNLOCKED = -11 #: Device is already unlocked 29 | DEVICE_NOT_READY = -12 #: Device is not ready 30 | UNKNOWN_ERROR = -13 #: An unknown error occurred 31 | ACTION_CANCELED = -14 #: Action was canceled by the user 32 | DEVICE_BUSY = -15 #: Device is busy 33 | NEED_TO_BE_ROOT = -16 #: User needs to be root to perform action 34 | HELP_TEXT = -17 #: Help text was requested by the user 35 | DEVICE_NOT_INITIALIZED = -18 #: Device is not initialized 36 | 37 | # Exceptions 38 | class HWWError(Exception): 39 | """ 40 | Generic exception type produced by HWI 41 | Subclassed by specific Errors to have Exceptions that have specific error codes. 42 | 43 | Contains a message and error code. 44 | """ 45 | def __init__(self, msg: str, code: int) -> None: 46 | """ 47 | Create an exception with the message and error code 48 | 49 | :param msg: The error message 50 | :param code: The error code 51 | """ 52 | Exception.__init__(self) 53 | self.code = code 54 | self.msg = msg 55 | 56 | def get_code(self) -> int: 57 | """ 58 | Get the error code for this Error 59 | 60 | :return: The error code 61 | """ 62 | return self.code 63 | 64 | def get_msg(self) -> str: 65 | """ 66 | Get the error message for this Error 67 | 68 | :return: The error message 69 | """ 70 | return self.msg 71 | 72 | def __str__(self) -> str: 73 | return self.msg 74 | 75 | class NoPasswordError(HWWError): 76 | """ 77 | :class:`HWWError` for :data:`NO_PASSWORD` 78 | """ 79 | def __init__(self, msg: str): 80 | """ 81 | :param msg: The error message 82 | """ 83 | HWWError.__init__(self, msg, NO_PASSWORD) 84 | 85 | class UnavailableActionError(HWWError): 86 | """ 87 | :class:`HWWError` for :data:`UNAVAILABLE_ACTION` 88 | """ 89 | def __init__(self, msg: str): 90 | """ 91 | :param msg: The error message 92 | """ 93 | HWWError.__init__(self, msg, UNAVAILABLE_ACTION) 94 | 95 | class DeviceAlreadyInitError(HWWError): 96 | """ 97 | :class:`HWWError` for :data:`DEVICE_ALREADY_INIT` 98 | """ 99 | def __init__(self, msg: str): 100 | """ 101 | :param msg: The error message 102 | """ 103 | HWWError.__init__(self, msg, DEVICE_ALREADY_INIT) 104 | 105 | class DeviceNotReadyError(HWWError): 106 | """ 107 | :class:`HWWError` for :data:`DEVICE_NOT_READY` 108 | """ 109 | def __init__(self, msg: str): 110 | """ 111 | :param msg: The error message 112 | """ 113 | HWWError.__init__(self, msg, DEVICE_NOT_READY) 114 | 115 | class DeviceAlreadyUnlockedError(HWWError): 116 | """ 117 | :class:`HWWError` for :data:`DEVICE_ALREADY_UNLOCKED` 118 | """ 119 | def __init__(self, msg: str): 120 | """ 121 | :param msg: The error message 122 | """ 123 | HWWError.__init__(self, msg, DEVICE_ALREADY_UNLOCKED) 124 | 125 | class UnknownDeviceError(HWWError): 126 | """ 127 | :class:`HWWError` for :data:`DEVICE_TYPE` 128 | """ 129 | def __init__(self, msg: str): 130 | """ 131 | :param msg: The error message 132 | """ 133 | HWWError.__init__(self, msg, UNKNWON_DEVICE_TYPE) 134 | 135 | class NotImplementedError(HWWError): 136 | """ 137 | :class:`HWWError` for :data:`NOT_IMPLEMENTED` 138 | """ 139 | def __init__(self, msg: str): 140 | """ 141 | :param msg: The error message 142 | """ 143 | HWWError.__init__(self, msg, NOT_IMPLEMENTED) 144 | 145 | class PSBTSerializationError(HWWError): 146 | """ 147 | :class:`HWWError` for :data:`INVALID_TX` 148 | """ 149 | def __init__(self, msg: str): 150 | """ 151 | :param msg: The error message 152 | """ 153 | HWWError.__init__(self, msg, INVALID_TX) 154 | 155 | class BadArgumentError(HWWError): 156 | """ 157 | :class:`HWWError` for :data:`BAD_ARGUMENT` 158 | """ 159 | def __init__(self, msg: str): 160 | """ 161 | :param msg: The error message 162 | """ 163 | HWWError.__init__(self, msg, BAD_ARGUMENT) 164 | 165 | class DeviceFailureError(HWWError): 166 | """ 167 | :class:`HWWError` for :data:`UNKNOWN_ERROR` 168 | """ 169 | def __init__(self, msg: str): 170 | """ 171 | :param msg: The error message 172 | """ 173 | HWWError.__init__(self, msg, UNKNOWN_ERROR) 174 | 175 | class ActionCanceledError(HWWError): 176 | """ 177 | :class:`HWWError` for :data:`ACTION_CANCELED` 178 | """ 179 | def __init__(self, msg: str): 180 | """ 181 | :param msg: The error message 182 | """ 183 | HWWError.__init__(self, msg, ACTION_CANCELED) 184 | 185 | class DeviceConnectionError(HWWError): 186 | """ 187 | :class:`HWWError` for :data:`DEVICE_CONN_ERROR` 188 | """ 189 | def __init__(self, msg: str): 190 | """ 191 | :param msg: The error message 192 | """ 193 | HWWError.__init__(self, msg, DEVICE_CONN_ERROR) 194 | 195 | class DeviceBusyError(HWWError): 196 | """ 197 | :class:`HWWError` for :data:`DEVICE_BUSY` 198 | """ 199 | def __init__(self, msg: str): 200 | """ 201 | :param msg: The error message 202 | """ 203 | HWWError.__init__(self, msg, DEVICE_BUSY) 204 | 205 | class NeedsRootError(HWWError): 206 | def __init__(self, msg: str): 207 | HWWError.__init__(self, msg, NEED_TO_BE_ROOT) 208 | 209 | @contextmanager 210 | def handle_errors( 211 | msg: Optional[str] = None, 212 | result: Optional[Dict[str, Any]] = None, 213 | code: int = UNKNOWN_ERROR, 214 | debug: bool = False, 215 | ) -> Iterator[None]: 216 | """ 217 | Context manager to catch all Exceptions and HWWErrors to return them as dictionaries containing the error message and code. 218 | 219 | :param msg: Error message prefix. Attached to the beginning of each error message 220 | :param result: The dictionary to put the resulting error in 221 | :param code: The default error code to use for Exceptions 222 | :param debug: Whether to also print out the traceback for debugging purposes 223 | """ 224 | if result is None: 225 | result = {} 226 | 227 | if msg is None: 228 | msg = "" 229 | else: 230 | msg = msg + " " 231 | 232 | try: 233 | yield 234 | 235 | except HWWError as e: 236 | result['error'] = msg + e.get_msg() 237 | result['code'] = e.get_code() 238 | except Exception as e: 239 | result['error'] = msg + str(e) 240 | result['code'] = code 241 | if debug: 242 | import traceback 243 | traceback.print_exc() 244 | return result 245 | 246 | 247 | common_err_msgs = { 248 | "enumerate": "Could not open client or get fingerprint information:" 249 | } 250 | -------------------------------------------------------------------------------- /matt/btctools/ripemd160.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Pieter Wuille 2 | # Distributed under the MIT software license, see the accompanying 3 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 | """Test-only pure Python RIPEMD160 implementation.""" 5 | 6 | import unittest 7 | 8 | # Message schedule indexes for the left path. 9 | ML = [ 10 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 11 | 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, 12 | 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, 13 | 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 14 | 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 15 | ] 16 | 17 | # Message schedule indexes for the right path. 18 | MR = [ 19 | 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 20 | 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, 21 | 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, 22 | 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, 23 | 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 24 | ] 25 | 26 | # Rotation counts for the left path. 27 | RL = [ 28 | 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 29 | 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, 30 | 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, 31 | 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, 32 | 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 33 | ] 34 | 35 | # Rotation counts for the right path. 36 | RR = [ 37 | 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 38 | 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, 39 | 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, 40 | 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, 41 | 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 42 | ] 43 | 44 | # K constants for the left path. 45 | KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e] 46 | 47 | # K constants for the right path. 48 | KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0] 49 | 50 | 51 | def fi(x, y, z, i): 52 | """The f1, f2, f3, f4, and f5 functions from the specification.""" 53 | if i == 0: 54 | return x ^ y ^ z 55 | elif i == 1: 56 | return (x & y) | (~x & z) 57 | elif i == 2: 58 | return (x | ~y) ^ z 59 | elif i == 3: 60 | return (x & z) | (y & ~z) 61 | elif i == 4: 62 | return x ^ (y | ~z) 63 | else: 64 | assert False 65 | 66 | 67 | def rol(x, i): 68 | """Rotate the bottom 32 bits of x left by i bits.""" 69 | return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff 70 | 71 | 72 | def compress(h0, h1, h2, h3, h4, block): 73 | """Compress state (h0, h1, h2, h3, h4) with block.""" 74 | # Left path variables. 75 | al, bl, cl, dl, el = h0, h1, h2, h3, h4 76 | # Right path variables. 77 | ar, br, cr, dr, er = h0, h1, h2, h3, h4 78 | # Message variables. 79 | x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)] 80 | 81 | # Iterate over the 80 rounds of the compression. 82 | for j in range(80): 83 | rnd = j >> 4 84 | # Perform left side of the transformation. 85 | al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el 86 | al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl 87 | # Perform right side of the transformation. 88 | ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er 89 | ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr 90 | 91 | # Compose old state, left transform, and right transform into new state. 92 | return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr 93 | 94 | 95 | def ripemd160(data): 96 | """Compute the RIPEMD-160 hash of data.""" 97 | # Initialize state. 98 | state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0) 99 | # Process full 64-byte blocks in the input. 100 | for b in range(len(data) >> 6): 101 | state = compress(*state, data[64*b:64*(b+1)]) 102 | # Construct final blocks (with padding and size). 103 | pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63) 104 | fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little') 105 | # Process final blocks. 106 | for b in range(len(fin) >> 6): 107 | state = compress(*state, fin[64*b:64*(b+1)]) 108 | # Produce output. 109 | return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state) 110 | 111 | 112 | class TestFrameworkKey(unittest.TestCase): 113 | def test_ripemd160(self): 114 | """RIPEMD-160 test vectors.""" 115 | # See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html 116 | for msg, hexout in [ 117 | (b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"), 118 | (b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"), 119 | (b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"), 120 | (b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"), 121 | (b"abcdefghijklmnopqrstuvwxyz", 122 | "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"), 123 | (b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 124 | "12a053384a9c0c88e405a06c27dcf49ada62eb2b"), 125 | (b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 126 | "b0e20b6e3116640286ed3a87a5713079b21f5189"), 127 | (b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"), 128 | (b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528") 129 | ]: 130 | self.assertEqual(ripemd160(msg).hex(), hexout) 131 | -------------------------------------------------------------------------------- /matt/btctools/segwit_addr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2017 Pieter Wuille 3 | # Distributed under the MIT software license, see the accompanying 4 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 5 | """Reference implementation for Bech32/Bech32m and segwit addresses.""" 6 | from enum import Enum 7 | 8 | CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 9 | BECH32_CONST = 1 10 | BECH32M_CONST = 0x2bc830a3 11 | 12 | class Encoding(Enum): 13 | """Enumeration type to list the various supported encodings.""" 14 | BECH32 = 1 15 | BECH32M = 2 16 | 17 | 18 | def bech32_polymod(values): 19 | """Internal function that computes the Bech32 checksum.""" 20 | generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] 21 | chk = 1 22 | for value in values: 23 | top = chk >> 25 24 | chk = (chk & 0x1ffffff) << 5 ^ value 25 | for i in range(5): 26 | chk ^= generator[i] if ((top >> i) & 1) else 0 27 | return chk 28 | 29 | 30 | def bech32_hrp_expand(hrp): 31 | """Expand the HRP into values for checksum computation.""" 32 | return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] 33 | 34 | 35 | def bech32_verify_checksum(hrp, data): 36 | """Verify a checksum given HRP and converted data characters.""" 37 | check = bech32_polymod(bech32_hrp_expand(hrp) + data) 38 | if check == BECH32_CONST: 39 | return Encoding.BECH32 40 | elif check == BECH32M_CONST: 41 | return Encoding.BECH32M 42 | else: 43 | return None 44 | 45 | def bech32_create_checksum(encoding, hrp, data): 46 | """Compute the checksum values given HRP and data.""" 47 | values = bech32_hrp_expand(hrp) + data 48 | const = BECH32M_CONST if encoding == Encoding.BECH32M else BECH32_CONST 49 | polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const 50 | return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] 51 | 52 | 53 | def bech32_encode(encoding, hrp, data): 54 | """Compute a Bech32 or Bech32m string given HRP and data values.""" 55 | combined = data + bech32_create_checksum(encoding, hrp, data) 56 | return hrp + '1' + ''.join([CHARSET[d] for d in combined]) 57 | 58 | 59 | def bech32_decode(bech): 60 | """Validate a Bech32/Bech32m string, and determine HRP and data.""" 61 | if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or 62 | (bech.lower() != bech and bech.upper() != bech)): 63 | return (None, None, None) 64 | bech = bech.lower() 65 | pos = bech.rfind('1') 66 | if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: 67 | return (None, None, None) 68 | if not all(x in CHARSET for x in bech[pos+1:]): 69 | return (None, None, None) 70 | hrp = bech[:pos] 71 | data = [CHARSET.find(x) for x in bech[pos+1:]] 72 | encoding = bech32_verify_checksum(hrp, data) 73 | if encoding is None: 74 | return (None, None, None) 75 | return (encoding, hrp, data[:-6]) 76 | 77 | 78 | def convertbits(data, frombits, tobits, pad=True): 79 | """General power-of-2 base conversion.""" 80 | acc = 0 81 | bits = 0 82 | ret = [] 83 | maxv = (1 << tobits) - 1 84 | max_acc = (1 << (frombits + tobits - 1)) - 1 85 | for value in data: 86 | if value < 0 or (value >> frombits): 87 | return None 88 | acc = ((acc << frombits) | value) & max_acc 89 | bits += frombits 90 | while bits >= tobits: 91 | bits -= tobits 92 | ret.append((acc >> bits) & maxv) 93 | if pad: 94 | if bits: 95 | ret.append((acc << (tobits - bits)) & maxv) 96 | elif bits >= frombits or ((acc << (tobits - bits)) & maxv): 97 | return None 98 | return ret 99 | 100 | 101 | def decode_segwit_address(hrp, addr): 102 | """Decode a segwit address.""" 103 | encoding, hrpgot, data = bech32_decode(addr) 104 | if hrpgot != hrp: 105 | return (None, None) 106 | decoded = convertbits(data[1:], 5, 8, False) 107 | if decoded is None or len(decoded) < 2 or len(decoded) > 40: 108 | return (None, None) 109 | if data[0] > 16: 110 | return (None, None) 111 | if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: 112 | return (None, None) 113 | if (data[0] == 0 and encoding != Encoding.BECH32) or (data[0] != 0 and encoding != Encoding.BECH32M): 114 | return (None, None) 115 | return (data[0], decoded) 116 | 117 | 118 | def encode_segwit_address(hrp, witver, witprog): 119 | """Encode a segwit address.""" 120 | encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M 121 | ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5)) 122 | if decode_segwit_address(hrp, ret) == (None, None): 123 | return None 124 | return ret 125 | -------------------------------------------------------------------------------- /matt/btctools/tx.py: -------------------------------------------------------------------------------- 1 | # from https://github.com/bitcoin-core/HWI 2 | 3 | #!/usr/bin/env python3 4 | # Copyright (c) 2010 ArtForz -- public domain half-a-node 5 | # Copyright (c) 2012 Jeff Garzik 6 | # Copyright (c) 2010-2016 The Bitcoin Core developers 7 | # Distributed under the MIT software license, see the accompanying 8 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 9 | """Bitcoin Object Python Serializations 10 | 11 | Modified from the test/test_framework/mininode.py file from the 12 | Bitcoin repository 13 | 14 | CTransaction,CTxIn, CTxOut, etc....: 15 | data structures that should map to corresponding structures in 16 | bitcoin/primitives for transactions only 17 | """ 18 | 19 | import copy 20 | import struct 21 | 22 | from .common import ( 23 | hash256, 24 | ) 25 | from ._script import ( 26 | is_opreturn, 27 | is_p2sh, 28 | is_p2pkh, 29 | is_p2pk, 30 | is_witness, 31 | is_p2wsh, 32 | ) 33 | from ._serialize import ( 34 | deser_uint256, 35 | deser_string, 36 | deser_string_vector, 37 | deser_vector, 38 | Readable, 39 | ser_uint256, 40 | ser_string, 41 | ser_string_vector, 42 | ser_vector, 43 | uint256_from_str, 44 | ) 45 | 46 | from typing import ( 47 | List, 48 | Optional, 49 | Tuple, 50 | ) 51 | 52 | # Objects that map to bitcoind objects, which can be serialized/deserialized 53 | 54 | MSG_WITNESS_FLAG = 1 << 30 55 | 56 | class COutPoint(object): 57 | def __init__(self, hash: int = 0, n: int = 0xffffffff): 58 | self.hash = hash 59 | self.n = n 60 | 61 | def deserialize(self, f: Readable) -> None: 62 | self.hash = deser_uint256(f) 63 | self.n = struct.unpack(" bytes: 66 | r = b"" 67 | r += ser_uint256(self.hash) 68 | r += struct.pack(" str: 72 | return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n) 73 | 74 | 75 | class CTxIn(object): 76 | def __init__( 77 | self, 78 | outpoint: Optional[COutPoint] = None, 79 | scriptSig: bytes = b"", 80 | nSequence: int = 0, 81 | ): 82 | if outpoint is None: 83 | self.prevout = COutPoint() 84 | else: 85 | self.prevout = outpoint 86 | self.scriptSig = scriptSig 87 | self.nSequence = nSequence 88 | 89 | def deserialize(self, f: Readable) -> None: 90 | self.prevout = COutPoint() 91 | self.prevout.deserialize(f) 92 | self.scriptSig = deser_string(f) 93 | self.nSequence = struct.unpack(" bytes: 96 | r = b"" 97 | r += self.prevout.serialize() 98 | r += ser_string(self.scriptSig) 99 | r += struct.pack(" str: 103 | return "CTxIn(prevout=%s scriptSig=%s nSequence=%i)" \ 104 | % (repr(self.prevout), self.scriptSig.hex(), 105 | self.nSequence) 106 | 107 | 108 | class CTxOut(object): 109 | def __init__(self, nValue: int = 0, scriptPubKey: bytes = b""): 110 | self.nValue = nValue 111 | self.scriptPubKey = scriptPubKey 112 | 113 | def deserialize(self, f: Readable) -> None: 114 | self.nValue = struct.unpack(" bytes: 118 | r = b"" 119 | r += struct.pack(" bool: 124 | return is_opreturn(self.scriptPubKey) 125 | 126 | def is_p2sh(self) -> bool: 127 | return is_p2sh(self.scriptPubKey) 128 | 129 | def is_p2wsh(self) -> bool: 130 | return is_p2wsh(self.scriptPubKey) 131 | 132 | def is_p2pkh(self) -> bool: 133 | return is_p2pkh(self.scriptPubKey) 134 | 135 | def is_p2pk(self) -> bool: 136 | return is_p2pk(self.scriptPubKey) 137 | 138 | def is_witness(self) -> Tuple[bool, int, bytes]: 139 | return is_witness(self.scriptPubKey) 140 | 141 | def __repr__(self) -> str: 142 | return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \ 143 | % (self.nValue // 100_000_000, self.nValue % 100_000_000, self.scriptPubKey.hex()) 144 | 145 | 146 | class CScriptWitness(object): 147 | def __init__(self) -> None: 148 | # stack is a vector of strings 149 | self.stack: List[bytes] = [] 150 | 151 | def __repr__(self) -> str: 152 | return "CScriptWitness(%s)" % \ 153 | (",".join([x.hex() for x in self.stack])) 154 | 155 | def is_null(self) -> bool: 156 | if self.stack: 157 | return False 158 | return True 159 | 160 | 161 | class CTxInWitness(object): 162 | def __init__(self) -> None: 163 | self.scriptWitness = CScriptWitness() 164 | 165 | def deserialize(self, f: Readable) -> None: 166 | self.scriptWitness.stack = deser_string_vector(f) 167 | 168 | def serialize(self) -> bytes: 169 | return ser_string_vector(self.scriptWitness.stack) 170 | 171 | def __repr__(self) -> str: 172 | return repr(self.scriptWitness) 173 | 174 | def is_null(self) -> bool: 175 | return self.scriptWitness.is_null() 176 | 177 | 178 | class CTxWitness(object): 179 | def __init__(self) -> None: 180 | self.vtxinwit: List[CTxInWitness] = [] 181 | 182 | def deserialize(self, f: Readable) -> None: 183 | for i in range(len(self.vtxinwit)): 184 | self.vtxinwit[i].deserialize(f) 185 | 186 | def serialize(self) -> bytes: 187 | r = b"" 188 | # This is different than the usual vector serialization -- 189 | # we omit the length of the vector, which is required to be 190 | # the same length as the transaction's vin vector. 191 | for x in self.vtxinwit: 192 | r += x.serialize() 193 | return r 194 | 195 | def __repr__(self) -> str: 196 | return "CTxWitness(%s)" % \ 197 | (';'.join([repr(x) for x in self.vtxinwit])) 198 | 199 | def is_null(self) -> bool: 200 | for x in self.vtxinwit: 201 | if not x.is_null(): 202 | return False 203 | return True 204 | 205 | 206 | class CTransaction(object): 207 | def __init__(self, tx: Optional['CTransaction'] = None) -> None: 208 | if tx is None: 209 | self.nVersion = 1 210 | self.vin: List[CTxIn] = [] 211 | self.vout: List[CTxOut] = [] 212 | self.wit = CTxWitness() 213 | self.nLockTime = 0 214 | self.sha256: Optional[int] = None 215 | self.hash: Optional[bytes] = None 216 | else: 217 | self.nVersion = tx.nVersion 218 | self.vin = copy.deepcopy(tx.vin) 219 | self.vout = copy.deepcopy(tx.vout) 220 | self.nLockTime = tx.nLockTime 221 | self.sha256 = tx.sha256 222 | self.hash = tx.hash 223 | self.wit = copy.deepcopy(tx.wit) 224 | 225 | def deserialize(self, f: Readable) -> None: 226 | self.nVersion = struct.unpack(" bytes: 246 | r = b"" 247 | r += struct.pack(" bytes: 255 | flags = 0 256 | if not self.wit.is_null(): 257 | flags |= 1 258 | r = b"" 259 | r += struct.pack(" bytes: 278 | return self.serialize_without_witness() 279 | 280 | # Recalculate the txid (transaction hash without witness) 281 | def rehash(self) -> None: 282 | self.sha256 = None 283 | self.calc_sha256() 284 | 285 | # We will only cache the serialization without witness in 286 | # self.sha256 and self.hash -- those are expected to be the txid. 287 | def calc_sha256(self, with_witness: bool = False) -> Optional[int]: 288 | if with_witness: 289 | # Don't cache the result, just return it 290 | return uint256_from_str(hash256(self.serialize_with_witness())) 291 | 292 | if self.sha256 is None: 293 | self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) 294 | self.hash = hash256(self.serialize()) 295 | return None 296 | 297 | def is_null(self) -> bool: 298 | return len(self.vin) == 0 and len(self.vout) == 0 299 | 300 | def __repr__(self) -> str: 301 | return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ 302 | % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) 303 | -------------------------------------------------------------------------------- /matt/environment.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Optional 3 | 4 | from .btctools.auth_proxy import AuthServiceProxy 5 | from .manager import ContractManager 6 | 7 | 8 | class Environment: 9 | def __init__(self, rpc: AuthServiceProxy, manager: ContractManager, host: str, port: int, interactive: bool): 10 | self.rpc = rpc 11 | self.manager = manager 12 | self.host = host 13 | self.port = port 14 | self.interactive = interactive 15 | 16 | def prompt(self, message: Optional[str] = None): 17 | if message is not None: 18 | print(message) 19 | if self.interactive: 20 | print("Press Enter to continue...") 21 | input() 22 | -------------------------------------------------------------------------------- /matt/hub/README.md: -------------------------------------------------------------------------------- 1 | ## MATT hub 2 | 3 | This folder will contain general purpose, reusable smart contracts that can be used as building blocks for more complex constructions. -------------------------------------------------------------------------------- /matt/hub/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkleize/pymatt/89de499399bb8592650b5d78a7d8baef1b3ae1a9/matt/hub/__init__.py -------------------------------------------------------------------------------- /matt/script_helpers.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from typing import Optional, Union 4 | from matt import CCV_FLAG_CHECK_INPUT 5 | from matt.btctools.script import OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_CAT, OP_CHECKCONTRACTVERIFY, OP_CHECKSEQUENCEVERIFY, OP_DROP, OP_DUP, OP_FROMALTSTACK, OP_PICK, OP_SHA256, OP_TOALTSTACK, CScript 6 | from matt.contracts import StandardAugmentedP2TR, StandardP2TR 7 | 8 | 9 | # Duplicates the last n elements of the stack 10 | def dup(n: int = 1) -> CScript: 11 | assert n >= 1 12 | if n == 1: 13 | return CScript([OP_DUP]) 14 | elif n == 2: 15 | return CScript([OP_2DUP]) 16 | elif n == 3: 17 | return CScript([OP_3DUP]) 18 | elif n == 4: 19 | return CScript([OP_2OVER, OP_2OVER]) 20 | else: 21 | # generic, unoptimized solution 22 | # TODO: can we find an optimal script for every n? 23 | return CScript([n - 1, OP_PICK] * n) 24 | 25 | 26 | # Drops n elements from the stack 27 | def drop(n: int = 1) -> CScript: 28 | assert n >= 0 29 | 30 | return CScript([OP_2DROP]*(n // 2) + [OP_DROP] * (n % 2)) 31 | 32 | 33 | # x_0, x_1, ..., x_{n-1} -- sha256(x_0 || x_1), sha256(x_2 || x_3), ... 34 | # if n is odd, the last element is copied unchanged 35 | def reduce_merkle_layer(n: int) -> CScript: 36 | assert n >= 1 37 | 38 | if n == 1: 39 | return CScript([]) 40 | elif n == 2: 41 | return CScript([OP_CAT, OP_SHA256]) 42 | if n % 2 == 1: 43 | return CScript([OP_TOALTSTACK, *reduce_merkle_layer(n-1), OP_FROMALTSTACK]) 44 | else: 45 | # compute the last pair, reduce to the case with one less pair 46 | return CScript([OP_CAT, OP_SHA256, OP_TOALTSTACK, *reduce_merkle_layer(n-2), OP_FROMALTSTACK]) 47 | 48 | 49 | # x_0, x_1, ..., x_{n - 1} -- x_0, x_1, ..., x_{n - 1} root 50 | # where root is the root of the merkle tree computed on x_0, ... x_{n - 1} 51 | # NOTE: leaves are not hashed here. 52 | def merkle_root(n_leaves: int) -> CScript: 53 | assert n_leaves >= 1 54 | 55 | ret = [] 56 | # compute layer by layer, from the bottom up to the root 57 | while n_leaves > 1: 58 | ret.extend(reduce_merkle_layer(n_leaves)) 59 | n_leaves = (n_leaves + 1) // 2 60 | return CScript(ret) 61 | 62 | 63 | # data -- 64 | # TODO: should we pass the contract instance (typically 'self') instead of the pubkey? 65 | def check_input_contract(index: int = -1, pubkey: Optional[bytes] = None) -> CScript: 66 | assert index >= -1 67 | assert pubkey is None or len(pubkey) == 32 68 | return CScript([ 69 | index, 70 | 0 if pubkey is None else pubkey, 71 | -1, 72 | CCV_FLAG_CHECK_INPUT, 73 | OP_CHECKCONTRACTVERIFY 74 | ]) 75 | 76 | 77 | # data -- 78 | def check_output_contract(out_contract: Union[StandardP2TR, StandardAugmentedP2TR], index: int = -1, pubkey: Optional[bytes] = None) -> CScript: 79 | assert index >= -1 80 | assert pubkey is None or len(pubkey) == 32 81 | return CScript([ 82 | index, 83 | 0 if pubkey is None else pubkey, 84 | out_contract.get_taptree_merkle_root(), 85 | 0, 86 | OP_CHECKCONTRACTVERIFY 87 | ]) 88 | 89 | 90 | # like the older() fragment in miniscript 91 | def older(n: int) -> CScript: 92 | assert 1 <= n < 2**31 93 | 94 | return CScript([n, OP_CHECKSEQUENCEVERIFY, OP_DROP]) 95 | -------------------------------------------------------------------------------- /matt/utils.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from typing import List, Optional, Tuple, Union 3 | import time 4 | 5 | from .btctools.auth_proxy import AuthServiceProxy, JSONRPCException 6 | from .btctools.messages import COutPoint, CTransaction, CTxIn, CTxOut 7 | from .btctools.script import CScript, CScriptNum, bn2vch 8 | from .btctools.segwit_addr import decode_segwit_address 9 | 10 | 11 | def vch2bn(s: bytes) -> int: 12 | """Convert bitcoin-specific little endian format to number.""" 13 | if len(s) == 0: 14 | return 0 15 | # The most significant bit is the sign bit. (remembering it's little-endian) 16 | is_negative = s[-1] & 0x80 != 0 17 | # Mask off the sign bit. 18 | s_abs = s[:-1] + bytes([s[-1] & 0x7f]) 19 | v_abs = int.from_bytes(s_abs, 'little') 20 | # Return as negative number if it's negative. 21 | return -v_abs if is_negative else v_abs 22 | 23 | 24 | def encode_wit_element(x: Union[bytes, int]) -> bytes: 25 | if isinstance(x, int): 26 | return bn2vch(x) 27 | elif isinstance(x, bytes): 28 | return x 29 | else: 30 | raise ValueError("Unexpected type") 31 | 32 | 33 | # We ignore the possibility of reorgs for simplicity. 34 | 35 | def wait_for_output( 36 | rpc_connection: AuthServiceProxy, 37 | script_pub_key: bytes, 38 | poll_interval: float = 1, 39 | starting_height: Optional[int] = None, 40 | txid: Optional[str] = None, 41 | min_amount: Optional[int] = None 42 | ) -> Tuple[COutPoint, int]: 43 | # Initialize the last block height using the provided starting_height or the current block height 44 | last_block_height = max(starting_height - 1, 0) if starting_height is not None else rpc_connection.getblockcount() 45 | 46 | while True: 47 | try: 48 | # Get the latest block height 49 | current_block_height = rpc_connection.getblockcount() 50 | 51 | if last_block_height > current_block_height: 52 | time.sleep(poll_interval) 53 | continue 54 | 55 | block_hash = rpc_connection.getblockhash(last_block_height) 56 | block = rpc_connection.getblock(block_hash, 2) 57 | 58 | # Check all transactions in the block 59 | for tx in block["tx"]: 60 | # If txid is provided, ensure the current transaction matches it 61 | if txid and tx["txid"] != txid: 62 | continue 63 | 64 | # Check all outputs in the transaction 65 | for vout_index, vout in enumerate(tx["vout"]): 66 | # Ensure the amount is above the min_amount if it is provided 67 | if min_amount and vout["value"] < min_amount: 68 | continue 69 | 70 | if vout["scriptPubKey"]["hex"] == script_pub_key.hex(): 71 | return COutPoint(int(tx["txid"], 16), vout_index), last_block_height 72 | 73 | # Update the last block height 74 | last_block_height += 1 75 | 76 | except JSONRPCException as json_exception: 77 | print(f"A JSON RPC Exception occurred: {json_exception}") 78 | 79 | time.sleep(poll_interval) 80 | 81 | 82 | def wait_for_spending_tx(rpc_connection: AuthServiceProxy, outpoint: COutPoint, poll_interval: float = 1, starting_height: Optional[int] = None) -> Tuple[CTransaction, int, int]: 83 | # Initialize the last block height using the provided starting_height or the current block height 84 | last_block_height = max(starting_height - 1, 0) if starting_height is not None else rpc_connection.getblockcount() 85 | 86 | while True: 87 | try: 88 | # Get the latest block height 89 | current_block_height = rpc_connection.getblockcount() 90 | 91 | if last_block_height > current_block_height: 92 | time.sleep(poll_interval) 93 | continue 94 | 95 | block_hash = rpc_connection.getblockhash(last_block_height) 96 | block = rpc_connection.getblock(block_hash, 2) 97 | 98 | for tx in block["tx"]: 99 | # Check all inputs in the transaction 100 | for vin_index, vin in enumerate(tx["vin"]): 101 | if "txid" not in vin: 102 | continue 103 | 104 | txid = int(vin["txid"], 16) 105 | 106 | if txid == outpoint.hash and vin["vout"] == outpoint.n: 107 | result_tx = CTransaction() 108 | result_tx.deserialize(BytesIO(bytes.fromhex(tx['hex']))) 109 | return result_tx, vin_index, last_block_height 110 | 111 | last_block_height += 1 112 | 113 | except JSONRPCException as json_exception: 114 | print(f"A JSON RPC Exception occurred: {json_exception}") 115 | 116 | time.sleep(poll_interval) 117 | 118 | 119 | # stolen from jamesob: https://github.com/bitcoin/bitcoin/pull/28550 120 | def _pprint_tx(tx: CTransaction) -> str: 121 | s = f"CTransaction: (nVersion={tx.nVersion}, {int(len(tx.serialize().hex()) / 2)} bytes)\n" 122 | s += " vin:\n" 123 | for i, inp in enumerate(tx.vin): 124 | s += f" - [{i}] {inp}\n" 125 | s += " vout:\n" 126 | for i, out in enumerate(tx.vout): 127 | s += f" - [{i}] {out}\n" 128 | 129 | s += " witnesses:\n" 130 | for i, wit in enumerate(tx.wit.vtxinwit): 131 | witbytes = sum(len(s) or 1 for s in wit.scriptWitness.stack) 132 | s += f" - [{i}] ({witbytes} bytes, {witbytes / 4} vB)\n" 133 | for j, item in enumerate(wit.scriptWitness.stack): 134 | if type(item) is bytes: 135 | scriptstr = repr(CScript([item])) 136 | elif type(item) in {CScript, CScriptNum}: 137 | scriptstr = repr(item) 138 | else: 139 | raise NotImplementedError 140 | 141 | s += f" - [{i}.{j}] ({len(item)} bytes) {scriptstr}\n" 142 | 143 | s += f" nLockTime: {tx.nLockTime}\n" 144 | return s 145 | 146 | 147 | # Utilities to print transactions 148 | def format_tx_markdown(tx: CTransaction, title: str) -> str: 149 | return f''' 150 |
{title}({tx.get_vsize()} vB) 151 | 152 | ```python 153 | {_pprint_tx(tx)} 154 | ``` 155 | 156 |
157 | 158 | ''' 159 | 160 | 161 | def addr_to_script(addr: str) -> bytes: 162 | # only for segwit/taproot on regtest 163 | # TODO: generalize to other address types, and other networks (currently, it assumes regtest) 164 | 165 | wit_ver, wit_prog = decode_segwit_address("bcrt", addr) 166 | 167 | if wit_ver is None or wit_prog is None: 168 | raise ValueError(f"Invalid segwit address (or wrong network): {addr}") 169 | 170 | return bytes([ 171 | wit_ver + (0x50 if wit_ver > 0 else 0), 172 | len(wit_prog), 173 | *wit_prog 174 | ]) 175 | 176 | 177 | def make_ctv_template(outputs: List[(Union[bytes, str, int])], *, nVersion: int = 2, nSequence: int = 0) -> CTransaction: 178 | tmpl = CTransaction() 179 | tmpl.nVersion = nVersion 180 | tmpl.vin = [CTxIn(nSequence=nSequence)] 181 | for dest, amount in outputs: 182 | tmpl.vout.append( 183 | CTxOut( 184 | nValue=amount, 185 | scriptPubKey=dest if isinstance(dest, bytes) else addr_to_script(dest) 186 | ) 187 | ) 188 | return tmpl 189 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "matt" 7 | version = "0.0.1" 8 | authors = [ 9 | {name = "Salvatore Ingala", email = "salvatoshi@protonmail.com"}, 10 | ] 11 | description = "Merkleize All The Things" 12 | readme = "README.md" 13 | requires-python = ">=3.8" 14 | keywords = ["covenant", "smart contracts", "bitcoin"] 15 | license = { file = "LICENSE" } 16 | dependencies = [ 17 | 'typing_extensions >= 4.9.0' 18 | ] 19 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = 3 | tests 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | bokeh>=3.1.0,<4 2 | networkx>=3.1,<4 3 | numpy>=1.24,<2 4 | pytest>=6.2,<7 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="matt", 5 | packages=find_packages(include=['matt', 'matt.*']), 6 | ) 7 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import sys 4 | import os 5 | from pathlib import Path 6 | 7 | from matt.btctools.auth_proxy import AuthServiceProxy 8 | from matt.manager import ContractManager 9 | from test_utils.utxograph import create_utxo_graph 10 | 11 | root_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../') 12 | sys.path.append(root_path) 13 | 14 | 15 | rpc_url = "http://%s:%s@%s:%s" % ( 16 | os.getenv("BTC_RPC_USER", "rpcuser"), 17 | os.getenv("BTC_RPC_PASSWORD", "rpcpass"), 18 | os.getenv("BTC_RPC_HOST", "localhost"), 19 | os.getenv("BTC_RPC_PORT", "18443") 20 | ) 21 | 22 | 23 | def pytest_addoption(parser): 24 | parser.addoption("--utxo_graph", action="store_true") 25 | 26 | 27 | @pytest.fixture 28 | def utxo_graph(request: pytest.FixtureRequest): 29 | return request.config.getoption("--utxo_graph", False) 30 | 31 | 32 | @pytest.fixture(scope="session") 33 | def rpc(): 34 | return AuthServiceProxy(f"{rpc_url}/wallet/testwallet") 35 | 36 | 37 | @pytest.fixture 38 | def manager(rpc, request: pytest.FixtureRequest, utxo_graph: bool): 39 | manager = ContractManager(rpc, mine_automatically=True, poll_interval=0.01) 40 | yield manager 41 | 42 | if utxo_graph: 43 | # Create the "tests/graphs" directory if it doesn't exist 44 | path = Path("tests/graphs") 45 | path.mkdir(exist_ok=True) 46 | create_utxo_graph(manager, f"tests/graphs/{request.node.name}.html") 47 | 48 | 49 | class TestReport: 50 | def __init__(self): 51 | self.sections = {} 52 | 53 | def write(self, section_name, content): 54 | if section_name not in self.sections: 55 | self.sections[section_name] = [] 56 | self.sections[section_name].append(content) 57 | 58 | def finalize_report(self, filename): 59 | with open(filename, "w") as file: 60 | for section, contents in self.sections.items(): 61 | file.write(f"## {section}\n") 62 | for content in contents: 63 | file.write(content + "\n") 64 | file.write("\n") 65 | 66 | 67 | @pytest.fixture(scope="session") 68 | def report(): 69 | report_obj = TestReport() 70 | yield report_obj 71 | report_obj.finalize_report("report.md") 72 | -------------------------------------------------------------------------------- /tests/test_fraud.py: -------------------------------------------------------------------------------- 1 | from examples.game256.game256_contracts import G256_S0, G256_S1, G256_S2, Compute2x 2 | 3 | from matt.btctools.common import sha256 4 | from matt.btctools.messages import CTxOut 5 | from matt.contracts import P2TR 6 | from matt.hub.fraud import Bisect_1, Bisect_2, Leaf 7 | from matt.manager import ContractManager, SchnorrSigner 8 | from matt.merkle import is_power_of_2 9 | from matt.btctools import key 10 | from matt.utils import encode_wit_element, format_tx_markdown 11 | 12 | 13 | AMOUNT = 20_000 14 | alice_key = key.ExtendedKey.deserialize( 15 | "tprv8ZgxMBicQKsPdpwA4vW8DcSdXzPn7GkS2RdziGXUX8k86bgDQLKhyXtB3HMbJhPFd2vKRpChWxgPe787WWVqEtjy8hGbZHqZKeRrEwMm3SN") 16 | bob_key = key.ExtendedKey.deserialize( 17 | "tprv8ZgxMBicQKsPeDvaW4xxmiMXxqakLgvukT8A5GR6mRwBwjsDJV1jcZab8mxSerNcj22YPrusm2Pz5oR8LTw9GqpWT51VexTNBzxxm49jCZZ") 18 | 19 | 20 | def test_leaf_reveal_alice(manager: ContractManager): 21 | L = Leaf(alice_key.pubkey[1:], bob_key.pubkey[1:], Compute2x) 22 | 23 | x_start = 347 24 | x_end_alice = 2 * x_start 25 | x_end_bob = 2 * x_start - 1 # some wrong value 26 | 27 | h_start = sha256(encode_wit_element(x_start)) 28 | h_end_alice = sha256(encode_wit_element(x_end_alice)) 29 | h_end_bob = sha256(encode_wit_element(x_end_bob)) 30 | 31 | L_inst = manager.fund_instance(L, AMOUNT, data=L.State( 32 | h_start=h_start, h_end_alice=h_end_alice, h_end_bob=h_end_bob)) 33 | 34 | outputs = [ 35 | CTxOut( 36 | nValue=AMOUNT, 37 | scriptPubKey=P2TR(alice_key.pubkey[1:], []).get_tr_info().scriptPubKey 38 | ) 39 | ] 40 | 41 | out_instances = L_inst("alice_reveal", SchnorrSigner(alice_key), outputs)( 42 | x=x_start, 43 | h_y_b=h_end_bob 44 | ) 45 | 46 | assert len(out_instances) == 0 47 | 48 | 49 | def test_leaf_reveal_bob(manager: ContractManager): 50 | L = Leaf(alice_key.pubkey[1:], bob_key.pubkey[1:], Compute2x) 51 | 52 | x_start = 347 53 | x_end_alice = 2 * x_start - 1 # some wrong value 54 | x_end_bob = 2 * x_start 55 | 56 | h_start = sha256(encode_wit_element(x_start)) 57 | h_end_alice = sha256(encode_wit_element(x_end_alice)) 58 | h_end_bob = sha256(encode_wit_element(x_end_bob)) 59 | 60 | L_inst = manager.fund_instance(L, AMOUNT, data=L.State( 61 | h_start=h_start, h_end_alice=h_end_alice, h_end_bob=h_end_bob)) 62 | 63 | outputs = [ 64 | CTxOut( 65 | nValue=AMOUNT, 66 | scriptPubKey=P2TR(bob_key.pubkey[1:], []).get_tr_info().scriptPubKey 67 | ) 68 | ] 69 | 70 | out_instances = L_inst("bob_reveal", SchnorrSigner(bob_key), outputs)( 71 | x=x_start, 72 | h_y_a=h_end_alice 73 | ) 74 | 75 | assert len(out_instances) == 0 76 | 77 | 78 | def test_fraud_proof_full(manager: ContractManager, report): 79 | alice_trace = [2, 4, 8, 16, 32, 64, 127, 254, 508] 80 | bob_trace = [2, 4, 8, 16, 32, 64, 128, 256, 512] 81 | 82 | assert alice_trace[0] == bob_trace[0] and len(alice_trace) == len(bob_trace) 83 | 84 | n = len(alice_trace) - 1 # the trace has n + 1 entries 85 | 86 | assert is_power_of_2(n) 87 | 88 | h_a = [sha256(encode_wit_element(x)) for x in alice_trace] 89 | h_b = [sha256(encode_wit_element(x)) for x in bob_trace] 90 | 91 | def t_from_trace(trace, i, j): 92 | assert len(trace) > j 93 | assert 0 <= i < n 94 | assert i <= j < n 95 | 96 | assert j >= i and is_power_of_2(j - i + 1) 97 | 98 | m = (j - i + 1) // 2 99 | 100 | if i == j: 101 | return sha256(trace[i] + trace[i + 1]) 102 | else: 103 | return sha256(trace[i] + trace[j + 1] + t_from_trace(trace, i, i + m - 1) + t_from_trace(trace, i + m, j)) 104 | 105 | def t_node_a(i, j) -> bytes: 106 | return t_from_trace(h_a, i, j) 107 | 108 | def t_node_b(i, j) -> bytes: 109 | return t_from_trace(h_b, i, j) 110 | 111 | x = 2 112 | y = alice_trace[-1] 113 | z = bob_trace[-1] 114 | 115 | assert z == 2 * 256 # Bob is saying the truth 116 | 117 | alice_signer = SchnorrSigner(alice_key) 118 | bob_signer = SchnorrSigner(bob_key) 119 | 120 | # Game starts, the UTXO is funded 121 | G = G256_S0(alice_key.pubkey[1:], bob_key.pubkey[1:]) 122 | 123 | inst = manager.fund_instance(G, AMOUNT) 124 | 125 | # Bob chooses its input 126 | [inst] = inst('choose', bob_signer)(x=x) 127 | 128 | assert isinstance(inst.contract, G256_S1) 129 | assert isinstance(inst.data_expanded, G256_S1.State) and inst.data_expanded.x == x 130 | 131 | t_a = t_node_a(0, n - 1) # trace root according to Alice 132 | t_b = t_node_b(0, n - 1) # trace root according to Bob 133 | 134 | # Alice reveals her answer 135 | [inst] = inst('reveal', alice_signer)(x=x, y=y, t_a=t_a) 136 | 137 | assert isinstance(inst.contract, G256_S2) 138 | assert inst.data_expanded == G256_S2.State(t_a=t_a, x=x, y=y) 139 | 140 | # Bob disagrees and starts the challenge 141 | [inst] = inst('start_challenge', bob_signer)( 142 | t_a=t_a, 143 | x=x, 144 | y=y, 145 | z=z, 146 | t_b=t_b 147 | ) 148 | 149 | # inst now represents a step in the bisection protocol corresponding to the root of the computation 150 | 151 | assert isinstance(inst.contract, Bisect_1) 152 | assert inst.contract.i == 0 and inst.contract.j == 7 153 | i, j = inst.contract.i, inst.contract.j 154 | m = (j - i + 1) // 2 155 | [inst] = inst('alice_reveal', alice_signer)( 156 | h_start=h_a[i], 157 | h_end_a=h_a[j + 1], 158 | h_end_b=h_b[j + 1], 159 | trace_a=t_node_a(i, j), 160 | trace_b=t_node_b(i, j), 161 | h_mid_a=h_a[i + m], 162 | trace_left_a=t_node_a(i, i + m - 1), 163 | trace_right_a=t_node_a(i + m, j) 164 | ) 165 | report.write("Fraud proof", format_tx_markdown(inst.funding_tx, "Bisection (Alice)")) 166 | 167 | assert isinstance(inst.contract, Bisect_2) 168 | assert inst.contract.i == 0 and inst.contract.j == 7 169 | 170 | [inst] = inst('bob_reveal_right', bob_signer)( 171 | h_start=h_a[i], 172 | h_end_a=h_a[j + 1], 173 | h_end_b=h_b[j + 1], 174 | trace_a=t_node_a(i, j), 175 | trace_b=t_node_b(i, j), 176 | h_mid_a=h_a[i + m], 177 | trace_left_a=t_node_a(i, i + m - 1), 178 | trace_right_a=t_node_a(i + m, j), 179 | h_mid_b=h_b[i + m], 180 | trace_left_b=t_node_b(i, i + m - 1), 181 | trace_right_b=t_node_b(i + m, j), 182 | ) 183 | report.write("Fraud proof", format_tx_markdown(inst.funding_tx, "Bisection (Bob, right child)")) 184 | 185 | assert isinstance(inst.contract, Bisect_1) 186 | i, j = inst.contract.i, inst.contract.j 187 | m = (j - i + 1) // 2 188 | assert i == 4 and j == 7 189 | 190 | # Bisection repeats on the node covering from index 4 to index 7 191 | [inst] = inst('alice_reveal', alice_signer)( 192 | h_start=h_a[i], 193 | h_end_a=h_a[j + 1], 194 | h_end_b=h_b[j + 1], 195 | trace_a=t_node_a(i, j), 196 | trace_b=t_node_b(i, j), 197 | h_mid_a=h_a[i + m], 198 | trace_left_a=t_node_a(i, i + m - 1), 199 | trace_right_a=t_node_a(i + m, j) 200 | ) 201 | report.write("Fraud proof", format_tx_markdown(inst.funding_tx, "Bisection (Alice)")) 202 | 203 | assert isinstance(inst.contract, Bisect_2) 204 | assert inst.contract.i == 4 and inst.contract.j == 7 205 | 206 | [inst] = inst('bob_reveal_left', bob_signer)( 207 | h_start=h_a[i], 208 | h_end_a=h_a[j + 1], 209 | h_end_b=h_b[j + 1], 210 | trace_a=t_node_a(i, j), 211 | trace_b=t_node_b(i, j), 212 | h_mid_a=h_a[i + m], 213 | trace_left_a=t_node_a(i, i + m - 1), 214 | trace_right_a=t_node_a(i + m, j), 215 | h_mid_b=h_b[i + m], 216 | trace_left_b=t_node_b(i, i + m - 1), 217 | trace_right_b=t_node_b(i + m, j), 218 | ) 219 | report.write("Fraud proof", format_tx_markdown(inst.funding_tx, "Bisection (Bob, left child)")) 220 | 221 | assert isinstance(inst.contract, Bisect_1) 222 | i, j = inst.contract.i, inst.contract.j 223 | m = (j - i + 1) // 2 224 | assert i == 4 and j == 5 225 | 226 | # Bisection repeats on the node covering from index 4 to index 5 (last bisection step) 227 | 228 | [inst] = inst('alice_reveal', alice_signer)( 229 | h_start=h_a[i], 230 | h_end_a=h_a[j + 1], 231 | h_end_b=h_b[j + 1], 232 | trace_a=t_node_a(i, j), 233 | trace_b=t_node_b(i, j), 234 | h_mid_a=h_a[i + m], 235 | trace_left_a=t_node_a(i, i + m - 1), 236 | trace_right_a=t_node_a(i + m, j) 237 | ) 238 | report.write("Fraud proof", format_tx_markdown(inst.funding_tx, "Bisection (Alice)")) 239 | 240 | assert isinstance(inst.contract, Bisect_2) 241 | assert inst.contract.i == 4 and inst.contract.j == 5 242 | 243 | [inst] = inst('bob_reveal_right', bob_signer)( 244 | h_start=h_a[i], 245 | h_end_a=h_a[j + 1], 246 | h_end_b=h_b[j + 1], 247 | trace_a=t_node_a(i, j), 248 | trace_b=t_node_b(i, j), 249 | h_mid_a=h_a[i + m], 250 | trace_left_a=t_node_a(i, i + m - 1), 251 | trace_right_a=t_node_a(i + m, j), 252 | h_mid_b=h_b[i + m], 253 | trace_left_b=t_node_b(i, i + m - 1), 254 | trace_right_b=t_node_b(i + m, j), 255 | ) 256 | report.write("Fraud proof", format_tx_markdown(inst.funding_tx, "Bisection (Bob, right child)")) 257 | 258 | # We reached a leaf. Only who was doubling correctly can withdraw 259 | 260 | assert isinstance(inst.contract, Leaf) 261 | 262 | assert alice_trace[5] == bob_trace[5] and alice_trace[6] != bob_trace[6] 263 | 264 | outputs = [ 265 | CTxOut( 266 | nValue=AMOUNT, 267 | scriptPubKey=P2TR(bob_key.pubkey[1:], []).get_tr_info().scriptPubKey 268 | ) 269 | ] 270 | out_instances = inst("bob_reveal", bob_signer, outputs)( 271 | x=bob_trace[5], 272 | h_y_a=h_a[6] 273 | ) 274 | 275 | assert len(out_instances) == 0 276 | 277 | report.write("Fraud proof", format_tx_markdown(inst.spending_tx, "Leaf reveal")) 278 | -------------------------------------------------------------------------------- /tests/test_minivault.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | import pytest 3 | 4 | from examples.vault.minivault_contracts import Vault, Unvaulting 5 | 6 | from matt.btctools import key 7 | from matt.btctools.auth_proxy import AuthServiceProxy, JSONRPCException 8 | from matt.btctools.messages import CTxOut 9 | from matt.contracts import OpaqueP2TR 10 | from matt.manager import ContractManager, SchnorrSigner 11 | from matt.utils import format_tx_markdown 12 | 13 | from test_utils import mine_blocks 14 | 15 | 16 | unvault_priv_key = key.ExtendedKey.deserialize( 17 | "tprv8ZgxMBicQKsPdpwA4vW8DcSdXzPn7GkS2RdziGXUX8k86bgDQLKhyXtB3HMbJhPFd2vKRpChWxgPe787WWVqEtjy8hGbZHqZKeRrEwMm3SN") 18 | recover_priv_key = key.ExtendedKey.deserialize( 19 | "tprv8ZgxMBicQKsPeDvaW4xxmiMXxqakLgvukT8A5GR6mRwBwjsDJV1jcZab8mxSerNcj22YPrusm2Pz5oR8LTw9GqpWT51VexTNBzxxm49jCZZ") 20 | 21 | 22 | locktime = 10 23 | 24 | MiniVaultSpecs = Tuple[str, Vault] 25 | 26 | 27 | V_full: MiniVaultSpecs = ( 28 | "MiniVault", 29 | Vault(None, locktime, recover_priv_key.pubkey[1:], unvault_priv_key.pubkey[1:]) 30 | ) 31 | V_no_partial_revault: MiniVaultSpecs = ( 32 | "MiniVault [no partial revault]", 33 | Vault(None, locktime, recover_priv_key.pubkey[1:], unvault_priv_key.pubkey[1:], has_partial_revault=False) 34 | ) 35 | 36 | V_no_early_recover: MiniVaultSpecs = ( 37 | "MiniVault [no early recover]", 38 | Vault(None, locktime, recover_priv_key.pubkey[1:], unvault_priv_key.pubkey[1:], has_early_recover=False) 39 | ) 40 | 41 | V_light: MiniVaultSpecs = ( 42 | "MiniVault [lightweight - no partial revault, no early recover]", 43 | Vault(None, locktime, recover_priv_key.pubkey[1:], unvault_priv_key.pubkey[1:], 44 | has_partial_revault=False, has_early_recover=False) 45 | ) 46 | 47 | 48 | @pytest.mark.parametrize("minivault_specs", [V_full]) 49 | def test_minivault_recover(minivault_specs: MiniVaultSpecs, manager: ContractManager, report): 50 | vault_description, vault_contract = minivault_specs 51 | 52 | amount = 20_000 53 | 54 | V_inst = manager.fund_instance(vault_contract, amount) 55 | 56 | out_instances = V_inst("recover")(out_i=0) 57 | 58 | out: CTxOut = V_inst.spending_tx.vout[0] 59 | 60 | assert out.nValue == amount 61 | assert out.scriptPubKey == OpaqueP2TR(recover_priv_key.pubkey[1:]).get_tr_info().scriptPubKey 62 | 63 | report.write(vault_description, format_tx_markdown(V_inst.spending_tx, "Recovery from vault, 1 input")) 64 | 65 | assert len(out_instances) == 0 66 | 67 | 68 | @pytest.mark.parametrize("minivault_specs", [V_full, V_no_partial_revault, V_no_early_recover, V_light]) 69 | def test_minivault_trigger_and_recover(minivault_specs: MiniVaultSpecs, manager: ContractManager, report): 70 | vault_description, vault_contract = minivault_specs 71 | 72 | amount = 49999900 73 | 74 | V_inst = manager.fund_instance(vault_contract, amount) 75 | 76 | withdrawal_pk = bytes.fromhex("0981368165440d4fe866f84d75ae53a95b192aa45155735d4cb2a8894b340b8f") 77 | 78 | [U_inst] = V_inst("trigger", signer=SchnorrSigner(unvault_priv_key))( 79 | out_i=0, 80 | withdrawal_pk=withdrawal_pk 81 | ) 82 | 83 | report.write(vault_description, format_tx_markdown(V_inst.spending_tx, "Trigger")) 84 | 85 | out_instances = U_inst("recover")(out_i=0) 86 | 87 | assert len(out_instances) == 0 88 | 89 | report.write(vault_description, format_tx_markdown(U_inst.spending_tx, "Recovery from trigger")) 90 | 91 | 92 | @pytest.mark.parametrize("minivault_specs", [V_full, V_no_partial_revault, V_no_early_recover, V_light]) 93 | def test_minivault_trigger_and_withdraw(minivault_specs: MiniVaultSpecs, rpc: AuthServiceProxy, manager: ContractManager, report): 94 | vault_description, vault_contract = minivault_specs 95 | 96 | signer = SchnorrSigner(unvault_priv_key) 97 | 98 | amount = 49999900 99 | 100 | V_inst = manager.fund_instance(vault_contract, amount) 101 | 102 | withdrawal_pk = bytes.fromhex("0981368165440d4fe866f84d75ae53a95b192aa45155735d4cb2a8894b340b8f") 103 | 104 | [U_inst] = V_inst("trigger", signer=signer)( 105 | out_i=0, 106 | withdrawal_pk=withdrawal_pk 107 | ) 108 | 109 | spend_tx, _ = manager.get_spend_tx( 110 | (U_inst, "withdraw", {"withdrawal_pk": withdrawal_pk}) 111 | ) 112 | 113 | spend_tx.wit.vtxinwit = [manager.get_spend_wit( 114 | U_inst, 115 | "withdraw", 116 | {"withdrawal_pk": withdrawal_pk} 117 | )] 118 | 119 | spend_tx.vin[0].nSequence = locktime 120 | 121 | with pytest.raises(JSONRPCException): 122 | manager.spend_and_wait(U_inst, spend_tx) 123 | 124 | mine_blocks(rpc, locktime - 1) 125 | 126 | manager.spend_and_wait(U_inst, spend_tx) 127 | 128 | report.write(vault_description, format_tx_markdown(U_inst.spending_tx, "Withdraw")) 129 | 130 | 131 | @pytest.mark.parametrize("minivault_specs", [V_full, V_no_early_recover]) 132 | def test_minivault_trigger_with_revault_and_withdraw(minivault_specs: MiniVaultSpecs, rpc: AuthServiceProxy, manager: ContractManager, report): 133 | # get coins on 3 different Vaults, then trigger with partial withdrawal 134 | # one of the vault uses "trigger_with_revault", the others us normal "trigger" 135 | 136 | vault_description, vault_contract = minivault_specs 137 | 138 | amount = 49_999_900 139 | 140 | V_inst_1 = manager.fund_instance(vault_contract, amount) 141 | V_inst_2 = manager.fund_instance(vault_contract, amount) 142 | V_inst_3 = manager.fund_instance(vault_contract, amount) 143 | 144 | withdrawal_pk = bytes.fromhex("0981368165440d4fe866f84d75ae53a95b192aa45155735d4cb2a8894b340b8f") 145 | revault_amount = 20_000_000 146 | 147 | spends = [ 148 | (V_inst_1, "trigger_and_revault", {"out_i": 0, "revault_out_i": 1, "withdrawal_pk": withdrawal_pk}), 149 | (V_inst_2, "trigger", {"out_i": 0, "withdrawal_pk": withdrawal_pk}), 150 | (V_inst_3, "trigger", {"out_i": 0, "withdrawal_pk": withdrawal_pk}), 151 | ] 152 | 153 | spend_tx, sighashes = manager.get_spend_tx(spends, output_amounts={1: revault_amount}) 154 | 155 | spend_tx.wit.vtxinwit = [] 156 | 157 | sigs = [key.sign_schnorr(unvault_priv_key.privkey, sighash) for sighash in sighashes] 158 | 159 | for i, (V_inst_i, action, args) in enumerate(spends): 160 | spend_tx.wit.vtxinwit.append(manager.get_spend_wit( 161 | V_inst_i, 162 | action, 163 | {**args, "sig": sigs[i]} 164 | )) 165 | 166 | [U_inst, V_revault_inst] = manager.spend_and_wait([V_inst_1, V_inst_2, V_inst_3], spend_tx) 167 | 168 | assert isinstance(U_inst.contract, Unvaulting) 169 | assert isinstance(V_revault_inst.contract, Vault) 170 | assert manager.instances.index(U_inst) >= 0 171 | assert manager.instances.index(V_revault_inst) >= 0 172 | 173 | report.write(vault_description, format_tx_markdown(spend_tx, "Trigger (with revault) [3 vault inputs]")) 174 | 175 | spend_tx, _ = manager.get_spend_tx( 176 | (U_inst, "withdraw", {"withdrawal_pk": withdrawal_pk}) 177 | ) 178 | 179 | # TODO: get_spend_wit does not fill the transaction 180 | # according to the template (which the manager doesn't know) 181 | # Figure out a better way to let the framework handle this 182 | spend_tx.wit.vtxinwit = [manager.get_spend_wit( 183 | U_inst, 184 | "withdraw", 185 | {"withdrawal_pk": withdrawal_pk} 186 | )] 187 | 188 | spend_tx.nVersion = 2 189 | spend_tx.vin[0].nSequence = locktime 190 | 191 | mine_blocks(rpc, locktime - 1) 192 | 193 | manager.spend_and_wait(U_inst, spend_tx) 194 | -------------------------------------------------------------------------------- /tests/test_ram.py: -------------------------------------------------------------------------------- 1 | from examples.ram.ram_contracts import RAM 2 | 3 | from matt.btctools.common import sha256 4 | from matt.btctools.messages import CTxOut 5 | from matt.manager import ContractManager 6 | from matt.merkle import MerkleTree 7 | 8 | 9 | AMOUNT = 20_000 10 | 11 | 12 | def test_withdraw(manager: ContractManager): 13 | # tests the "withdraw" clause, that allows spending anywhere as long 14 | # as a valid Merkle proof is provided 15 | for size in [8, 16]: 16 | for leaf_index in [0, 1, 4, size - 2, size - 1]: 17 | data = [sha256(i.to_bytes(1, byteorder='little')) for i in range(size)] 18 | mt = MerkleTree(data) 19 | 20 | R = RAM(len(data)) 21 | R_inst = manager.fund_instance(R, AMOUNT, data=R.State(data)) 22 | 23 | outputs = [ 24 | CTxOut( 25 | nValue=AMOUNT, 26 | scriptPubKey=bytes([0, 0x20, *[0x42]*32]) 27 | ) 28 | ] 29 | 30 | out_instances = R_inst("withdraw", outputs=outputs)( 31 | merkle_root=mt.root, 32 | merkle_proof=mt.prove_leaf(leaf_index) 33 | ) 34 | 35 | assert len(out_instances) == 0 36 | 37 | 38 | def test_write(manager: ContractManager): 39 | # tests the "write" clause, spending the RAM into a new RAM where a single element is modified 40 | size = 8 41 | leaf_index = 5 42 | new_value = sha256("now this is different".encode()) 43 | 44 | data = [sha256(i.to_bytes(1, byteorder='little')) for i in range(size)] 45 | mt = MerkleTree(data) 46 | 47 | R = RAM(len(data)) 48 | R_inst = manager.fund_instance(R, AMOUNT, data=R.State(data)) 49 | 50 | out_instances = R_inst("write")( 51 | merkle_root=mt.root, 52 | new_value=new_value, 53 | merkle_proof=mt.prove_leaf(leaf_index) 54 | ) 55 | 56 | assert len(out_instances) == 1 57 | 58 | assert isinstance(out_instances[0].contract, RAM) 59 | 60 | data_modified = data[:leaf_index] + [new_value] + data[leaf_index + 1:] 61 | mt_modified = MerkleTree(data_modified) 62 | 63 | assert out_instances[0].data == mt_modified.root 64 | 65 | 66 | def test_write_loop(manager: ContractManager): 67 | # spend a RAM contract in a chain, modifying one element each time 68 | size = 8 69 | 70 | data = [sha256(i.to_bytes(1, byteorder='little')) for i in range(size)] 71 | 72 | R = RAM(len(data)) 73 | R_inst = manager.fund_instance(R, AMOUNT, data=R.State(data)) 74 | 75 | for i in range(16): 76 | leaf_index = i % size 77 | new_value = sha256((100 + i).to_bytes(1, byteorder='little')) 78 | 79 | out_instances = R_inst("write")( 80 | merkle_root=MerkleTree(data).root, 81 | new_value=new_value, 82 | merkle_proof=MerkleTree(data).prove_leaf(leaf_index) 83 | ) 84 | 85 | assert len(out_instances) == 1 86 | 87 | R_inst = out_instances[0] 88 | assert isinstance(R_inst.contract, RAM) 89 | 90 | data = data[:leaf_index] + [new_value] + data[leaf_index + 1:] 91 | 92 | assert R_inst.data == MerkleTree(data).root 93 | -------------------------------------------------------------------------------- /tests/test_rps.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from examples.rps.rps_contracts import DEFAULT_STAKE, RPS, RPSGameS0 4 | 5 | from matt.btctools import key 6 | from matt.btctools.auth_proxy import JSONRPCException 7 | from matt.manager import ContractManager, SchnorrSigner 8 | 9 | 10 | import random 11 | 12 | random.seed(0) 13 | 14 | 15 | alice_key = key.ExtendedKey.deserialize( 16 | "tprv8ZgxMBicQKsPdpwA4vW8DcSdXzPn7GkS2RdziGXUX8k86bgDQLKhyXtB3HMbJhPFd2vKRpChWxgPe787WWVqEtjy8hGbZHqZKeRrEwMm3SN") 17 | bob_key = key.ExtendedKey.deserialize( 18 | "tprv8ZgxMBicQKsPeDvaW4xxmiMXxqakLgvukT8A5GR6mRwBwjsDJV1jcZab8mxSerNcj22YPrusm2Pz5oR8LTw9GqpWT51VexTNBzxxm49jCZZ") 19 | 20 | moves = { 21 | "rock": 0, 22 | "paper": 1, 23 | "scissors": 2 24 | } 25 | 26 | alice_signer = SchnorrSigner(alice_key) 27 | bob_signer = SchnorrSigner(bob_key) 28 | 29 | 30 | def test_rps(manager: ContractManager): 31 | m_a = moves["rock"] 32 | 33 | r_a = bytes([random.randint(0, 255) for _ in range(32)]) 34 | c_a = RPS.calculate_hash(m_a, r_a) 35 | 36 | S0 = RPSGameS0(alice_key.pubkey[1:], bob_key.pubkey[1:], c_a) 37 | S0_inst = manager.fund_instance(S0, DEFAULT_STAKE*2) 38 | 39 | # Bob's move 40 | m_b = moves["paper"] 41 | 42 | [S1_inst] = S0_inst("bob_move", bob_signer)(m_b=m_b) 43 | 44 | # cheating attempt 45 | with pytest.raises(JSONRPCException, match='Script failed an OP_EQUALVERIFY operation'): 46 | S1_inst("alice_wins")(m_a=m_a, m_b=m_b, r_a=r_a) 47 | 48 | # cheat a bit less 49 | with pytest.raises(JSONRPCException, match='Script failed an OP_EQUALVERIFY operation'): 50 | S1_inst("tie")(m_a=m_a, m_b=m_b, r_a=r_a) 51 | 52 | # correct adjudication 53 | S1_inst("bob_wins")(m_a=m_a, m_b=m_b, r_a=r_a) 54 | -------------------------------------------------------------------------------- /tests/test_utils/README.md: -------------------------------------------------------------------------------- 1 | # Test utils 2 | 3 | This folder contains shared python utility functions for several pytest test suites in this repository. -------------------------------------------------------------------------------- /tests/test_utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import List 3 | from matt.btctools.auth_proxy import AuthServiceProxy 4 | 5 | 6 | def mine_blocks(rpc: AuthServiceProxy, n_blocks: int) -> List[str]: 7 | address = rpc.getnewaddress() 8 | return rpc.generatetoaddress(n_blocks, address) 9 | -------------------------------------------------------------------------------- /tests/test_utils/utxograph.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | import networkx as nx 3 | from bokeh.io import output_file, save 4 | from bokeh.models import (Arrow, Segment, NormalHead, BoxZoomTool, HoverTool, Plot, Range1d, 5 | ResetTool, Rect, Text, ColumnDataSource, TapTool, CustomJS, Div) 6 | from bokeh.palettes import Spectral4 7 | from bokeh.layouts import column 8 | 9 | from matt.manager import ContractInstance, ContractInstanceStatus, ContractManager 10 | 11 | NODE_WIDTH = 0.2 12 | NODE_HEIGHT = 0.15 13 | 14 | 15 | def instance_info(inst: ContractInstance) -> str: 16 | return f"""{inst.contract} 17 | Data: {inst.data_expanded} 18 | """ 19 | 20 | 21 | def create_utxo_graph(manager: ContractManager, filename: str): 22 | 23 | # Function to calculate the intersection point 24 | def calculate_intersection(sx, sy, ex, ey, width, height): 25 | dx = ex - sx 26 | dy = ey - sy 27 | 28 | if dx == 0: # Vertical line 29 | return (ex, sy + height / 2 * (-1 if ey < sy else 1)) 30 | 31 | slope = dy / dx 32 | if abs(slope) * width / 2 < height / 2: 33 | # Intersects with left/right side 34 | x_offset = width / 2 * (-1 if ex < sx else 1) 35 | y_offset = x_offset * slope 36 | else: 37 | # Intersects with top/bottom 38 | y_offset = height / 2 * (-1 if ey < sy else 1) 39 | x_offset = y_offset / slope 40 | 41 | return (ex - x_offset, ey - y_offset) 42 | 43 | # Prepare Data 44 | 45 | G = nx.Graph() 46 | 47 | node_to_instance: Dict[int, ContractInstance] = {} 48 | 49 | for i, inst in enumerate(manager.instances): 50 | if inst.status in [ContractInstanceStatus.FUNDED, ContractInstanceStatus.SPENT]: 51 | G.add_node(i, label=str(inst.contract)) 52 | node_to_instance[i] = inst 53 | 54 | for i, inst in enumerate(manager.instances): 55 | if inst.next is not None: 56 | for next_inst in inst.next: 57 | i_next = manager.instances.index(next_inst) 58 | G.add_edge(i, i_next) 59 | 60 | # Layout 61 | # TODO: we should find a layout that respects the "transactions", grouping together 62 | # inputs of the same transaction, and positioning UTXOs left-to-right in a 63 | # topological order 64 | pos = nx.spring_layout(G) 65 | 66 | min_x = min(v[0] for v in pos.values()) 67 | max_x = max(v[0] for v in pos.values()) 68 | min_y = min(v[1] for v in pos.values()) 69 | max_y = max(v[1] for v in pos.values()) 70 | 71 | # Convert position to the format bokeh uses 72 | x, y = zip(*pos.values()) 73 | 74 | node_names = [node_to_instance[i].contract.__class__.__name__ for i in G.nodes()] 75 | node_labels = [str(node_to_instance[i].contract) for i in G.nodes()] 76 | node_infos = [instance_info(node_to_instance[i]) for i in G.nodes()] 77 | 78 | source = ColumnDataSource({ 79 | 'x': x, 80 | 'y': y, 81 | 'node_names': node_names, 82 | 'node_labels': node_labels, 83 | 'node_infos': node_infos, 84 | }) 85 | 86 | # Show with Bokeh 87 | plot = Plot(width=1024, height=768, x_range=Range1d(min_x - NODE_WIDTH*2, max_x + NODE_WIDTH*2), 88 | y_range=Range1d(min_y - NODE_HEIGHT*2, max_y + NODE_HEIGHT*2)) 89 | 90 | plot.title.text = "Contracts graph" 91 | 92 | node_hover_tool = HoverTool(tooltips=[("index", "@node_labels")]) 93 | 94 | plot.add_tools(node_hover_tool, BoxZoomTool(), ResetTool()) 95 | 96 | # Nodes as rounded rectangles 97 | node_glyph = Rect(width=NODE_WIDTH, height=NODE_HEIGHT, 98 | fill_color=Spectral4[0], line_color=None, fill_alpha=0.7) 99 | plot.add_glyph(source, node_glyph) 100 | 101 | # Labels for the nodes 102 | labels = Text(x='x', y='y', text='node_names', 103 | text_baseline="middle", text_align="center") 104 | plot.add_glyph(source, labels) 105 | 106 | # Create a Div to display information 107 | info_div = Div(width=200, height=100, sizing_mode="fixed", 108 | text="Click on a node") 109 | 110 | # CustomJS callback to update the Div content 111 | callback = CustomJS(args=dict(info_div=info_div, nodes_source=source), code=""" 112 | const info = info_div; 113 | const selected_node_indices = nodes_source.selected.indices; 114 | 115 | if (selected_node_indices.length > 0) { 116 | const node_index = selected_node_indices[0]; 117 | const node_info = nodes_source.data.node_infos[node_index]; 118 | info.text = node_info; 119 | } else { 120 | info.text = "Click on a node"; 121 | } 122 | """) 123 | 124 | for start_node, end_node in G.edges(): 125 | sx, sy = pos[start_node] 126 | ex, ey = pos[end_node] 127 | 128 | ix_start, iy_start = calculate_intersection( 129 | sx, sy, ex, ey, NODE_WIDTH, NODE_HEIGHT) 130 | ix_end, iy_end = calculate_intersection( 131 | ex, ey, sx, sy, NODE_WIDTH, NODE_HEIGHT) 132 | 133 | start_instance = node_to_instance[start_node] 134 | clause_args = f"{start_instance.spending_clause}" 135 | 136 | edge_source = ColumnDataSource(data={ 137 | 'x0': [ix_start], 138 | 'y0': [iy_start], 139 | 'x1': [ix_end], 140 | 'y1': [iy_end], 141 | 'edge_label': [f"{clause_args}"] 142 | }) 143 | 144 | segment_glyph = Segment(x0='x0', y0='y0', x1='x1', 145 | y1='y1', line_color="black", line_width=2) 146 | segment_renderer = plot.add_glyph(edge_source, segment_glyph) 147 | 148 | arrow_glyph = Arrow(end=NormalHead(fill_color="black", size=10), 149 | x_start='x1', y_start='y1', x_end='x0', y_end='y0', 150 | source=edge_source, line_color="black") 151 | plot.add_layout(arrow_glyph) 152 | 153 | edge_hover = HoverTool(renderers=[segment_renderer], tooltips=[ 154 | ("Clause: ", "@edge_label")]) 155 | plot.add_tools(edge_hover) 156 | 157 | tap_tool = TapTool(callback=callback) 158 | plot.add_tools(tap_tool) 159 | 160 | layout = column(plot, info_div) 161 | 162 | output_file(filename) 163 | save(layout) 164 | -------------------------------------------------------------------------------- /tests/test_vault.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | import pytest 3 | 4 | from examples.vault.vault_contracts import Vault, Unvaulting 5 | 6 | from matt.btctools import key 7 | from matt.btctools.auth_proxy import AuthServiceProxy, JSONRPCException 8 | from matt.btctools.messages import CTxOut 9 | from matt.contracts import OpaqueP2TR 10 | from matt.manager import ContractManager, SchnorrSigner 11 | from matt.utils import format_tx_markdown, make_ctv_template 12 | 13 | from test_utils import mine_blocks 14 | 15 | 16 | unvault_priv_key = key.ExtendedKey.deserialize( 17 | "tprv8ZgxMBicQKsPdpwA4vW8DcSdXzPn7GkS2RdziGXUX8k86bgDQLKhyXtB3HMbJhPFd2vKRpChWxgPe787WWVqEtjy8hGbZHqZKeRrEwMm3SN") 18 | recover_priv_key = key.ExtendedKey.deserialize( 19 | "tprv8ZgxMBicQKsPeDvaW4xxmiMXxqakLgvukT8A5GR6mRwBwjsDJV1jcZab8mxSerNcj22YPrusm2Pz5oR8LTw9GqpWT51VexTNBzxxm49jCZZ") 20 | 21 | 22 | locktime = 10 23 | 24 | VaultSpecs = Tuple[str, Vault] 25 | 26 | 27 | V_full: VaultSpecs = ( 28 | "Vault", 29 | Vault(None, locktime, recover_priv_key.pubkey[1:], unvault_priv_key.pubkey[1:]) 30 | ) 31 | V_no_partial_revault: VaultSpecs = ( 32 | "Vault [no partial revault]", 33 | Vault(None, locktime, recover_priv_key.pubkey[1:], unvault_priv_key.pubkey[1:], has_partial_revault=False) 34 | ) 35 | 36 | V_no_early_recover: VaultSpecs = ( 37 | "Vault [no early recover]", 38 | Vault(None, locktime, recover_priv_key.pubkey[1:], unvault_priv_key.pubkey[1:], has_early_recover=False) 39 | ) 40 | 41 | V_light: VaultSpecs = ( 42 | "Vault [lightweight - no partial revault, no early recover]", 43 | Vault(None, locktime, recover_priv_key.pubkey[1:], unvault_priv_key.pubkey[1:], 44 | has_partial_revault=False, has_early_recover=False) 45 | ) 46 | 47 | 48 | @pytest.mark.parametrize("vault_specs", [V_full, V_no_partial_revault]) 49 | def test_vault_recover(vault_specs: VaultSpecs, manager: ContractManager, report): 50 | vault_description, vault_contract = vault_specs 51 | 52 | amount = 20_000 53 | 54 | V_inst = manager.fund_instance(vault_contract, amount) 55 | 56 | out_instances = V_inst("recover")(out_i=0) 57 | 58 | out: CTxOut = V_inst.spending_tx.vout[0] 59 | 60 | assert out.nValue == amount 61 | assert out.scriptPubKey == OpaqueP2TR(recover_priv_key.pubkey[1:]).get_tr_info().scriptPubKey 62 | 63 | report.write(vault_description, format_tx_markdown(V_inst.spending_tx, "Recovery from vault, 1 input")) 64 | 65 | assert len(out_instances) == 0 66 | 67 | 68 | @pytest.mark.parametrize("vault_specs", [V_full, V_no_partial_revault, V_no_early_recover, V_light]) 69 | def test_vault_trigger_and_recover(vault_specs: VaultSpecs, manager: ContractManager, report): 70 | vault_description, vault_contract = vault_specs 71 | 72 | amount = 49999900 73 | 74 | V_inst = manager.fund_instance(vault_contract, amount) 75 | 76 | ctv_tmpl = make_ctv_template([ 77 | ("bcrt1qqy0kdmv0ckna90ap6efd6z39wcdtpfa3a27437", 49999900), 78 | ("bcrt1qpnpjyzkfe7n5eppp2ktwpvuxfw5qfn2zjdum83", 49999900), 79 | ("bcrt1q6vqduw24yjjll6nfkxlfy2twwt52w58tnvnd46", 49999900), 80 | ], nSequence=locktime) 81 | 82 | [U_inst] = V_inst("trigger", signer=SchnorrSigner(unvault_priv_key))( 83 | out_i=0, 84 | ctv_hash=ctv_tmpl.get_standard_template_hash(0) 85 | ) 86 | 87 | report.write(vault_description, format_tx_markdown(V_inst.spending_tx, "Trigger")) 88 | 89 | out_instances = U_inst("recover")(out_i=0) 90 | 91 | assert len(out_instances) == 0 92 | 93 | report.write(vault_description, format_tx_markdown(U_inst.spending_tx, "Recovery from trigger")) 94 | 95 | 96 | @pytest.mark.parametrize("vault_specs", [V_full, V_no_partial_revault, V_no_early_recover, V_light]) 97 | def test_vault_trigger_and_withdraw(vault_specs: VaultSpecs, rpc: AuthServiceProxy, manager: ContractManager, report): 98 | vault_description, vault_contract = vault_specs 99 | 100 | signer = SchnorrSigner(unvault_priv_key) 101 | 102 | amount = 49999900 103 | 104 | V_inst = manager.fund_instance(vault_contract, amount) 105 | 106 | ctv_tmpl = make_ctv_template([ 107 | ("bcrt1qqy0kdmv0ckna90ap6efd6z39wcdtpfa3a27437", 16663333), 108 | ("bcrt1qpnpjyzkfe7n5eppp2ktwpvuxfw5qfn2zjdum83", 16663333), 109 | ("bcrt1q6vqduw24yjjll6nfkxlfy2twwt52w58tnvnd46", 16663334), 110 | ], nSequence=locktime) 111 | 112 | [U_inst] = V_inst("trigger", signer=signer)( 113 | out_i=0, 114 | ctv_hash=ctv_tmpl.get_standard_template_hash(0) 115 | ) 116 | 117 | spend_tx, _ = manager.get_spend_tx( 118 | (U_inst, "withdraw", {"ctv_hash": ctv_tmpl.get_standard_template_hash(0)}) 119 | ) 120 | 121 | # TODO: get_spend_wit does not fill the transaction 122 | # according to the template (which the manager doesn't know) 123 | # Figure out a better way to let the framework handle this 124 | spend_tx.wit.vtxinwit = [manager.get_spend_wit( 125 | U_inst, 126 | "withdraw", 127 | {"ctv_hash": ctv_tmpl.get_standard_template_hash(0)} 128 | )] 129 | 130 | spend_tx.nVersion = ctv_tmpl.nVersion 131 | spend_tx.nLockTime = ctv_tmpl.nLockTime 132 | spend_tx.vin[0].nSequence = ctv_tmpl.vin[0].nSequence # we assume only 1 input 133 | spend_tx.vout = ctv_tmpl.vout 134 | 135 | with pytest.raises(JSONRPCException, match='non-BIP68-final'): 136 | manager.spend_and_wait(U_inst, spend_tx) 137 | 138 | mine_blocks(rpc, locktime - 1) 139 | 140 | manager.spend_and_wait(U_inst, spend_tx) 141 | 142 | report.write(vault_description, format_tx_markdown(U_inst.spending_tx, "Withdraw [3 outputs]")) 143 | 144 | 145 | @pytest.mark.parametrize("vault_specs", [V_full, V_no_early_recover]) 146 | def test_vault_trigger_with_revault_and_withdraw(vault_specs: VaultSpecs, rpc: AuthServiceProxy, manager: ContractManager, report): 147 | # get coins on 3 different Vaults, then trigger with partial withdrawal 148 | # one of the vault uses "trigger_with_revault", the others us normal "trigger" 149 | 150 | vault_description, vault_contract = vault_specs 151 | 152 | amount = 49999900 153 | 154 | V_inst_1 = manager.fund_instance(vault_contract, amount) 155 | V_inst_2 = manager.fund_instance(vault_contract, amount) 156 | V_inst_3 = manager.fund_instance(vault_contract, amount) 157 | 158 | ctv_tmpl = make_ctv_template([ 159 | ("bcrt1qqy0kdmv0ckna90ap6efd6z39wcdtpfa3a27437", 49999900), 160 | ("bcrt1qpnpjyzkfe7n5eppp2ktwpvuxfw5qfn2zjdum83", 49999900), 161 | ("bcrt1q6vqduw24yjjll6nfkxlfy2twwt52w58tnvnd46", 29999900), 162 | ], nSequence=locktime) 163 | ctv_hash = ctv_tmpl.get_standard_template_hash(nIn=0) 164 | 165 | revault_amount = 3*amount - sum(out.nValue for out in ctv_tmpl.vout) 166 | 167 | spends = [ 168 | (V_inst_1, "trigger_and_revault", {"out_i": 0, "revault_out_i": 1, "ctv_hash": ctv_hash}), 169 | (V_inst_2, "trigger", {"out_i": 0, "ctv_hash": ctv_hash}), 170 | (V_inst_3, "trigger", {"out_i": 0, "ctv_hash": ctv_hash}), 171 | ] 172 | 173 | spend_tx, sighashes = manager.get_spend_tx(spends, output_amounts={1: revault_amount}) 174 | 175 | spend_tx.wit.vtxinwit = [] 176 | 177 | sigs = [key.sign_schnorr(unvault_priv_key.privkey, sighash) for sighash in sighashes] 178 | 179 | for i, (V_inst_i, action, args) in enumerate(spends): 180 | spend_tx.wit.vtxinwit.append(manager.get_spend_wit( 181 | V_inst_i, 182 | action, 183 | {**args, "sig": sigs[i]} 184 | )) 185 | 186 | [U_inst, V_revault_inst] = manager.spend_and_wait([V_inst_1, V_inst_2, V_inst_3], spend_tx) 187 | 188 | assert isinstance(U_inst.contract, Unvaulting) 189 | assert isinstance(V_revault_inst.contract, Vault) 190 | assert manager.instances.index(U_inst) >= 0 191 | assert manager.instances.index(V_revault_inst) >= 0 192 | 193 | report.write(vault_description, format_tx_markdown(spend_tx, "Trigger (with revault) [3 vault inputs]")) 194 | 195 | spend_tx, _ = manager.get_spend_tx( 196 | (U_inst, "withdraw", {"ctv_hash": ctv_tmpl.get_standard_template_hash(0)}) 197 | ) 198 | 199 | # TODO: get_spend_wit does not fill the transaction 200 | # according to the template (which the manager doesn't know) 201 | # Figure out a better way to let the framework handle this 202 | spend_tx.wit.vtxinwit = [manager.get_spend_wit( 203 | U_inst, 204 | "withdraw", 205 | {"ctv_hash": ctv_tmpl.get_standard_template_hash(0)} 206 | )] 207 | 208 | spend_tx.nVersion = ctv_tmpl.nVersion 209 | spend_tx.nLockTime = ctv_tmpl.nLockTime 210 | spend_tx.vin[0].nSequence = ctv_tmpl.vin[0].nSequence # we assume only 1 input 211 | spend_tx.vout = ctv_tmpl.vout 212 | 213 | mine_blocks(rpc, locktime - 1) 214 | 215 | manager.spend_and_wait(U_inst, spend_tx) 216 | --------------------------------------------------------------------------------