├── requirements.txt ├── Makefile ├── ms-example-segwit.txt ├── ms-example.txt ├── psbt_faker ├── helpers.py ├── multisig.py ├── base58.py ├── ripemd.py ├── segwit_addr.py ├── serialize.py ├── __init__.py ├── ctransaction.py ├── txn.py ├── psbt.py └── bip32.py ├── LICENSE ├── setup.py ├── .gitignore └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | click>=6.7 2 | ecdsa 3 | # or can use python-secp256k1 4 | 5 | # BBQr library 6 | git+https://github.com/coinkite/BBQr.git@master#egg=bbqr&subdirectory=python 7 | pyqrcode 8 | Pillow -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This Makefile is only useful for maintainers of this package. 2 | 3 | all: 4 | echo Targets: build, tag, upload 5 | 6 | .PHONY: build 7 | build: 8 | python3 setup.py sdist 9 | 10 | .PHONY: upload 11 | upload: FNAME := $(shell ls -1t dist/*-*gz | head -1) 12 | upload: 13 | gpg -u 5A2A5B10 --detach-sign -a $(FNAME) 14 | twine upload $(FNAME)* 15 | 16 | .PHONY: tag 17 | tag: VER := $(shell python -c 'from setup import VERSION; print(VERSION)') 18 | tag: 19 | git tag v$(VER) -am "New release" 20 | git push --tags 21 | 22 | -------------------------------------------------------------------------------- /ms-example-segwit.txt: -------------------------------------------------------------------------------- 1 | # Coldcard Multisig setup file (created on 0F056943) 2 | # 3 | Name: CC-2-of-4 4 | Policy: 2 of 4 5 | Format: P2WSH 6 | 7 | Derivation: m/48'/1'/0'/2' 8 | 9 | 0F056943: tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP 10 | 6BA6CFD0: tpubDFcrvj5n7gyaxWQkoX69k2Zij4vthiAwvN2uhYjDrE6wktKoQaE7gKVZRiTbYdrAYH1UFPGdzdtWJc6WfR2gFMq6XpxA12gCdQmoQNU9mgm 11 | 747B698E: tpubDExj5FnaUnPAn7sHGUeBqD3buoNH5dqmjAT6884vbDpH1iDYWigb7kFo2cA97dc8EHb54u13TRcZxC4kgRS9gc3Ey2xc8c5urytEzTcp3ac 12 | 7BB026BE: tpubDFiuHYSJhNbHcbLJoxWdbjtUcbKR6PvLq53qC1Xq6t93CrRx78W3wcng8vJyQnY3giMJZEgNCRVzTojLb8RqPFpW5Ms2dYpjcJYofN1joyu -------------------------------------------------------------------------------- /ms-example.txt: -------------------------------------------------------------------------------- 1 | # Example Coldcard Multisig setup file 2 | # 3 | # This includes the simulator's default key, and BIP-39 derived keys using passwords 4 | # "Me", "Myself", "And I". It is a 2 of 4 (but that could be edited here) 5 | # 6 | Name: MeMyselfAndI 7 | Policy: 2 of 4 8 | 9 | Derivation: m/45h 10 | 11 | 6BA6CFD0: tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9 12 | 747B698E: tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc 13 | 7BB026BE: tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa 14 | 0F056943: tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n 15 | -------------------------------------------------------------------------------- /psbt_faker/helpers.py: -------------------------------------------------------------------------------- 1 | import struct, hashlib 2 | from .ripemd import ripemd160 3 | 4 | def str2ipath(s): 5 | # convert text to numeric path for BIP174 6 | for i in s.split('/'): 7 | if i == 'm': continue 8 | if not i: continue # trailing or duplicated slashes 9 | 10 | if i[-1] in "'ph": 11 | assert len(i) >= 2, i 12 | here = int(i[:-1]) | 0x80000000 13 | else: 14 | here = int(i) 15 | assert 0 <= here < 0x80000000, here 16 | 17 | yield here 18 | 19 | def xfp2str(xfp): 20 | # Standardized way to show an xpub's fingerprint... it's a 4-byte string 21 | # and not really an integer. Used to show as '0x%08x' but that's wrong endian. 22 | return struct.pack('>I', xfp).hex().upper() 23 | 24 | def str2path(xfp, s): 25 | # output binary needed for BIP-174 26 | p = list(str2ipath(s)) 27 | return bytes.fromhex(xfp) + struct.pack('<%dI' % (len(p)), *p) 28 | 29 | def hash160(data): 30 | return ripemd160(hashlib.sha256(data).digest()) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Coinkite Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # based on 2 | # 3 | # To use this, install with: 4 | # 5 | # pip install --editable . 6 | 7 | from setuptools import setup, find_packages 8 | 9 | VERSION = '1.4' 10 | 11 | with open('README.md', 'rt') as fd: 12 | desc = fd.read() 13 | 14 | if __name__ == '__main__': 15 | setup( 16 | name='psbt_faker', 17 | author='Coinkite Inc.', 18 | author_email='support@coinkite.com', 19 | description="Constructs a valid PSBT files which spend non-existant BTC to random addresses", 20 | version=VERSION, 21 | packages=find_packages(), 22 | long_description=desc, 23 | long_description_content_type="text/markdown", 24 | url="https://github.com/Coldcard/psbt_faker", 25 | py_modules=['psbt_faker'], 26 | python_requires='>3.6.0', 27 | install_requires=[ 28 | "click>=6.7", 29 | "ecdsa", 30 | "pyqrcode", 31 | "Pillow", 32 | "bbqr @ git+https://github.com/coinkite/BBQr.git@master#subdirectory=python", 33 | ], 34 | entry_points=''' 35 | [console_scripts] 36 | psbt_faker=psbt_faker:main 37 | ''', 38 | classifiers=[ 39 | "Programming Language :: Python :: 3", 40 | "License :: OSI Approved :: MIT License", 41 | "Operating System :: OS Independent", 42 | ] 43 | ) 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | public.txt 2 | output.psbt 3 | foo*.psbt 4 | foo*.txt 5 | pp 6 | 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | 113 | .DS_Store 114 | -------------------------------------------------------------------------------- /psbt_faker/multisig.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .bip32 import BIP32Node 3 | 4 | def from_simple_text(lines): 5 | # standard multisig file format - more than one line 6 | M, N = -1, -1 7 | deriv = None 8 | name = None 9 | xpubs = [] 10 | addr_fmt = "p2sh" 11 | for ln in lines: 12 | # remove comments 13 | comm = ln.find('#') 14 | if comm == 0: 15 | continue 16 | if comm != -1: 17 | if not ln[comm + 1:comm + 2].isdigit(): 18 | ln = ln[0:comm] 19 | 20 | ln = ln.strip() 21 | 22 | if ':' not in ln: 23 | if 'pub' in ln: 24 | # pointless optimization: allow bare xpub if we can calc xfp 25 | label = '00000000' 26 | value = ln 27 | else: 28 | # complain? 29 | # if ln: print("no colon: " + ln) 30 | continue 31 | else: 32 | label, value = ln.split(':', 1) 33 | label = label.lower() 34 | 35 | value = value.strip() 36 | 37 | if label == 'name': 38 | name = value 39 | elif label == 'policy': 40 | try: 41 | # accepts: 2 of 3 2/3 2,3 2 3 etc 42 | mat = re.search(r'(\d+)\D*(\d+)', value) 43 | assert mat 44 | M = int(mat.group(1)) 45 | N = int(mat.group(2)) 46 | assert 1 <= M <= N <= 15 47 | except: 48 | raise AssertionError('bad policy line') 49 | 50 | elif label == 'derivation': 51 | # reveal the path derivation for following key(s) 52 | try: 53 | assert value, 'blank' 54 | deriv = value 55 | except BaseException as exc: 56 | raise AssertionError('bad derivation line: ' + str(exc)) 57 | 58 | elif label == 'format': 59 | # pick segwit vs. classic vs. wrapped version 60 | value = value.lower() 61 | assert value in ['p2wsh', 'p2sh', 'p2sh-p2wsh', 'p2wsh-p2sh'] 62 | addr_fmt = value 63 | 64 | elif len(label) == 8: 65 | xpubs.append((label, deriv, BIP32Node.from_hwif(value))) 66 | 67 | return name, addr_fmt, xpubs, M, N -------------------------------------------------------------------------------- /psbt_faker/base58.py: -------------------------------------------------------------------------------- 1 | # This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License 2 | 3 | import hashlib 4 | 5 | 6 | BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 7 | 8 | 9 | def hash256(s: bytes) -> bytes: 10 | """ 11 | two rounds of sha256 12 | 13 | :param s: data 14 | :return: hashed data 15 | """ 16 | return hashlib.sha256(hashlib.sha256(s).digest()).digest() 17 | 18 | 19 | def encode_base58(data: bytes) -> str: 20 | """ 21 | Encode base58. 22 | 23 | :param data: data to encode 24 | :return: base58 encoded string 25 | """ 26 | count = 0 27 | for c in data: 28 | if c == 0: 29 | count += 1 30 | else: 31 | break 32 | num = int.from_bytes(data, 'big') 33 | prefix = '1' * count 34 | result = '' 35 | while num > 0: 36 | num, mod = divmod(num, 58) 37 | result = BASE58_ALPHABET[mod] + result 38 | return prefix + result 39 | 40 | 41 | def encode_base58_checksum(data: bytes) -> str: 42 | """ 43 | Encode base58 checksum. 44 | 45 | :param data: data to encode 46 | :return: base58 encoded string with checksum 47 | """ 48 | return encode_base58(data + hash256(data)[:4]) 49 | 50 | 51 | def decode_base58(s: str) -> bytes: 52 | """ 53 | Decode base58. 54 | 55 | :param s: base58 encoded string 56 | :return: decoded data 57 | """ 58 | num = 0 59 | for c in s: 60 | if c not in BASE58_ALPHABET: 61 | raise ValueError( 62 | "character {} is not valid base58 character".format(c) 63 | ) 64 | num *= 58 65 | num += BASE58_ALPHABET.index(c) 66 | 67 | h = hex(num)[2:] 68 | h = '0' + h if len(h) % 2 else h 69 | res = bytes.fromhex(h) 70 | 71 | # Add padding back. 72 | pad = 0 73 | for c in s[:-1]: 74 | if c == BASE58_ALPHABET[0]: 75 | pad += 1 76 | else: 77 | break 78 | return b'\x00' * pad + res 79 | 80 | 81 | def decode_base58_checksum(s: str) -> bytes: 82 | """ 83 | Decode base58 checksum. 84 | 85 | :param s: base58 encoded string with checksum 86 | :return: decoded data (without checksum) 87 | """ 88 | num_bytes = decode_base58(s=s) 89 | checksum = num_bytes[-4:] 90 | if hash256(num_bytes[:-4])[:4] != checksum: 91 | raise ValueError( 92 | 'bad checksum: {} {}'.format( 93 | checksum, 94 | hash256(num_bytes[:-4])[:4] 95 | ) 96 | ) 97 | return num_bytes[:-4] 98 | 99 | -------------------------------------------------------------------------------- /psbt_faker/ripemd.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 | -------------------------------------------------------------------------------- /psbt_faker/segwit_addr.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017, 2020 Pieter Wuille 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Reference implementation for Bech32/Bech32m and segwit addresses.""" 22 | 23 | 24 | from enum import Enum 25 | 26 | class Encoding(Enum): 27 | """Enumeration type to list the various supported encodings.""" 28 | BECH32 = 1 29 | BECH32M = 2 30 | 31 | CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 32 | BECH32M_CONST = 0x2bc830a3 33 | 34 | def bech32_polymod(values): 35 | """Internal function that computes the Bech32 checksum.""" 36 | generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] 37 | chk = 1 38 | for value in values: 39 | top = chk >> 25 40 | chk = (chk & 0x1ffffff) << 5 ^ value 41 | for i in range(5): 42 | chk ^= generator[i] if ((top >> i) & 1) else 0 43 | return chk 44 | 45 | 46 | def bech32_hrp_expand(hrp): 47 | """Expand the HRP into values for checksum computation.""" 48 | return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] 49 | 50 | 51 | def bech32_verify_checksum(hrp, data): 52 | """Verify a checksum given HRP and converted data characters.""" 53 | const = bech32_polymod(bech32_hrp_expand(hrp) + data) 54 | if const == 1: 55 | return Encoding.BECH32 56 | if const == BECH32M_CONST: 57 | return Encoding.BECH32M 58 | return None 59 | 60 | def bech32_create_checksum(hrp, data, spec): 61 | """Compute the checksum values given HRP and data.""" 62 | values = bech32_hrp_expand(hrp) + data 63 | const = BECH32M_CONST if spec == Encoding.BECH32M else 1 64 | polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const 65 | return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] 66 | 67 | 68 | def bech32_encode(hrp, data, spec): 69 | """Compute a Bech32 string given HRP and data values.""" 70 | combined = data + bech32_create_checksum(hrp, data, spec) 71 | return hrp + '1' + ''.join([CHARSET[d] for d in combined]) 72 | 73 | def bech32_decode(bech): 74 | """Validate a Bech32/Bech32m string, and determine HRP and data.""" 75 | if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or 76 | (bech.lower() != bech and bech.upper() != bech)): 77 | return (None, None, None) 78 | bech = bech.lower() 79 | pos = bech.rfind('1') 80 | if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: 81 | return (None, None, None) 82 | if not all(x in CHARSET for x in bech[pos+1:]): 83 | return (None, None, None) 84 | hrp = bech[:pos] 85 | data = [CHARSET.find(x) for x in bech[pos+1:]] 86 | spec = bech32_verify_checksum(hrp, data) 87 | if spec is None: 88 | return (None, None, None) 89 | return (hrp, data[:-6], spec) 90 | 91 | def convertbits(data, frombits, tobits, pad=True): 92 | """General power-of-2 base conversion.""" 93 | acc = 0 94 | bits = 0 95 | ret = [] 96 | maxv = (1 << tobits) - 1 97 | max_acc = (1 << (frombits + tobits - 1)) - 1 98 | for value in data: 99 | if value < 0 or (value >> frombits): 100 | return None 101 | acc = ((acc << frombits) | value) & max_acc 102 | bits += frombits 103 | while bits >= tobits: 104 | bits -= tobits 105 | ret.append((acc >> bits) & maxv) 106 | if pad: 107 | if bits: 108 | ret.append((acc << (tobits - bits)) & maxv) 109 | elif bits >= frombits or ((acc << (tobits - bits)) & maxv): 110 | return None 111 | return ret 112 | 113 | 114 | def decode(hrp, addr): 115 | """Decode a segwit address.""" 116 | hrpgot, data, spec = bech32_decode(addr) 117 | if hrpgot != hrp: 118 | return (None, None) 119 | decoded = convertbits(data[1:], 5, 8, False) 120 | if decoded is None or len(decoded) < 2 or len(decoded) > 40: 121 | return (None, None) 122 | if data[0] > 16: 123 | return (None, None) 124 | if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: 125 | return (None, None) 126 | if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: 127 | return (None, None) 128 | return (data[0], decoded) 129 | 130 | 131 | def encode(hrp, witver, witprog): 132 | """Encode a segwit address.""" 133 | spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M 134 | ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) 135 | if decode(hrp, ret) == (None, None): 136 | return None 137 | return ret 138 | -------------------------------------------------------------------------------- /psbt_faker/serialize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2010 ArtForz -- public domain half-a-node 3 | # Copyright (c) 2012 Jeff Garzik 4 | # Copyright (c) 2010-2016 The Bitcoin Core developers 5 | # Distributed under the MIT software license, see the accompanying 6 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 7 | """ 8 | Bitcoin Object Python Serializations 9 | ************************************ 10 | 11 | Modified from the test/test_framework/mininode.py file from the 12 | Bitcoin repository 13 | """ 14 | 15 | import struct 16 | from struct import error as struct_err 17 | from typing import List 18 | 19 | 20 | # Serialization/deserialization tools 21 | def ser_compact_size(size: int) -> bytes: 22 | """ 23 | Serialize an integer using Bitcoin's compact size unsigned integer serialization. 24 | 25 | :param size: The int to serialize 26 | :returns: The int serialized as a compact size unsigned integer 27 | """ 28 | r = b"" 29 | if size < 253: 30 | r = struct.pack("B", size) 31 | elif size < 0x10000: 32 | r = struct.pack(" bytes: 60 | """ 61 | Deserialize a variable length byte string serialized with Bitcoin's variable length string serialization from a byte stream. 62 | 63 | :param f: The byte stream 64 | :returns: The byte string that was serialized 65 | """ 66 | nit = deser_compact_size(f) 67 | return f.read(nit) 68 | 69 | def ser_string(s: bytes) -> bytes: 70 | """ 71 | Serialize a byte string with Bitcoin's variable length string serialization. 72 | 73 | :param s: The byte string to be serialized 74 | :returns: The serialized byte string 75 | """ 76 | return ser_compact_size(len(s)) + s 77 | 78 | def deser_uint256(f) -> int: 79 | """ 80 | Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte stream. 81 | 82 | :param f: The byte stream. 83 | :returns: The integer that was serialized 84 | """ 85 | r = 0 86 | for i in range(8): 87 | t = struct.unpack(" bytes: 93 | """ 94 | Serialize a 256 bit integer with Bitcoin's 256 bit integer serialization. 95 | 96 | :param u: The integer to serialize 97 | :returns: The serialized 256 bit integer 98 | """ 99 | rs = b"" 100 | for _ in range(8): 101 | rs += struct.pack(">= 32 103 | return rs 104 | 105 | 106 | def uint256_from_str(s: bytes) -> int: 107 | """ 108 | Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte string. 109 | 110 | :param s: The byte string 111 | :returns: The integer that was serialized 112 | """ 113 | r = 0 114 | t = struct.unpack(" List: 121 | """ 122 | Deserialize a vector of objects with Bitcoin's object vector serialization from a byte stream. 123 | 124 | :param f: The byte stream 125 | :param c: The class of object to deserialize for each object in the vector 126 | :returns: A list of objects that were serialized 127 | """ 128 | nit = deser_compact_size(f) 129 | r = [] 130 | for _ in range(nit): 131 | t = c() 132 | t.deserialize(f) 133 | r.append(t) 134 | return r 135 | 136 | 137 | def ser_vector(v) -> bytes: 138 | """ 139 | Serialize a vector of objects with Bitcoin's object vector serialzation. 140 | 141 | :param v: The list of objects to serialize 142 | :returns: The serialized objects 143 | """ 144 | r = ser_compact_size(len(v)) 145 | for i in v: 146 | r += i.serialize() 147 | return r 148 | 149 | 150 | def deser_string_vector(f) -> List[bytes]: 151 | """ 152 | Deserialize a vector of byte strings from a byte stream. 153 | 154 | :param f: The byte stream 155 | :returns: The list of byte strings that were serialized 156 | """ 157 | nit = deser_compact_size(f) 158 | r = [] 159 | for _ in range(nit): 160 | t = deser_string(f) 161 | r.append(t) 162 | return r 163 | 164 | 165 | def ser_string_vector(v: List[bytes]) -> bytes: 166 | """ 167 | Serialize a list of byte strings as a vector of byte strings. 168 | 169 | :param v: The list of byte strings to serialize 170 | :returns: The serialized list of byte strings 171 | """ 172 | r = ser_compact_size(len(v)) 173 | for sv in v: 174 | r += ser_string(sv) 175 | return r 176 | 177 | def ser_sig_der(r: bytes, s: bytes) -> bytes: 178 | """ 179 | Serialize the ``r`` and ``s`` values of an ECDSA signature using DER. 180 | 181 | :param r: The ``r`` value bytes 182 | :param s: The ``s`` value bytes 183 | :returns: The DER encoded signature 184 | """ 185 | sig = b"\x30" 186 | 187 | # Make r and s as short as possible 188 | ri = 0 189 | for b in r: 190 | if b == 0: 191 | ri += 1 192 | else: 193 | break 194 | r = r[ri:] 195 | si = 0 196 | for b in s: 197 | if b == 0: 198 | si += 1 199 | else: 200 | break 201 | s = s[si:] 202 | 203 | # Make positive of neg 204 | first = r[0] 205 | if first & (1 << 7) != 0: 206 | r = b"\x00" + r 207 | first = s[0] 208 | if first & (1 << 7) != 0: 209 | s = b"\x00" + s 210 | 211 | # Write total length 212 | total_len = len(r) + len(s) + 4 213 | sig += struct.pack("B", total_len) 214 | 215 | # write r 216 | sig += b"\x02" 217 | sig += struct.pack("B", len(r)) 218 | sig += r 219 | 220 | # write s 221 | sig += b"\x02" 222 | sig += struct.pack("B", len(s)) 223 | sig += s 224 | 225 | sig += b"\x01" 226 | return sig 227 | 228 | def ser_sig_compact(r: bytes, s: bytes, recid: bytes) -> bytes: 229 | """ 230 | Serialize the ``r`` and ``s`` values of an ECDSA signature using the compact signature serialization scheme. 231 | 232 | :param r: The ``r`` value bytes 233 | :param s: The ``s`` value bytes 234 | :returns: The compact signature 235 | """ 236 | rec = struct.unpack("B", recid)[0] 237 | prefix = struct.pack("B", 27 + 4 + rec) 238 | 239 | sig = b"" 240 | sig += prefix 241 | sig += r + s 242 | 243 | return sig 244 | -------------------------------------------------------------------------------- /psbt_faker/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # To use this, install with: 4 | # 5 | # pip install --editable . 6 | # 7 | # That will create the command "psbt_faker" in your path... or just use "./main.py ..." here 8 | # 9 | # 10 | import click, io, pyqrcode, os 11 | from binascii import b2a_hex as _b2a_hex 12 | from base64 import b64encode 13 | from decimal import Decimal 14 | from .txn import fake_ms_txn, fake_txn, ADDR_STYLES 15 | from .multisig import from_simple_text 16 | from bbqr import split_qrs 17 | from PIL import Image, ImageDraw, ImageChops 18 | 19 | 20 | b2a_hex = lambda a: str(_b2a_hex(a), 'ascii') 21 | #xfp2hex = lambda a: b2a_hex(a[::-1]).upper() 22 | 23 | SIM_XPUB = 'tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh' 24 | 25 | def parse_int_string(v): 26 | return int(v, 0) 27 | 28 | def repeat_until_length(values, target_length): 29 | # Creates a list of target_length length, by repeating the values. 30 | return [values[i % len(values)] for i in range(target_length)] 31 | 32 | @click.command() 33 | @click.argument('out_psbt', type=click.File('wb'), metavar="OUTPUT.PSBT") 34 | @click.argument('xpub', type=str, default=SIM_XPUB) 35 | @click.option('--num-ins', '-i', help="Number of inputs (default 1)", default=1) 36 | @click.option('--sequence', '-S', help="Sequence number for each input", multiple=True, type=parse_int_string) 37 | @click.option('--num-outs', '-o', help="Number of all txn outputs (default 2)", default=2) 38 | @click.option('--output-amount', '-O', help="Amount of each non-change output in sats (default: all outputs are the same)", multiple=True, default=(), type=int) 39 | @click.option('--num-change', '-c', help="Number of change outputs (default 1) from num-outs", default=1) 40 | @click.option('--fee', '-f', help="Miner's fee in Satoshis", default=1000) 41 | @click.option('--psbt2', '-2', help="Make PSBTv2", is_flag=True, default=False) 42 | @click.option('--bbqr', '-b', help="Output BBQr OUTPUT.gif in CWD", is_flag=True, default=False) 43 | @click.option('--segwit', '-s', help="[SS] Make inputs be segwit style", is_flag=True, default=False) 44 | @click.option('--wrapped', '-w', help="[SS] Make inputs be wrapped segwit style (requires --segwit flag)", is_flag=True, default=False) 45 | @click.option('--styles', '-a', help="Output address style (multiple ok). If multisig only applies to non-change addresses.", multiple=True, default=None, type=click.Choice(ADDR_STYLES)) 46 | @click.option('--base64', '-6', help="Output base64 (default binary)", is_flag=True, default=False) 47 | @click.option('--testnet', '-t', help="Assume testnet4 addresses (default mainnet)", is_flag=True, default=False) 48 | @click.option('--partial', '-p', help="[SS] Change first input so its different XPUB and result cannot be finalized", is_flag=True, default=False) 49 | @click.option('--zero-xfp', '-z', help="[SS] Provide zero XFP and junk XPUB (cannot be signed, but should be decodable)", is_flag=True, default=False) 50 | @click.option('--multisig', '-m', type=click.File('rt'), metavar="config.txt", help="[MS] CC Multisig config file (text)", default=None) 51 | @click.option('--locktime', '-l', help="nLocktime value (default 0), use 'current' to fetch best block height from mempool.space", default="0") 52 | @click.option('--input-amount', '-n', help="Size of each input in sats (default 100k sats each input)", default=(100000,), multiple=True, type=int) 53 | @click.option('--incl-xpubs', '-I', help="[MS] Include XPUBs in PSBT global section", is_flag=True, default=False) 54 | def main(num_ins, sequence, num_change, num_outs, output_amount, out_psbt, testnet, xpub, segwit, fee, styles, base64, 55 | partial, zero_xfp, multisig, locktime, input_amount, psbt2, incl_xpubs, wrapped, bbqr): 56 | '''Construct a valid PSBT which spends non-existant BTC to random addresses!''' 57 | 58 | if locktime == "current": 59 | try: 60 | import urllib.request 61 | u = urllib.request.urlopen("https://mempool.space/api/blocks/tip/height") 62 | locktime = int(u.read().decode()) 63 | except: 64 | locktime = 0 65 | else: 66 | locktime = int(locktime) 67 | 68 | input_amount = repeat_until_length(input_amount, num_ins) 69 | change_outputs=list(range(num_change)) # always the first outputs 70 | 71 | outvals = None 72 | if output_amount: # not empty 73 | output_amount = repeat_until_length(output_amount, num_outs - num_change) 74 | if num_change > 0: 75 | change_amount = int(round((sum(input_amount) - fee - sum(output_amount)) / num_change)) 76 | if change_amount <= 0: 77 | raise ValueError("Change amount is too large") 78 | outvals = [change_amount] * num_change + output_amount 79 | else: 80 | outvals = output_amount 81 | # Update to final fee, after rounding and change amount calculation. 82 | fee = sum(input_amount) - sum(outvals) 83 | 84 | if len(sequence) > 0: 85 | sequence = repeat_until_length(sequence, num_ins) 86 | else: 87 | sequence = None 88 | 89 | if multisig: 90 | ms_config = multisig.read() 91 | name, af, keys, M, N = from_simple_text(ms_config.split("\n")) 92 | psbt, outs = fake_ms_txn(input_amount, num_outs, M, keys, fee=fee, outvals=outvals, locktime=locktime, 93 | change_outputs=change_outputs, outstyles=styles, 94 | psbt_v2=psbt2, change_af=af, 95 | incl_xpubs=incl_xpubs, sequences=sequence, is_testnet=testnet) 96 | else: 97 | if zero_xfp: 98 | xpub = None 99 | 100 | psbt, outs = fake_txn(input_amount, num_outs, master_xpub=xpub, outvals=outvals, fee=fee, 101 | segwit_in=segwit, outstyles=styles, locktime=locktime, 102 | partial=partial, is_testnet=testnet, wrapped=wrapped, 103 | change_outputs=change_outputs, 104 | psbt_v2=psbt2, sequences=sequence) 105 | 106 | res = b64encode(psbt) if base64 else psbt 107 | out_psbt.write(res) 108 | 109 | print(f"\nFake PSBT would send {(sum(input_amount)/Decimal(1E8))} BTC to: ") 110 | print('\n'.join(" %.8f => %s %s" % (Decimal(amt)/Decimal(1E8),dest, ' (change back)' if chg else '') 111 | for amt,dest,chg in outs)) 112 | if fee: 113 | print(" %.8f => miners fee" % (Decimal(fee)/Decimal(1E8))) 114 | 115 | print("\nPSBT to be signed: " + out_psbt.name, end='\n\n') 116 | 117 | if bbqr: 118 | bbqr_out_file = os.path.splitext(out_psbt.name)[0] + ".gif" 119 | vers, parts = split_qrs(res, type_code="U" if base64 else "P", encoding="2", 120 | max_version=20) 121 | qs = [pyqrcode.create(data, error='L', version=vers, mode='alphanumeric') for 122 | data in parts] 123 | 124 | frames = [] 125 | num_parts = len(parts) 126 | for i in range(num_parts): 127 | xbm = qs[i].xbm(scale=4, quiet_zone=10) 128 | img = ImageChops.invert(Image.open(io.BytesIO(xbm.encode()))).convert('L') 129 | if num_parts > 1: 130 | # add progress bar 131 | pw = img.width // num_parts 132 | lm = (img.width - (pw * num_parts)) // 2 133 | draw = ImageDraw.Draw(img) 134 | h = 4 // 2 135 | y = img.height - h - (4 // 2) - 1 136 | 137 | for j in range(num_parts): 138 | draw.rectangle((lm + (j * pw), y, lm + ((j + 1) * pw), y + h), fill=(128 if i != j else 0)) 139 | 140 | frames.append(img) 141 | 142 | if num_parts == 1: 143 | frames[0].save(bbqr_out_file) 144 | else: 145 | frames[0].save(bbqr_out_file, format="gif", save_all=True, loop=0, 146 | duration=250, default_image=False, append_images=frames[1:]) 147 | 148 | 149 | if __name__ == '__main__': 150 | main() 151 | 152 | # EOF 153 | -------------------------------------------------------------------------------- /psbt_faker/ctransaction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2010 ArtForz -- public domain half-a-node 3 | # Copyright (c) 2012 Jeff Garzik 4 | # Copyright (c) 2010-2016 The Bitcoin Core developers 5 | # Distributed under the MIT software license, see the accompanying 6 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 7 | """Bitcoin Object Python Serializations 8 | 9 | Modified from the test/test_framework/mininode.py file from the 10 | Bitcoin repository 11 | 12 | CTransaction,CTxIn, CTxOut, etc....: 13 | data structures that should map to corresponding structures in 14 | bitcoin/primitives for transactions only 15 | """ 16 | 17 | import copy, struct, hashlib 18 | 19 | from .serialize import ( 20 | deser_uint256, 21 | deser_string, 22 | deser_string_vector, 23 | deser_vector, 24 | ser_uint256, 25 | ser_string, 26 | ser_string_vector, 27 | ser_vector, 28 | uint256_from_str, 29 | ) 30 | 31 | from typing import ( 32 | List, 33 | Optional, 34 | ) 35 | 36 | # Objects that map to bitcoind objects, which can be serialized/deserialized 37 | 38 | MSG_WITNESS_FLAG = 1 << 30 39 | 40 | 41 | def hash256(data): 42 | return hashlib.sha256(hashlib.sha256(data).digest()).digest() 43 | 44 | class COutPoint(object): 45 | def __init__(self, hash: int = 0, n: int = 0xffffffff): 46 | self.hash = hash 47 | self.n = n 48 | 49 | def deserialize(self, f) -> None: 50 | self.hash = deser_uint256(f) 51 | self.n = struct.unpack(" bytes: 54 | r = b"" 55 | r += ser_uint256(self.hash) 56 | r += struct.pack(" str: 60 | return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n) 61 | 62 | 63 | class CTxIn(object): 64 | def __init__( 65 | self, 66 | outpoint: Optional[COutPoint] = None, 67 | scriptSig: bytes = b"", 68 | nSequence: int = 0, 69 | ): 70 | if outpoint is None: 71 | self.prevout = COutPoint() 72 | else: 73 | self.prevout = outpoint 74 | self.scriptSig = scriptSig 75 | self.nSequence = nSequence 76 | 77 | def deserialize(self, f) -> None: 78 | self.prevout = COutPoint() 79 | self.prevout.deserialize(f) 80 | self.scriptSig = deser_string(f) 81 | self.nSequence = struct.unpack(" bytes: 84 | r = b"" 85 | r += self.prevout.serialize() 86 | r += ser_string(self.scriptSig) 87 | r += struct.pack(" str: 91 | return "CTxIn(prevout=%s scriptSig=%s nSequence=%i)" \ 92 | % (repr(self.prevout), self.scriptSig.hex(), 93 | self.nSequence) 94 | 95 | 96 | class CTxOut(object): 97 | def __init__(self, nValue: int = 0, scriptPubKey: bytes = b""): 98 | self.nValue = nValue 99 | self.scriptPubKey = scriptPubKey 100 | 101 | def deserialize(self, f) -> None: 102 | self.nValue = struct.unpack(" bytes: 106 | r = b"" 107 | r += struct.pack(" str: 112 | return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \ 113 | % (self.nValue // 100_000_000, self.nValue % 100_000_000, self.scriptPubKey.hex()) 114 | 115 | 116 | class CScriptWitness(object): 117 | def __init__(self) -> None: 118 | # stack is a vector of strings 119 | self.stack: List[bytes] = [] 120 | 121 | def __repr__(self) -> str: 122 | return "CScriptWitness(%s)" % \ 123 | (",".join([x.hex() for x in self.stack])) 124 | 125 | def is_null(self) -> bool: 126 | if self.stack: 127 | return False 128 | return True 129 | 130 | 131 | class CTxInWitness(object): 132 | def __init__(self) -> None: 133 | self.scriptWitness = CScriptWitness() 134 | 135 | def deserialize(self, f) -> None: 136 | self.scriptWitness.stack = deser_string_vector(f) 137 | 138 | def serialize(self) -> bytes: 139 | return ser_string_vector(self.scriptWitness.stack) 140 | 141 | def __repr__(self) -> str: 142 | return repr(self.scriptWitness) 143 | 144 | def is_null(self) -> bool: 145 | return self.scriptWitness.is_null() 146 | 147 | 148 | class CTxWitness(object): 149 | def __init__(self) -> None: 150 | self.vtxinwit: List[CTxInWitness] = [] 151 | 152 | def deserialize(self, f) -> None: 153 | for i in range(len(self.vtxinwit)): 154 | self.vtxinwit[i].deserialize(f) 155 | 156 | def serialize(self) -> bytes: 157 | r = b"" 158 | # This is different than the usual vector serialization -- 159 | # we omit the length of the vector, which is required to be 160 | # the same length as the transaction's vin vector. 161 | for x in self.vtxinwit: 162 | r += x.serialize() 163 | return r 164 | 165 | def __repr__(self) -> str: 166 | return "CTxWitness(%s)" % \ 167 | (';'.join([repr(x) for x in self.vtxinwit])) 168 | 169 | def is_null(self) -> bool: 170 | for x in self.vtxinwit: 171 | if not x.is_null(): 172 | return False 173 | return True 174 | 175 | 176 | class CTransaction(object): 177 | def __init__(self, tx: Optional['CTransaction'] = None) -> None: 178 | if tx is None: 179 | self.nVersion = 1 180 | self.vin: List[CTxIn] = [] 181 | self.vout: List[CTxOut] = [] 182 | self.wit = CTxWitness() 183 | self.nLockTime = 0 184 | self.sha256: Optional[int] = None 185 | self.hash: Optional[bytes] = None 186 | else: 187 | self.nVersion = tx.nVersion 188 | self.vin = copy.deepcopy(tx.vin) 189 | self.vout = copy.deepcopy(tx.vout) 190 | self.nLockTime = tx.nLockTime 191 | self.sha256 = tx.sha256 192 | self.hash = tx.hash 193 | self.wit = copy.deepcopy(tx.wit) 194 | 195 | def deserialize(self, f) -> None: 196 | self.nVersion = struct.unpack(" bytes: 216 | r = b"" 217 | r += struct.pack(" bytes: 225 | flags = 0 226 | if not self.wit.is_null(): 227 | flags |= 1 228 | r = b"" 229 | r += struct.pack(" bytes: 248 | return self.serialize_without_witness() 249 | 250 | # Recalculate the txid (transaction hash without witness) 251 | def rehash(self) -> None: 252 | self.sha256 = None 253 | self.calc_sha256() 254 | 255 | # We will only cache the serialization without witness in 256 | # self.sha256 and self.hash -- those are expected to be the txid. 257 | def calc_sha256(self, with_witness: bool = False) -> Optional[int]: 258 | if with_witness: 259 | # Don't cache the result, just return it 260 | return uint256_from_str(hash256(self.serialize_with_witness())) 261 | 262 | if self.sha256 is None: 263 | self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) 264 | self.hash = hash256(self.serialize()) 265 | return None 266 | 267 | def is_null(self) -> bool: 268 | return len(self.vin) == 0 and len(self.vout) == 0 269 | 270 | def txid(self): 271 | # convenience 272 | if self.sha256 is None: 273 | self.calc_sha256() 274 | return self.sha256.to_bytes(32, "big") 275 | 276 | def __repr__(self) -> str: 277 | return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ 278 | % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSBT Faker 2 | 3 | A simple program to create test PSBT files, that are plausible and 4 | self-consistent so that PSBT-signing tools will actually sign them. 5 | Does not involve any blockchains... completely made up inputs and 6 | output addresses are chosen at random. 7 | 8 | You should use the XPUB of the Coldcard you want experiment against. 9 | This can be retrieved using `ckcc xpub` with the `ckcc-protocol` 10 | CLI tool, or by exporting the wallet (see Advanced > MicroSD > Export Wallet menu). 11 | 12 | For the Coldcard Simulator, you could use `tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh` which is also default. 13 | 14 | ## Installation 15 | 16 | ```sh 17 | git clone https://github.com/Coldcard/psbt_faker.git 18 | cd psbt_faker 19 | python3 -m pip install -U pip setuptools 20 | python3 -m pip install --editable . 21 | rehash 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```sh 27 | $ psbt_faker --help 28 | Usage: psbt_faker [OPTIONS] OUTPUT.PSBT [XPUB] 29 | 30 | Construct a valid PSBT which spends non-existant BTC to random addresses! 31 | 32 | Options: 33 | -i, --num-ins INTEGER Number of inputs (default 1) 34 | -S, --sequence PARSE_INT_STRING 35 | Sequence number for each input 36 | -o, --num-outs INTEGER Number of all txn outputs (default 2) 37 | -O, --output-amount INTEGER Amount of each non-change output in sats 38 | (default: all outputs are the same) 39 | -c, --num-change INTEGER Number of change outputs (default 1) from 40 | num-outs 41 | -f, --fee INTEGER Miner's fee in Satoshis 42 | -2, --psbt2 Make PSBTv2 43 | -b, --bbqr Output BBQr OUTPUT.gif in CWD 44 | -s, --segwit [SS] Make inputs be segwit style 45 | -w, --wrapped [SS] Make inputs be wrapped segwit style 46 | (requires --segwit flag) 47 | -a, --styles [p2wsh|p2sh|p2sh-p2wsh|p2wsh-p2sh|p2wpkh|p2pkh|p2wpkh-p2sh|p2sh-p2wpkh|p2tr] 48 | Output address style (multiple ok). If 49 | multisig only applies to non-change 50 | addresses. 51 | -6, --base64 Output base64 (default binary) 52 | -t, --testnet Assume testnet4 addresses (default mainnet) 53 | -p, --partial [SS] Change first input so its different 54 | XPUB and result cannot be finalized 55 | -z, --zero-xfp [SS] Provide zero XFP and junk XPUB (cannot 56 | be signed, but should be decodable) 57 | -m, --multisig config.txt [MS] CC Multisig config file (text) 58 | -l, --locktime TEXT nLocktime value (default 0), use 'current' 59 | to fetch best block height from 60 | mempool.space 61 | -n, --input-amount INTEGER Size of each input in sats (default 100k 62 | sats each input) 63 | -I, --incl-xpubs [MS] Include XPUBs in PSBT global section 64 | --help Show this message and exit. 65 | ``` 66 | 67 | Options with `[MS]` are not supported & ignored for single-sig. 68 | Options with `[SS]` are not supported & ignored for multi-sig. 69 | 70 | ## Examples 71 | 72 | ```sh 73 | $ export XPUB=tpubD6NzVbkrYhZ4Xp6tGusznF6KMdYHy1JSCdDk3XVLDuAA7EgJKghA5J1FP4pDXb4sCypJjAYPB4uTTXkVo2iWzK8BsMaccXTNyShDx3gxagi 74 | 75 | $ psbt_faker foo.psbt $XPUB -s -a p2wsh --fee 15000000 -n 300000000 -o 1 -c 0 76 | 77 | Fake PSBT would send 3 BTC to: 78 | 2.85000000 => bc1qppvspp5ahvjg28rv90644857c5df3mwr7ypcy7a093n90prg992qtjkkgv 79 | 0.15000000 => miners fee 80 | 81 | 82 | $ psbt_faker foo.psbt $XPUB -o 10 -n 300000000 83 | 84 | Fake PSBT would send 3 BTC to: 85 | 0.29999900 => 12YjQWLgh1TAtSzSS1BHKQaCrhEd2Cypv4 (change back) 86 | 0.29999900 => 136QZHT1icbGUkQNcAv4CLFp6Gfoxf9ixN 87 | 0.29999900 => 1M667r3frAjiLMWucHS14MBcgtmAjYL6fi 88 | 0.29999900 => 17puZckh3RzNaac3XmX2JYFaF4wNrNu1ng 89 | 0.29999900 => 1HbtSJcovvkTpCuKq7UPVfYyTg7G8wAdAk 90 | 0.29999900 => 1CYN8P1vPyfCscsUbWy2nRU4tLccewtBVQ 91 | 0.29999900 => 1KhMtnJGSk9pRN2DrGgEzdEZUs8w1H4zna 92 | 0.29999900 => 1Dx6uFvs2jY4xA4o9g36UFwSqrSGsUzxhD 93 | 0.29999900 => 1CvBjipyE9Vbdi8AJw345YMbhvq7TbTN7u 94 | 0.29999900 => 1AW1Z4oseyWj6ib2CkwYY9eEBS5mkvgymN 95 | 0.00001000 => miners fee 96 | 97 | 98 | $ psbt_faker foo.psbt $XPUB -n 10000000000 -o 13 -c 10 99 | 100 | Fake PSBT would send 100 BTC to: 101 | 7.69230692 => 12YjQWLgh1TAtSzSS1BHKQaCrhEd2Cypv4 (change back) 102 | 7.69230692 => 1Pqtjz6c6fg3pduEGmnzUbBbZ8JzgERtR5 (change back) 103 | 7.69230692 => 1DsDJeZwmwsU9TY2vEmsVMWMfDrtGHHc2T (change back) 104 | 7.69230692 => 18W8nXPVKwUFovKpTffLbL3uik3x4Qf6TX (change back) 105 | 7.69230692 => 1qNozdz8fFMn7LVszM77WYwoJowUtKyaY (change back) 106 | 7.69230692 => 1AtvwyUV634pGG46wyuZkZ5WhK7UCrhdgf (change back) 107 | 7.69230692 => 1ABABZuHK2VF5w8pHQE23878adHYXSPWz3 (change back) 108 | 7.69230692 => 1GSfhvFLj75Xz7cHsLMMaLhuLMoYaUna4B (change back) 109 | 7.69230692 => 16fBJHM7z91JxSnbrcHsDYTviespdouXUT (change back) 110 | 7.69230692 => 1errmuAqcQNMW42XG1p2G7RqX2uBqeF8F (change back) 111 | 7.69230692 => 15Kd3GBpTqbS6rzMMKHDPnSAktUoKDRqgc 112 | 7.69230692 => 15wErNoy7QSgovSHWUPWUt73fAs3bq98gN 113 | 7.69230692 => 1NkpHeWJ1dXeYQP8CWp6NuVLkTsZpNKQjx 114 | 0.00001000 => miners fee 115 | 116 | 117 | psbt_faker foo.psbt $XPUB -o 10 -a p2wpkh -a p2wsh -a p2sh -a p2pkh -a p2wsh-p2sh -a p2wpkh-p2sh 118 | 119 | Fake PSBT would send 0.001 BTC to: 120 | 0.00009900 => bc1qzruxkvnknt2xmqu9y5pr09n4ewhtm89w6mfelv (change back) 121 | 0.00009900 => bc1qc6yc0dmeu7tshepwsa7q8gwmxsa64gv0u476kqgdlruvndqe7nmsqh6krs 122 | 0.00009900 => 37UA1NpD2XNyLcn1eQXAFjJn3SFssXS84V 123 | 0.00009900 => 1A3okvZp3wGF2XZNqRhd8AAR23KH7rxt8W 124 | 0.00009900 => 34U4wbXXDsgn7Msr3Z1dgRybqLgJ2uN3qL 125 | 0.00009900 => 35FWGXE75wiedtsUe873qmKNZqJyCruCEf 126 | 0.00009900 => bc1q8qwl4vyj2avfa95st5zc5yj28kq4t874f0qkfk 127 | 0.00009900 => bc1qsxjshmg4zn6mul23gq2wk868qpm3f3tcmaqvr7zkz2xf6vwvl3vsqszsl6 128 | 0.00009900 => 3G4GWg8v9mCQA9rFVncgyYPZqRSuQmhs7o 129 | 0.00009900 => 1Fwnq5tgepfYytk4n6cHcAjA44fXB7AYMz 130 | 0.00001000 => miners fee 131 | 132 | 133 | # how much BTC is send is regulated by -n/--input-amount and -i/--num-ins 134 | # by default all inputs have size of 100k SATS 135 | # below: 3 inputs each sending 1 million SATS 136 | psbt_faker foo.psbt $XPUB -i 3 -n 1000000 137 | 138 | Fake PSBT would send 0.03 BTC to: 139 | 0.01499500 => 12YjQWLgh1TAtSzSS1BHKQaCrhEd2Cypv4 (change back) 140 | 0.01499500 => 1Cadzk6VAJaQasRnAxgoC43DUoDcq6dGua 141 | 0.00001000 => miners fee 142 | 143 | 144 | # fetches actual block height from mempool.space 145 | psbt_faker foo.psbt $XPUB -i 3 -n 10000000 -s -w --locktime current 146 | 147 | Fake PSBT would send 0.3 BTC to: 148 | 0.14999500 => 3GcLByjaiNtTriQx2pSiU1sJoENFfKiUaf (change back) 149 | 0.14999500 => 32VSUWdkJDGEJSuKd1oRUoowy22ThyA7LB 150 | 0.00001000 => miners fee 151 | 152 | PSBT to be signed: foo.psbt 153 | 154 | 155 | psbt_faker foo.psbt -o 10 -a p2wpkh -a p2wsh -a p2sh -a p2pkh -a p2wsh-p2sh -a p2wpkh-p2sh --multisig ms-example-segwit.txt -c 3 156 | 157 | Fake PSBT would send 0.001 BTC to: 158 | 0.00009900 => bc1qme4du64p8q3l8aedn83vdh4exe7a8mxelsdcwvcx67hgyd9jfqeshx863n (change back) 159 | 0.00009900 => bc1qc6qdln78rw8xhfc847v8jk4qdzx2pepvux3wrx403jmzwumqvwfq5st3vk (change back) 160 | 0.00009900 => bc1qngfqnl7p6pkrmpyz7ttcqt6mx3phq4c7dm23f2dvgvczmkfajjzq98cjpm (change back) 161 | 0.00009900 => bc1qpgdcenn3yecd0p28gk3guh4f2w4l4xrfas83z3 162 | 0.00009900 => bc1q6tvdfcn0emctdg3vvpx2kn40msan34glku9pm7tsn8557kzjyqzstuekem 163 | 0.00009900 => 3CC1pUNnrMqp7gG2GfPDZe4JmjrJEqpmnk 164 | 0.00009900 => 1AjT1kjfcbS8aMRQc27FwzknqeqzHzM7Vs 165 | 0.00009900 => 36w5DNqWSR3vuKNrZsLF42SnFfjkEMMHv2 166 | 0.00009900 => 3ETZ6Cp9Fsdrh5pkB9q9ay17E5JELJvma4 167 | 0.00009900 => bc1qwse4nh9ful5ww95j7ej8jw562tas6j8aqa6qd6 168 | 0.00001000 => miners fee 169 | 170 | 171 | psbt_faker foo.psbt -i 3 -o 5 -c 3 -n 1000000000 --multisig ms-example-segwit.txt --incl-xpubs 172 | 173 | Fake PSBT would send 30 BTC to: 174 | 5.99999800 => bc1qngfqnl7p6pkrmpyz7ttcqt6mx3phq4c7dm23f2dvgvczmkfajjzq98cjpm (change back) 175 | 5.99999800 => bc1qlt8yharuphh08l5trw96kz8w4jts2t45zwvafq72ma2rqgfktdvqcfa5xq (change back) 176 | 5.99999800 => bc1qnytf7s8crz35lwk6822kqdajdlr30n2tl43jusdx0q2q26gfneyssc3lc5 (change back) 177 | 5.99999800 => bc1qrtz4gvmk453zplt78c264hkl3333f8xxcg2nq8cgvkrnucwgxzjqm05kge 178 | 5.99999800 => bc1qep2a66uh3kz20qk4vgr6yw8rezyzppszd3fyqzmmmt8xqamg66zqvjkg2h 179 | 0.00001000 => miners fee 180 | 181 | 182 | # PSBT version 2 183 | psbt_faker foo.psbt -i 3 -o 5 -c 3 -n 1000000000 --multisig ms-example-segwit.txt --psbt2 -a p2pkh 184 | 185 | Fake PSBT would send 30 BTC to: 186 | 5.99999800 => bc1qngfqnl7p6pkrmpyz7ttcqt6mx3phq4c7dm23f2dvgvczmkfajjzq98cjpm (change back) 187 | 5.99999800 => bc1qlt8yharuphh08l5trw96kz8w4jts2t45zwvafq72ma2rqgfktdvqcfa5xq (change back) 188 | 5.99999800 => bc1qnytf7s8crz35lwk6822kqdajdlr30n2tl43jusdx0q2q26gfneyssc3lc5 (change back) 189 | 5.99999800 => 1HRPDRJ9tVSpE2gsn2qfphJbXGneDsqiDA 190 | 5.99999800 => 1CW14Y5ZzjHSxCidm2wWBRwgeHmzsFNbQM 191 | 0.00001000 => miners fee 192 | 193 | 194 | psbt_faker foo.psbt $XPUB -i 3 -n 100000000 --multisig ms-example.txt 195 | 196 | Fake PSBT would send 3 BTC to: 197 | 1.49999500 => 3JeauQqiGXd5znAMums9KSpsXe8UhpS1tf (change back) 198 | 1.49999500 => 3Q7NmnYDxh4yyFuT152SQbLtFsn19EJVED 199 | 0.00001000 => miners fee 200 | 201 | 202 | psbt_faker foo.psbt $XPUB -i 3 -o 10 -n 100000000 --multisig ms-example.txt --locktime 899000 203 | 204 | Fake PSBT would send 3 BTC to: 205 | 0.29999900 => 3JeauQqiGXd5znAMums9KSpsXe8UhpS1tf (change back) 206 | 0.29999900 => 3NVNTQVFNwLEN5qBK7292cbSeQhY8DCDCm 207 | 0.29999900 => 3K4UA17iU9UXhULNa3yC5B3WB8qq6XE5hp 208 | 0.29999900 => 37kfug24cD6AhZhukJrDPRU5sxwAvAYwU6 209 | 0.29999900 => 38imUgvwSBJbo4CeUBQMFX7TeeKNsBnvdK 210 | 0.29999900 => 3CAzLxDv3fefoPQCDWkiuuwmsnSUBFzD4w 211 | 0.29999900 => 39SjZMdSfVAf5b2hBm1VmhegWYYHUnTnpn 212 | 0.29999900 => 38hJkV67aF6mX2Q6GGepGV8JModVs4k4VL 213 | 0.29999900 => 3L8uS7WF2K1Qbp3s8321zdZEpHJRo2KB2Z 214 | 0.29999900 => 3Euxvk1HcejZBc8VySTHp6icgieP4m2k7s 215 | 0.00001000 => miners fee 216 | 217 | 218 | # extended private key can be used instead of extended public key XPUB for signle-sig PSBTs 219 | # proper BIP-44 derivation path from master used in that case 220 | XPRV=tprv8ZgxMBicQKsPeXJHL3vPPgTAEqQ5P2FD9qDeCQT4Cp1EMY5QkwMPWFxHdxHrxZhhcVRJ2m7BNWTz9Xre68y7mX5vCdMJ5qXMUfnrZ2si2X4 221 | 222 | psbt_faker foo.psbt $XPRV -i 2 -o 2 -n 50000000 --locktime 899000 -s -w -6 223 | 224 | Fake PSBT would send 1 BTC to: 225 | 0.49999500 => 36q7XpzinU7hM7eDaF37fBKV4sz73MPsfq (change back) 226 | 0.49999500 => 38q4ecMQt33o6HP1kh1dZJ6CdRcUUAdftd 227 | 0.00001000 => miners fee 228 | 229 | 230 | # or use extended public key with key origin info to have "deeper" derivations in PSBT 231 | # no validation is run against the xpub 232 | XPUB='[0F056943/84h/1h/0h]tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r' 233 | 234 | psbt_faker foo.psbt $XPUB -i 3 -o 3 -c 2 -n 50000000 --locktime current -s -6 235 | 236 | Fake PSBT would send 1.5 BTC to: 237 | 0.49999666 => bc1qupyd58ndsh7lut0et0vtrq432jvu9jtdwgtkgk (change back) 238 | 0.49999666 => bc1qceytj4vfrg22cy7mp5mnfps4ffgseas20ak7fj (change back) 239 | 0.49999666 => bc1qj55nlp4ntq35sklzgq34pr0ujz2muuws5nrvrg 240 | 0.00001000 => miners fee 241 | ``` 242 | -------------------------------------------------------------------------------- /psbt_faker/txn.py: -------------------------------------------------------------------------------- 1 | # 2 | # Creating fake transactions. Not simple... but only for testing purposes, so .... 3 | # 4 | import struct, random, hashlib 5 | from io import BytesIO 6 | from .segwit_addr import encode as bech32_encode 7 | from .psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput 8 | from .base58 import encode_base58_checksum 9 | from .helpers import str2path, hash160 10 | from .serialize import uint256_from_str 11 | from .bip32 import BIP32Node 12 | from .ctransaction import CTransaction, CTxIn, CTxOut, COutPoint 13 | 14 | # all possible addr types, including multisig/scripts 15 | # single-signer 16 | ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh', 'p2sh-p2wpkh', 'p2tr'] 17 | # multi-signer 18 | ADDR_STYLES_MULTI = ['p2wsh', 'p2sh', 'p2sh-p2wsh', 'p2wsh-p2sh'] 19 | 20 | ADDR_STYLES = ADDR_STYLES_MULTI + ADDR_STYLES_SINGLE 21 | 22 | def prandom(count): 23 | # make some bytes, randomly, but not: deterministic 24 | return bytes(random.randint(0, 255) for i in range(count)) 25 | 26 | def fake_dest_addr(style='p2pkh'): 27 | # Make a plausible output address, but it's random garbage. Cant use for change outs 28 | 29 | # See CTxOut.get_address() in ../shared/serializations 30 | 31 | if style == 'p2wpkh': 32 | return bytes([0, 20]) + prandom(20) 33 | 34 | if style == 'p2wsh': 35 | return bytes([0, 32]) + prandom(32) 36 | 37 | if style == 'p2tr': 38 | # OP_1 = int(81) 39 | return bytes([81, 32]) + prandom(32) 40 | 41 | if style in ['p2sh', 'p2wsh-p2sh', 'p2wpkh-p2sh', 'p2sh-p2wsh', 'p2sh-p2wpkh']: 42 | # all equally bogus P2SH outputs 43 | return bytes([0xa9, 0x14]) + prandom(20) + bytes([0x87]) 44 | 45 | if style == 'p2pkh': 46 | return bytes([0x76, 0xa9, 0x14]) + prandom(20) + bytes([0x88, 0xac]) 47 | 48 | # missing: if style == 'p2pk' => pay to pubkey, considered obsolete 49 | 50 | raise ValueError('not supported: ' + style) 51 | 52 | def make_change_addr(master_xfp, orig_der, account_key, idx, style): 53 | # provide script, pubkey and xpath for a legit-looking change output 54 | 55 | redeem_scr, actual_scr = None, None 56 | 57 | if orig_der: 58 | path = str2path(master_xfp, f"{orig_der}/0/{idx}") 59 | else: 60 | path = str2path(master_xfp, f"0/{idx}") 61 | 62 | dest = account_key.subkey_for_path(f"0/{idx}") 63 | 64 | target = dest.hash160() 65 | assert len(target) == 20 66 | 67 | is_segwit = False 68 | if style == 'p2pkh': 69 | redeem_scr = bytes([0x76, 0xa9, 0x14]) + target + bytes([0x88, 0xac]) 70 | elif style == 'p2wpkh': 71 | redeem_scr = bytes([0, 20]) + target 72 | is_segwit = True 73 | elif style in ('p2wpkh-p2sh', 'p2sh-p2wpkh'): 74 | redeem_scr = bytes([0, 20]) + target 75 | actual_scr = bytes([0xa9, 0x14]) + hash160(redeem_scr) + bytes([0x87]) 76 | else: 77 | raise ValueError('cant make fake change output of type: ' + style) 78 | 79 | return redeem_scr, actual_scr, is_segwit, dest.sec(), path 80 | 81 | 82 | def fake_txn(input_amounts, num_outs, master_xpub=None, fee=10000, 83 | outvals=None, segwit_in=False, wrapped=False, outstyles=None, 84 | change_outputs=[], op_return=None, psbt_v2=None, 85 | locktime=0, sequences=None, is_testnet=False, partial=False): 86 | 87 | num_ins = len(input_amounts) 88 | 89 | af = ("p2sh-p2wpkh" if wrapped else "p2wpkh") if segwit_in else "p2pkh" 90 | 91 | # we have a key; use it to provide "plausible" value inputs 92 | orig_der = None 93 | if master_xpub: 94 | master_xpub = master_xpub.strip() # annoying whitespaces 95 | key_orig_info = None 96 | close_idx = master_xpub.find("]") 97 | if master_xpub[0] == "[" and (close_idx != -1): 98 | # key has origin derivation public - parse 99 | key_orig_info = master_xpub[1:close_idx] 100 | master_xpub = master_xpub[close_idx + 1:] 101 | 102 | account_key = BIP32Node.from_wallet_key(master_xpub) 103 | if key_orig_info: 104 | split_der = key_orig_info.split("/", 1) 105 | if len(split_der) == 1: 106 | str_xfp = split_der[0] 107 | orig_der = None 108 | else: 109 | str_xfp, orig_der = split_der 110 | 111 | xfp = bytes.fromhex(str_xfp) 112 | 113 | else: 114 | xfp = account_key.fingerprint() 115 | try: 116 | assert account_key.privkey() is not None 117 | # user provided extended private key, we can simulate proper hardened derivations 118 | if af == "p2wpkh": 119 | purpose = 84 120 | elif af == "p2sh-p2wpkh": 121 | purpose = 49 122 | else: 123 | purpose = 44 124 | 125 | orig_der = f"{purpose}h/{int(is_testnet)}h/0h" 126 | account_key = account_key.subkey_for_path(orig_der) 127 | except: pass 128 | else: 129 | # special value for COLDCARD: zero xfp => anyone can try to sign 130 | account_key = BIP32Node.from_master_secret(b'1' * 32) 131 | xfp = bytes(4) 132 | 133 | psbt = BasicPSBT() 134 | 135 | if psbt_v2: 136 | psbt.version = 2 137 | psbt.txn_version = 2 138 | psbt.input_count = num_ins 139 | psbt.output_count = num_outs 140 | psbt.fallback_locktime = locktime 141 | 142 | txn = CTransaction() 143 | txn.nVersion = 2 144 | txn.nLockTime = locktime 145 | 146 | psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)] 147 | psbt.outputs = [BasicPSBTOutput(idx=i) for i in range(num_outs)] 148 | 149 | outputs = [] 150 | 151 | for i in range(num_ins): 152 | # make a fake txn to supply each of the inputs 153 | # addr where the fake money will be stored. 154 | # always from internal address chain 155 | subder = f"1/{i}" 156 | subkey = account_key.subkey_for_path(subder) 157 | sec = subkey.sec() 158 | assert len(sec) == 33, "expect compressed" 159 | 160 | if partial and (i == 0): 161 | psbt.inputs[i].bip32_paths[sec] = b'Nope' + struct.pack(' 310 | 311 | data = [] 312 | for cosigner_idx, (xfp, str_path, node) in enumerate(keys): 313 | sp = f"{int(is_change)}/{idx}" 314 | n = node.subkey_for_path(sp) 315 | pk = n.sec() 316 | data.append((pk, str2path(xfp, str_path + "/" + sp))) 317 | 318 | if bip67: 319 | data.sort(key=lambda i: i[0]) 320 | 321 | mm = [80 + M] if M <= 16 else [1, M] 322 | nn = [80 + N] if N <= 16 else [1, N] 323 | 324 | rv = bytes(mm) 325 | 326 | for pk, _ in data: 327 | rv += bytes([len(pk)]) + pk 328 | 329 | rv += bytes(nn + [0xAE]) 330 | 331 | return rv, data 332 | 333 | def make_ms_address(M, keys, idx, is_change, addr_fmt="p2wsh", testnet=1, bip67=True): 334 | # Construct addr and script need to represent a p2sh address 335 | script, bip32paths = make_redeem(M, keys, idx, is_change, bip67=bip67) 336 | 337 | if addr_fmt == "p2wsh": 338 | # testnet=2 --> regtest 339 | hrp = ['bc', 'tb', 'bcrt'][testnet] 340 | data = hashlib.sha256(script).digest() 341 | addr = bech32_encode(hrp, 0, data) 342 | scriptPubKey = bytes([0x0, 0x20]) + data 343 | else: 344 | if addr_fmt == "p2sh": 345 | digest = hash160(script) 346 | elif addr_fmt in ("p2sh-p2wsh", "p2wsh-p2sh"): 347 | digest = hash160(b'\x00\x20' + hashlib.sha256(script).digest()) 348 | else: 349 | raise ValueError(addr_fmt) 350 | 351 | prefix = bytes([196]) if testnet else bytes([5]) 352 | addr = encode_base58_checksum(prefix + digest) 353 | 354 | scriptPubKey = bytes([0xa9, 0x14]) + digest + bytes([0x87]) 355 | 356 | return addr, scriptPubKey, script, bip32paths 357 | 358 | 359 | def fake_ms_txn(input_amounts, num_outs, M, keys, fee=10000, outvals=None, 360 | outstyles=['p2wsh'], change_outputs=[], incl_xpubs=False, 361 | bip67=True, locktime=0, psbt_v2=False, 362 | sequences=None, is_testnet=False, change_af=None): 363 | num_ins = len(input_amounts) 364 | 365 | # make various size MULTISIG txn's ... completely fake and pointless values 366 | # - but has UTXO's to match needs 367 | # spending change outputs 368 | psbt = BasicPSBT() 369 | 370 | if psbt_v2: 371 | psbt.version = 2 372 | psbt.txn_version = 2 373 | psbt.input_count = num_ins 374 | psbt.output_count = num_outs 375 | psbt.fallback_locktime = locktime 376 | 377 | txn = CTransaction() 378 | txn.nVersion = 2 379 | txn.nLockTime = locktime 380 | 381 | if incl_xpubs: 382 | # add global header with XPUB's 383 | for idx, (xfp, str_path, node) in enumerate(keys): 384 | kk = str2path(xfp, str_path) 385 | psbt.xpubs.append((node.node.serialize_public(), kk)) 386 | 387 | psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)] 388 | psbt.outputs = [BasicPSBTOutput(idx=i) for i in range(num_outs)] 389 | 390 | for i in range(num_ins): 391 | # make a fake txn to supply each of the inputs 392 | # - each input is 1BTC 393 | 394 | # addr where the fake money will be stored. 395 | # always same address format as config defines 396 | addr, scriptPubKey, script, details = make_ms_address(M, keys, i, True, 397 | addr_fmt=change_af, 398 | bip67=bip67) 399 | if "p2wsh" in change_af: 400 | psbt.inputs[i].witness_script = script 401 | 402 | if "p2sh" in change_af: 403 | psbt.inputs[i].redeem_script = script 404 | 405 | for pubkey, xfp_path in details: 406 | psbt.inputs[i].bip32_paths[pubkey] = xfp_path 407 | 408 | # UTXO that provides the funding for to-be-signed txn 409 | supply = CTransaction() 410 | supply.nVersion = 2 411 | out_point = COutPoint( 412 | uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, 0)), 413 | 73 414 | ) 415 | supply.vin = [CTxIn(out_point, nSequence=0xffffffff)] 416 | 417 | supply.vout.append(CTxOut(int(input_amounts[i]), scriptPubKey)) 418 | 419 | if "wsh" in change_af: 420 | psbt.inputs[i].utxo = supply.serialize_with_witness() 421 | else: 422 | psbt.inputs[i].witness_utxo = supply.vout[-1].serialize() 423 | 424 | supply.calc_sha256() 425 | 426 | seq = None 427 | if sequences: 428 | # custom provided properly encoded sequence numbers 429 | try: 430 | seq = sequences[i] 431 | except: pass 432 | 433 | if seq is None: 434 | seq = 0xffffffff 435 | if locktime and (i == 0): 436 | # decrement one sequence to enable nLockTime enforcement 437 | # only on 0th input 438 | seq = 0xfffffffe 439 | 440 | if psbt_v2: 441 | psbt.inputs[i].previous_txid = supply.hash 442 | psbt.inputs[i].prevout_idx = 0 443 | psbt.inputs[i].sequence = seq 444 | if locktime: 445 | # no need to do this as fallback locktime is already set in globals but yolo 446 | if locktime < 500000000: 447 | psbt.inputs[i].req_height_locktime = locktime 448 | else: 449 | psbt.inputs[i].req_time_locktime = locktime 450 | 451 | spendable = CTxIn(COutPoint(supply.sha256, 0), nSequence=seq) 452 | txn.vin.append(spendable) 453 | 454 | outputs = [] 455 | for i in range(num_outs): 456 | if i in change_outputs: 457 | # change outputs are always same as multisig address format 458 | addr, scriptPubKey, scr, details = make_ms_address(M, keys, num_ins+i, False, 459 | addr_fmt=change_af, bip67=bip67) 460 | 461 | for pubkey, xfp_path in details: 462 | psbt.outputs[i].bip32_paths[pubkey] = xfp_path 463 | 464 | if 'w' in change_af: 465 | psbt.outputs[i].witness_script = scr 466 | if change_af.endswith('p2sh'): 467 | psbt.outputs[i].redeem_script = b'\0\x20' + hashlib.sha256(scr).digest() 468 | elif change_af.endswith('sh'): 469 | psbt.outputs[i].redeem_script = scr 470 | else: 471 | if not outstyles: 472 | # make same outstyles as instyles 473 | style = change_af 474 | elif len(outstyles) == 1: 475 | style = outstyles[0] 476 | elif len(outstyles) == num_outs: 477 | style = outstyles[i-len(change_outputs)] 478 | else: 479 | style = outstyles[(i-len(change_outputs)) % len(outstyles)] 480 | 481 | scriptPubKey = fake_dest_addr(style) 482 | 483 | assert scriptPubKey 484 | 485 | if psbt_v2: 486 | psbt.outputs[i].script = scriptPubKey 487 | if outvals: 488 | psbt.outputs[i].amount = outvals[i] 489 | else: 490 | psbt.outputs[i].amount = int(round((sum(input_amounts) - fee) / num_outs, 4)) 491 | 492 | 493 | if not outvals: 494 | h = CTxOut(int(round((sum(input_amounts)-fee) / num_outs, 4)), scriptPubKey) 495 | else: 496 | h = CTxOut(int(outvals[i]), scriptPubKey) 497 | 498 | txn.vout.append(h) 499 | 500 | outputs.append((h.nValue, scriptPubKey, (i in change_outputs))) 501 | 502 | if not psbt_v2: 503 | psbt.txn = txn.serialize_with_witness() 504 | 505 | rv = BytesIO() 506 | psbt.serialize(rv) 507 | 508 | return rv.getvalue(), [(n, render_address(s, is_testnet), ic) for n,s,ic in outputs] 509 | 510 | def render_address(script, testnet=True): 511 | # take a scriptPubKey (part of the TxOut) and convert into conventional human-readable 512 | # string... aka: the "payment address" 513 | from .segwit_addr import encode as bech32_encode 514 | 515 | ll = len(script) 516 | 517 | if not testnet: 518 | bech32_hrp = 'bc' 519 | b58_addr = bytes([0]) 520 | b58_script = bytes([5]) 521 | else: 522 | bech32_hrp = 'tb' 523 | b58_addr = bytes([111]) 524 | b58_script = bytes([196]) 525 | 526 | # P2PKH 527 | if ll == 25 and script[0:3] == b'\x76\xA9\x14' and script[23:26] == b'\x88\xAC': 528 | return encode_base58_checksum(b58_addr + script[3:3+20]) 529 | 530 | # P2SH 531 | if ll == 23 and script[0:2] == b'\xA9\x14' and script[22] == 0x87: 532 | return encode_base58_checksum(b58_script + script[2:2+20]) 533 | 534 | # segwit v0 (P2WPKH, P2WSH) 535 | if script[0] == 0 and script[1] in (0x14, 0x20) and (ll - 2) == script[1]: 536 | return bech32_encode(bech32_hrp, script[0], script[2:]) 537 | 538 | # segwit v1 (P2TR) and later segwit version OP_1 .. OP_16 539 | if ll == 34 and (81 <= script[0] <= 96) and script[1] == 0x20: 540 | return bech32_encode(bech32_hrp, script[0] - 80, script[2:]) 541 | 542 | raise ValueError('Unknown payment script', repr(script)) 543 | 544 | # EOF 545 | -------------------------------------------------------------------------------- /psbt_faker/psbt.py: -------------------------------------------------------------------------------- 1 | # (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. 2 | # 3 | # psbt.py - yet another PSBT parser/serializer but used only for test cases. 4 | # 5 | import io, struct 6 | from binascii import b2a_hex as _b2a_hex 7 | from binascii import a2b_hex 8 | from base64 import b64decode, b64encode 9 | from .serialize import ser_compact_size, deser_compact_size 10 | from .ctransaction import CTransaction, CTxOut, CTxIn, COutPoint, uint256_from_str, ser_uint256 11 | 12 | b2a_hex = lambda a: str(_b2a_hex(a), 'ascii') 13 | 14 | # BIP-174 aka PSBT defined values 15 | # 16 | # GLOBAL === 17 | PSBT_GLOBAL_UNSIGNED_TX = 0x00 18 | PSBT_GLOBAL_XPUB = 0x01 19 | PSBT_GLOBAL_VERSION = 0xfb 20 | PSBT_GLOBAL_PROPRIETARY = 0xfc 21 | 22 | # BIP-370 23 | PSBT_GLOBAL_TX_VERSION = 0x02 24 | PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03 25 | PSBT_GLOBAL_INPUT_COUNT = 0x04 26 | PSBT_GLOBAL_OUTPUT_COUNT = 0x05 27 | PSBT_GLOBAL_TX_MODIFIABLE = 0x06 28 | 29 | # INPUTS === 30 | PSBT_IN_NON_WITNESS_UTXO = 0x00 31 | PSBT_IN_WITNESS_UTXO = 0x01 32 | PSBT_IN_PARTIAL_SIG = 0x02 33 | PSBT_IN_SIGHASH_TYPE = 0x03 34 | PSBT_IN_REDEEM_SCRIPT = 0x04 35 | PSBT_IN_WITNESS_SCRIPT = 0x05 36 | PSBT_IN_BIP32_DERIVATION = 0x06 37 | PSBT_IN_FINAL_SCRIPTSIG = 0x07 38 | PSBT_IN_FINAL_SCRIPTWITNESS = 0x08 39 | PSBT_IN_POR_COMMITMENT = 0x09 # Proof of Reserves 40 | PSBT_IN_RIPEMD160 = 0x0a 41 | PSBT_IN_SHA256 = 0x0b 42 | PSBT_IN_HASH160 = 0x0c 43 | PSBT_IN_HASH256 = 0x0d 44 | # BIP-370 45 | PSBT_IN_PREVIOUS_TXID = 0x0e 46 | PSBT_IN_OUTPUT_INDEX = 0x0f 47 | PSBT_IN_SEQUENCE = 0x10 48 | PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11 49 | PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12 50 | # BIP-371 51 | PSBT_IN_TAP_KEY_SIG = 0x13 52 | PSBT_IN_TAP_SCRIPT_SIG = 0x14 53 | PSBT_IN_TAP_LEAF_SCRIPT = 0x15 54 | PSBT_IN_TAP_BIP32_DERIVATION = 0x16 55 | PSBT_IN_TAP_INTERNAL_KEY = 0x17 56 | PSBT_IN_TAP_MERKLE_ROOT = 0x18 57 | 58 | # OUTPUTS === 59 | PSBT_OUT_REDEEM_SCRIPT = 0x00 60 | PSBT_OUT_WITNESS_SCRIPT = 0x01 61 | PSBT_OUT_BIP32_DERIVATION = 0x02 62 | # BIP-370 63 | PSBT_OUT_AMOUNT = 0x03 64 | PSBT_OUT_SCRIPT = 0x04 65 | # BIP-371 66 | PSBT_OUT_TAP_INTERNAL_KEY = 0x05 67 | PSBT_OUT_TAP_TREE = 0x06 68 | PSBT_OUT_TAP_BIP32_DERIVATION = 0x07 69 | 70 | PSBT_PROP_CK_ID = b"COINKITE" 71 | 72 | 73 | def ser_prop_key(identifier, subtype, keydata=b''): 74 | # arg types are: bytes, int (< 256), bytes 75 | key = b"" 76 | key += ser_compact_size(len(identifier)) 77 | key += identifier 78 | key += ser_compact_size(subtype) 79 | key += keydata 80 | return key 81 | 82 | 83 | class PSBTSection: 84 | 85 | def __init__(self, fd=None, idx=None): 86 | self.defaults() 87 | self.my_index = idx 88 | 89 | if not fd: return 90 | 91 | while 1: 92 | ks = deser_compact_size(fd) 93 | if ks is None: break 94 | if ks == 0: break 95 | 96 | key = fd.read(ks) 97 | vs = deser_compact_size(fd) 98 | val = fd.read(vs) 99 | 100 | kt = key[0] 101 | self.parse_kv(kt, key[1:], val) 102 | 103 | def serialize(self, fd, v2): 104 | 105 | def wr(ktype, val, key=b''): 106 | fd.write(ser_compact_size(1 + len(key))) 107 | fd.write(bytes([ktype]) + key) 108 | fd.write(ser_compact_size(len(val))) 109 | fd.write(val) 110 | 111 | self.serialize_kvs(wr, v2) 112 | 113 | fd.write(b'\0') 114 | 115 | 116 | class BasicPSBTInput(PSBTSection): 117 | def defaults(self): 118 | self.utxo = None 119 | self.witness_utxo = None 120 | self.part_sigs = {} 121 | self.sighash = None 122 | self.bip32_paths = {} 123 | self.taproot_bip32_paths = {} 124 | self.taproot_internal_key = None 125 | self.taproot_key_sig = None 126 | self.redeem_script = None 127 | self.witness_script = None 128 | self.previous_txid = None # v2 129 | self.prevout_idx = None # v2 130 | self.sequence = None # v2 131 | self.req_time_locktime = None # v2 132 | self.req_height_locktime = None # v2 133 | self.others = {} 134 | self.unknown = {} 135 | 136 | def __eq__(a, b): 137 | if a.sighash != b.sighash: 138 | # no sighash == SIGHASH_ALL 139 | if {a.sighash, b.sighash} != {None, 1}: 140 | return False 141 | 142 | rv = a.utxo == b.utxo and \ 143 | a.witness_utxo == b.witness_utxo and \ 144 | a.redeem_script == b.redeem_script and \ 145 | a.witness_script == b.witness_script and \ 146 | a.my_index == b.my_index and \ 147 | a.bip32_paths == b.bip32_paths and \ 148 | a.taproot_key_sig == b.taproot_key_sig and \ 149 | a.taproot_bip32_paths == b.taproot_bip32_paths and \ 150 | a.taproot_internal_key == b.taproot_internal_key and \ 151 | sorted(a.part_sigs.keys()) == sorted(b.part_sigs.keys()) and \ 152 | a.previous_txid == b.previous_txid and \ 153 | a.prevout_idx == b.prevout_idx and \ 154 | a.sequence == b.sequence and \ 155 | a.req_time_locktime == b.req_time_locktime and \ 156 | a.req_height_locktime == b.req_height_locktime and \ 157 | a.unknown == b.unknown 158 | if rv: 159 | # NOTE: equality test on signatures requires parsing DER stupidness 160 | # and some maybe understanding of R/S values on curve that I don't have. 161 | assert all(a.part_sigs[k] == b.part_sigs[k] for k in a.part_sigs) 162 | return rv 163 | 164 | def parse_kv(self, kt, key, val): 165 | if kt == PSBT_IN_NON_WITNESS_UTXO: 166 | self.utxo = val 167 | assert not key 168 | elif kt == PSBT_IN_WITNESS_UTXO: 169 | self.witness_utxo = val 170 | assert not key 171 | elif kt == PSBT_IN_PARTIAL_SIG: 172 | self.part_sigs[key] = val 173 | elif kt == PSBT_IN_SIGHASH_TYPE: 174 | assert len(val) == 4 175 | self.sighash = struct.unpack(" val=(path) 406 | # ignore PSBT_GLOBAL_XPUB on 0th index (should not be part of parsed key) 407 | self.xpubs.append((key[1:], val)) 408 | elif kt == PSBT_GLOBAL_VERSION: 409 | self.version = struct.unpack(" int: 33 | """ 34 | Big endian representation to integer. 35 | 36 | :param b: big endian representation 37 | :return: integer 38 | """ 39 | return int.from_bytes(b, "big") 40 | 41 | 42 | def int_to_big_endian(n: int, length: int) -> bytes: 43 | """ 44 | Represents integer in big endian byteorder. 45 | 46 | :param n: integer 47 | :param length: byte length 48 | :return: big endian 49 | """ 50 | return n.to_bytes(length, "big") 51 | 52 | 53 | def hmac_sha512(key: bytes, msg: bytes) -> bytes: 54 | """ 55 | Hash-based message authentication code with sha512 56 | 57 | :param key: secret key 58 | :param msg: message 59 | :return: digest bytes 60 | """ 61 | return hmac.new(key=key, msg=msg, digestmod=hashlib.sha512).digest() 62 | 63 | 64 | class PrivateKey(object): 65 | 66 | __slots__ = ( 67 | "k", 68 | "K" 69 | ) 70 | 71 | def __init__(self, sec_exp: Union[bytes, int]): 72 | """ 73 | Initializes private key. 74 | 75 | :param sec_exp: secret 76 | """ 77 | if isinstance(sec_exp, int): 78 | sec_exp = int_to_big_endian(sec_exp, 32) 79 | try: 80 | ec_seckey_verify(sec_exp) 81 | self.k = sec_exp 82 | self.K = PublicKey(ec_pubkey_create(self.k)) 83 | except NameError: 84 | k = ecdsa.SigningKey.from_string(sec_exp, curve=SECP256k1) 85 | self.K = PublicKey(pub_key=k.get_verifying_key()) 86 | self.k = k.to_string() 87 | else: 88 | ec_seckey_verify(self.k) 89 | self.K = PublicKey(ec_pubkey_create(self.k)) 90 | 91 | def __bytes__(self) -> bytes: 92 | """ 93 | Encodes private key into corresponding byte sequence. 94 | 95 | :return: byte representation of PrivateKey object 96 | """ 97 | return self.k 98 | 99 | def __eq__(self, other: "PrivateKey") -> bool: 100 | """ 101 | Checks whether two private keys are equal. 102 | 103 | :param other: other private key 104 | """ 105 | return self.k == other.k 106 | 107 | def wif(self, compressed: bool = True, testnet: bool = False) -> str: 108 | """ 109 | Encodes private key into wallet import/export format. 110 | 111 | :param compressed: whether public key is compressed (default=True) 112 | :param testnet: whether to encode as a testnet key (default=False) 113 | :return: WIF encoded private key 114 | """ 115 | prefix = b"\xef" if testnet else b"\x80" 116 | suffix = b"\x01" if compressed else b"" 117 | return encode_base58_checksum(prefix + bytes(self) + suffix) 118 | 119 | def tweak_add(self, tweak32: bytes) -> "PrivateKey": 120 | tweaked = ec_seckey_tweak_add(self.k, tweak32) 121 | return PrivateKey(sec_exp=tweaked) 122 | 123 | @classmethod 124 | def from_wif(cls, wif_str: str) -> "PrivateKey": 125 | """ 126 | Initializes private key from wallet import format encoding. 127 | 128 | :param wif_str: wallet import format private key 129 | :return: private key 130 | """ 131 | decoded = decode_base58_checksum(s=wif_str) 132 | if wif_str[0] in ("K", "L", "c"): 133 | # compressed key --> so remove last byte that has to be 01 134 | assert decoded[-1] == 1 135 | decoded = decoded[:-1] 136 | return cls(sec_exp=decoded[1:]) 137 | 138 | @classmethod 139 | def parse(cls, key_bytes: bytes) -> "PrivateKey": 140 | """ 141 | Initializes private key from byte sequence. 142 | 143 | :param key_bytes: byte representation of private key 144 | :return: private key 145 | """ 146 | return cls(sec_exp=key_bytes) 147 | 148 | @classmethod 149 | def from_int(cls, sec_exp: int) -> "Privatekey": 150 | return cls(sec_exp=int_to_big_endian(sec_exp, 32)) 151 | 152 | 153 | class PublicKey(object): 154 | 155 | __slots__ = ( 156 | "K" 157 | ) 158 | 159 | def __init__(self, pub_key): 160 | """ 161 | Initializes PublicKey object. 162 | 163 | :param key: secp256k1 pubkey or ecdsa.VerifyingKey 164 | """ 165 | self.K = pub_key 166 | 167 | def __eq__(self, other: "PublicKey") -> bool: 168 | """ 169 | Checks whether two public keys are equal. 170 | 171 | :param other: other public key 172 | """ 173 | return self.sec() == other.sec() 174 | 175 | @property 176 | def point(self): # -> ecdsa.ellipticcurve.Point: 177 | """ 178 | Point on curve (x and y coordinates). 179 | 180 | :return: point on curve 181 | """ 182 | return self.K.pubkey.point 183 | 184 | def sec(self, compressed: bool = True) -> bytes: 185 | """ 186 | Encodes public key to SEC format. 187 | 188 | :param compressed: whether to use compressed format (default=True) 189 | :return: SEC encoded public key 190 | """ 191 | try: 192 | return ec_pubkey_serialize(self.K, compressed=compressed) 193 | except NameError: 194 | return self.K.to_string(encoding="compressed" if compressed else "uncompressed") 195 | 196 | def tweak_add(self, tweak32: bytes) -> "PublicKey": 197 | return PublicKey(pub_key=ec_pubkey_tweak_add(self.K, tweak32)) 198 | 199 | @classmethod 200 | def parse(cls, key_bytes: bytes) -> "PublicKey": 201 | """ 202 | Initializes public key from byte sequence. 203 | 204 | :param key_bytes: byte representation of public key 205 | :return: public key 206 | """ 207 | try: 208 | return cls(pub_key=ec_pubkey_parse(key_bytes)) 209 | except NameError: 210 | return cls(ecdsa.VerifyingKey.from_string(key_bytes, curve=SECP256k1)) 211 | 212 | @classmethod 213 | def from_point(cls, point) -> "PublicKey": 214 | """ 215 | Initializes public key from point on elliptic curve. 216 | 217 | :param point: point on elliptic curve 218 | :return: public key 219 | """ 220 | return cls(ecdsa.VerifyingKey.from_public_point(point, curve=SECP256k1)) 221 | 222 | def h160(self, compressed: bool = True) -> bytes: 223 | """ 224 | SHA256 followed by RIPEMD160 of public key. 225 | 226 | :param compressed: whether to use compressed format (default=True) 227 | :return: SHA256(RIPEMD160(public key)) 228 | """ 229 | return hash160(self.sec(compressed=compressed)) 230 | 231 | def address(self, compressed: bool = True, testnet: bool = False, 232 | addr_fmt: str = "p2wpkh") -> str: 233 | """ 234 | Generates bitcoin address from public key. 235 | 236 | :param compressed: whether to use compressed format (default=True) 237 | :param testnet: whether to encode as a testnet address (default=False) 238 | :param addr_type: which address type to generate: 239 | 1. p2pkh 240 | 2. p2sh-p2wpkh 241 | 3. p2wpkh (default) 242 | :return: bitcoin address 243 | """ 244 | h160 = self.h160(compressed=compressed) 245 | if addr_fmt == "p2pkh": 246 | prefix = b"\x6f" if testnet else b"\x00" 247 | return encode_base58_checksum(prefix + h160) 248 | elif addr_fmt == "p2wpkh": 249 | hrp = "tb" if testnet else "bc" 250 | return bech32_encode(hrp=hrp, witver=0, witprog=h160) 251 | elif addr_fmt == "p2sh-p2wpkh": 252 | scr = b"\x00\x14" + h160 # witversion 0 + pubkey hash 253 | h160 = hash160(scr) 254 | prefix = b"\xc4" if testnet else b"\x05" 255 | return encode_base58_checksum(prefix + h160) 256 | 257 | raise ValueError("Unsupported address type.") 258 | 259 | 260 | class PubKeyNode(object): 261 | 262 | mark: str = "M" 263 | testnet_version: int = 0x043587CF 264 | mainnet_version: int = 0x0488B21E 265 | 266 | __slots__ = ( 267 | "parent", 268 | "key", 269 | "chain_code", 270 | "depth", 271 | "index", 272 | "parsed_parent_fingerprint", 273 | "parsed_version", 274 | "testnet", 275 | "children" 276 | ) 277 | 278 | def __init__(self, key: bytes, chain_code: bytes, index: int = 0, 279 | depth: int = 0, testnet: bool = False, 280 | parent: Union["PubKeyNode", "PrvKeyNode"] = None, 281 | parent_fingerprint: bytes = None): 282 | """ 283 | Initializes Pub/PrvKeyNode. 284 | 285 | :param key: public or private key 286 | :param chain_code: chain code 287 | :param index: current node derivation index (default=0) 288 | :param depth: current node depth (default=0) 289 | :param testnet: whether this node is testnet node (default=False) 290 | :param parent: parent node of the current node (default=None) 291 | :param parent_fingerprint: fingerprint of parent node (default=None) 292 | """ 293 | self.parent = parent 294 | self.key = key 295 | self.chain_code = chain_code 296 | self.depth = depth 297 | self.index = index 298 | self.parsed_parent_fingerprint = parent_fingerprint 299 | self.parsed_version = None 300 | self.testnet = testnet 301 | 302 | def __eq__(self, other) -> bool: 303 | """ 304 | Checks whether two private/public key nodes are equal. 305 | 306 | :param other: other private/public key node 307 | """ 308 | if type(self) != type(other): 309 | return False 310 | self_key = big_endian_to_int(self.key) 311 | other_key = big_endian_to_int(other.key) 312 | return self_key == other_key and \ 313 | self.chain_code == other.chain_code and \ 314 | self.depth == other.depth and \ 315 | self.index == other.index and \ 316 | self.testnet == other.testnet and \ 317 | self.parent_fingerprint == other.parent_fingerprint 318 | 319 | @property 320 | def public_key(self) -> PublicKey: 321 | """ 322 | Public key node's public key. 323 | 324 | :return: public key of public key node 325 | """ 326 | return PublicKey.parse(key_bytes=self.key) 327 | 328 | @property 329 | def parent_fingerprint(self) -> bytes: 330 | """ 331 | Gets parent fingerprint. 332 | 333 | If node is parsed from extended key, only parsed parent fingerprint 334 | is available. If node is derived, parent fingerprint is calculated 335 | from parent node. 336 | 337 | :return: parent fingerprint 338 | """ 339 | if self.parent: 340 | fingerprint = self.parent.fingerprint() 341 | else: 342 | fingerprint = self.parsed_parent_fingerprint 343 | # in case there is still None here - it is master 344 | return fingerprint or b"\x00\x00\x00\x00" 345 | 346 | @property 347 | def pub_version(self) -> int: 348 | """ 349 | Decides which extended public key version integer to use 350 | based on testnet parameter. 351 | 352 | :return: extended public key version 353 | """ 354 | if self.testnet: 355 | return PubKeyNode.testnet_version 356 | return PubKeyNode.mainnet_version 357 | 358 | def __repr__(self) -> str: 359 | if self.is_master() or self.is_root(): 360 | return self.mark 361 | if self.is_hardened(): 362 | index = str(self.index - 2**31) + "'" 363 | else: 364 | index = str(self.index) 365 | parent = str(self.parent) if self.parent else self.mark 366 | return parent + "/" + index 367 | 368 | def is_hardened(self) -> bool: 369 | """Check whether current key node is hardened.""" 370 | return self.index >= 2**31 371 | 372 | def is_master(self) -> bool: 373 | """Check whether current key node is master node.""" 374 | return self.depth == 0 and self.index == 0 and self.parent is None 375 | 376 | def is_root(self) -> bool: 377 | """Check whether current key node is root (has no parent).""" 378 | return self.parent is None 379 | 380 | def fingerprint(self) -> bytes: 381 | """ 382 | Gets current node fingerprint. 383 | 384 | :return: first four bytes of SHA256(RIPEMD160(public key)) 385 | """ 386 | return hash160(self.public_key.sec())[:4] 387 | 388 | @classmethod 389 | def parse(cls, s: Union[str, bytes, BytesIO], 390 | testnet: bool = False) -> Prv_or_PubKeyNode: 391 | """ 392 | Initializes private/public key node from serialized node or 393 | extended key. 394 | 395 | :param s: serialized node or extended key 396 | :param testnet: whether this node is testnet node 397 | :return: public/private key node 398 | """ 399 | if isinstance(s, str): 400 | s = BytesIO(decode_base58_checksum(s=s)) 401 | elif isinstance(s, bytes): 402 | s = BytesIO(s) 403 | elif isinstance(s, BytesIO): 404 | pass 405 | else: 406 | raise ValueError("has to be bytes, str or BytesIO") 407 | return cls._parse(s, testnet=testnet) 408 | 409 | @classmethod 410 | def _parse(cls, s: BytesIO, testnet: bool = False) -> Prv_or_PubKeyNode: 411 | """ 412 | Initializes private/public key node from serialized node buffer. 413 | 414 | :param s: serialized node buffer 415 | :param testnet: whether this node is testnet node (default=False) 416 | :return: public/private key node 417 | """ 418 | version = big_endian_to_int(s.read(4)) 419 | depth = big_endian_to_int(s.read(1)) 420 | parent_fingerprint = s.read(4) 421 | index = big_endian_to_int(s.read(4)) 422 | chain_code = s.read(32) 423 | key_bytes = s.read(33) 424 | key = cls( 425 | key=key_bytes, 426 | chain_code=chain_code, 427 | index=index, 428 | depth=depth, 429 | testnet=testnet, 430 | parent_fingerprint=parent_fingerprint, 431 | ) 432 | key.parsed_version = version 433 | return key 434 | 435 | def _serialize(self, key: bytes, version: int = None) -> bytes: 436 | """ 437 | Serializes public/private key node to extended key format. 438 | 439 | :param version: extended public/private key version (default=None) 440 | :return: serialized extended public/private key node 441 | """ 442 | # 4 byte: version bytes 443 | result = int_to_big_endian(version, 4) 444 | # 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 derived keys 445 | result += int_to_big_endian(self.depth, 1) 446 | # 4 bytes: the fingerprint of the parent key (0x00000000 if master key) 447 | if self.is_master(): 448 | result += int_to_big_endian(0x00000000, 4) 449 | else: 450 | result += self.parent_fingerprint 451 | # 4 bytes: child number. This is ser32(i) for i in xi = xpar/i, 452 | # with xi the key being serialized. (0x00000000 if master key) 453 | result += int_to_big_endian(self.index, 4) 454 | # 32 bytes: the chain code 455 | result += self.chain_code 456 | # 33 bytes: the public key or private key data 457 | # (serP(K) for public keys, 0x00 || ser256(k) for private keys) 458 | result += key 459 | return result 460 | 461 | def serialize_public(self, version: int = None) -> bytes: 462 | """ 463 | Serializes public key node to extended key format. 464 | 465 | :param version: extended public key version (default=None) 466 | :return: serialized extended public key node 467 | """ 468 | return self._serialize( 469 | version=self.pub_version if version is None else version, 470 | key=self.public_key.sec() 471 | ) 472 | 473 | def extended_public_key(self, version: int = None) -> str: 474 | """ 475 | Base58 encodes serialized public key node. If version is not 476 | provided (default) it is determined by result of self.pub_version. 477 | 478 | :param version: extended public key version (default=None) 479 | :return: extended public key 480 | """ 481 | return encode_base58_checksum(self.serialize_public(version=version)) 482 | 483 | def ckd(self, index: int) -> "PubKeyNode": 484 | """ 485 | The function CKDpub((Kpar, cpar), i) → (Ki, ci) computes a child 486 | extended public key from the parent extended public key. 487 | It is only defined for non-hardened child keys. 488 | 489 | * Check whether i ≥ 231 (whether the child is a hardened key). 490 | * If so (hardened child): 491 | return failure 492 | * If not (normal child): 493 | let I = HMAC-SHA512(Key=cpar, Data=serP(Kpar) || ser32(i)). 494 | * Split I into two 32-byte sequences, IL and IR. 495 | * The returned child key Ki is point(parse256(IL)) + Kpar. 496 | * The returned chain code ci is IR. 497 | * In case parse256(IL) ≥ n or Ki is the point at infinity, 498 | the resulting key is invalid, and one should proceed with the next 499 | value for i. 500 | 501 | :param index: derivation index 502 | :return: derived child 503 | """ 504 | if index >= HARDENED: 505 | raise RuntimeError("failure: hardened child for public ckd") 506 | I = hmac_sha512( 507 | key=self.chain_code, 508 | msg=self.key + int_to_big_endian(index, 4) 509 | ) 510 | IL, IR = I[:32], I[32:] 511 | # TODO this does not check whether IL is not zero (secp256k1 also does not check) 512 | try: 513 | Ki = self.public_key.tweak_add(IL) 514 | except NameError: 515 | if big_endian_to_int(IL) >= CURVE_ORDER: 516 | InvalidKeyError( 517 | "public key {} is greater/equal to curve order".format( 518 | big_endian_to_int(IL) 519 | ) 520 | ) 521 | point = PrivateKey.parse(IL).K.point + self.public_key.point 522 | if point == INFINITY: 523 | raise InvalidKeyError("public key is a point at infinity") 524 | Ki = PublicKey.from_point(point=point) 525 | 526 | child = self.__class__( 527 | key=Ki.sec(), 528 | chain_code=IR, 529 | index=index, 530 | depth=self.depth + 1, 531 | testnet=self.testnet, 532 | parent=self 533 | ) 534 | return child 535 | 536 | 537 | class PrvKeyNode(PubKeyNode): 538 | 539 | mark: str = "m" 540 | testnet_version: int = 0x04358394 541 | mainnet_version: int = 0x0488ADE4 542 | 543 | @property 544 | def private_key(self) -> PrivateKey: 545 | """ 546 | Private key node's private key. 547 | 548 | :return: public key of private key node 549 | """ 550 | if len(self.key) == 33 and self.key[0] == 0: 551 | return PrivateKey(self.key[1:]) 552 | return PrivateKey(self.key) 553 | 554 | @property 555 | def public_key(self) -> PublicKey: 556 | """ 557 | Private key node's public key. 558 | 559 | :return: public key of public key node 560 | """ 561 | return self.private_key.K 562 | 563 | @property 564 | def prv_version(self) -> int: 565 | """ 566 | Decides which extended private key version integer to use 567 | based on testnet parameter. 568 | 569 | :return: extended private key version 570 | """ 571 | if self.testnet: 572 | return PrvKeyNode.testnet_version 573 | return PrvKeyNode.mainnet_version 574 | 575 | @classmethod 576 | def master_key(cls, bip39_seed: bytes, testnet=False) -> "PrvKeyNode": 577 | """ 578 | Generates master private key node from bip39 seed. 579 | 580 | * Generate a seed byte sequence S (bip39_seed arg) of a chosen length 581 | (between 128 and 512 bits; 256 bits is advised) from a (P)RNG. 582 | * Calculate I = HMAC-SHA512(Key = "Bitcoin seed", Data = S) 583 | * Split I into two 32-byte sequences, IL and IR. 584 | * Use parse256(IL) as master secret key, and IR as master chain code. 585 | 586 | :param bip39_seed: bip39_seed 587 | :param testnet: whether this node is testnet node (default=False) 588 | :return: master private key node 589 | """ 590 | I = hmac_sha512(key=b"Bitcoin seed", msg=bip39_seed) 591 | # private key 592 | IL = I[:32] 593 | # In case IL is 0 or ≥ n, the master key is invalid 594 | int_left_key = big_endian_to_int(IL) 595 | if int_left_key == 0: 596 | raise InvalidKeyError("master key is zero") 597 | try: 598 | ec_seckey_verify(IL) 599 | except NameError: 600 | if int_left_key >= CURVE_ORDER: 601 | raise InvalidKeyError( 602 | "master key {} is greater/equal to curve order".format( 603 | int_left_key 604 | ) 605 | ) 606 | # chain code 607 | IR = I[32:] 608 | return cls( 609 | key=IL, 610 | chain_code=IR, 611 | testnet=testnet 612 | ) 613 | 614 | def serialize_private(self, version: int = None) -> bytes: 615 | """ 616 | Serializes private key node to extended key format. 617 | 618 | :param version: extended private key version (default=None) 619 | :return: serialized extended private key node 620 | """ 621 | return self._serialize( 622 | version=self.prv_version if version is None else version, 623 | key=b"\x00" + bytes(self.private_key) 624 | ) 625 | 626 | def extended_private_key(self, version: int = None) -> str: 627 | """ 628 | Base58 encodes serialized private key node. If version is not 629 | provided (default) it is determined by result of self.prv_version. 630 | 631 | :param version: extended private key version (default=None) 632 | :return: extended private key 633 | """ 634 | return encode_base58_checksum(self.serialize_private(version=version)) 635 | 636 | def ckd(self, index: int) -> "PrvKeyNode": 637 | """ 638 | The function CKDpriv((kpar, cpar), i) → (ki, ci) computes 639 | a child extended private key from the parent extended private key: 640 | 641 | * Check whether i ≥ 2**31 (whether the child is a hardened key). 642 | * If so (hardened child): 643 | let I = HMAC-SHA512(Key=cpar, Data=0x00 || ser256(kpar) || ser32(i)) 644 | (Note: The 0x00 pads the private key to make it 33 bytes long.) 645 | * If not (normal child): 646 | let I = HMAC-SHA512(Key=cpar, Data=serP(point(kpar)) || ser32(i)) 647 | * Split I into two 32-byte sequences, IL and IR. 648 | * The returned child key ki is parse256(IL) + kpar (mod n). 649 | * The returned chain code ci is IR. 650 | * In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, 651 | and one should proceed with the next value for i. 652 | (Note: this has probability lower than 1 in 2**127.) 653 | 654 | :param index: derivation index 655 | :return: derived child 656 | """ 657 | if index >= HARDENED: 658 | # hardened 659 | data = b"\x00"+bytes(self.private_key) + int_to_big_endian(index, 4) 660 | else: 661 | data = self.public_key.sec() + int_to_big_endian(index, 4) 662 | I = hmac_sha512(key=self.chain_code, msg=data) 663 | IL, IR = I[:32], I[32:] 664 | try: 665 | ki = self.private_key.tweak_add(IL) 666 | # if ki == PrivateKey.from_int(0): 667 | # InvalidKeyError("private key is zero") 668 | except NameError: 669 | if big_endian_to_int(IL) >= CURVE_ORDER: 670 | InvalidKeyError( 671 | "private key {} is greater/equal to curve order".format( 672 | big_endian_to_int(IL) 673 | ) 674 | ) 675 | ki = (int.from_bytes(IL, "big") + 676 | big_endian_to_int(bytes(self.private_key))) % CURVE_ORDER 677 | if ki == 0: 678 | InvalidKeyError("private key is zero") 679 | ki = int_to_big_endian(ki, 32) 680 | 681 | child = self.__class__( 682 | key=bytes(ki), 683 | chain_code=IR, 684 | index=index, 685 | depth=self.depth + 1, 686 | testnet=self.testnet, 687 | parent=self 688 | ) 689 | return child 690 | 691 | 692 | class BIP32Node: 693 | def __init__(self, node, netcode="XTN"): 694 | self.node = node 695 | self._netcode = netcode 696 | 697 | @classmethod 698 | def from_master_secret(cls, bip39_seed: bytes, netcode="XTN"): 699 | return cls(PrvKeyNode.master_key(bip39_seed, False if netcode == "BTC" else True), 700 | netcode=netcode) 701 | 702 | @classmethod 703 | def from_hwif(cls, extended_key): 704 | assert extended_key[0] in "xt" 705 | testnet = extended_key[0] == "t" 706 | if extended_key[1:4] == "prv": 707 | ek = PrvKeyNode.parse(extended_key, testnet) 708 | else: 709 | ek = PubKeyNode.parse(extended_key, testnet) 710 | return cls(ek, netcode="XTN" if testnet else "BTC") 711 | 712 | def subkey_for_path(self, path): 713 | path_list = list(str2ipath(path)) 714 | node = self.node 715 | for idx in path_list: 716 | node = node.ckd(idx) 717 | return BIP32Node(node) 718 | 719 | def hwif(self, as_private=False): 720 | is_pub = type(self.node) is PubKeyNode 721 | if is_pub and as_private: 722 | raise ValueError("no private key") 723 | if as_private: 724 | return self.node.extended_private_key() 725 | return self.node.extended_public_key() 726 | 727 | @classmethod 728 | def from_wallet_key(cls, extended_key): 729 | return cls.from_hwif(extended_key) 730 | 731 | def hash160(self, compressed=True): 732 | return self.node.public_key.h160(compressed) 733 | 734 | def address(self, compressed=True, netcode="XTN", addr_fmt="p2pkh"): 735 | return self.node.public_key.address(compressed, addr_fmt=addr_fmt, 736 | testnet=False if netcode == "BTC" else True) 737 | 738 | def sec(self, compressed=True): 739 | return self.node.public_key.sec(compressed) 740 | 741 | def fingerprint(self): 742 | return self.node.fingerprint() 743 | 744 | def netcode(self): 745 | return self._netcode 746 | 747 | def chain_code(self): 748 | return self.node.chain_code 749 | 750 | def privkey(self): 751 | assert isinstance(self.node, PrvKeyNode) 752 | return bytes(self.node.private_key) 753 | 754 | def parent_fingerprint(self): 755 | return self.node.parent_fingerprint 756 | --------------------------------------------------------------------------------