├── .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 |
--------------------------------------------------------------------------------