Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hwilib/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.1.0"
__version__ = "3.2.0-rc.1"
92 changes: 92 additions & 0 deletions hwilib/psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ class PartiallySignedInput:
PSBT_IN_TAP_BIP32_DERIVATION = 0x16
PSBT_IN_TAP_INTERNAL_KEY = 0x17
PSBT_IN_TAP_MERKLE_ROOT = 0x18
PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a
PSBT_IN_MUSIG2_PUB_NONCE = 0x1b
PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c

def __init__(self, version: int) -> None:
self.non_witness_utxo: Optional[CTransaction] = None
Expand All @@ -125,6 +128,9 @@ def __init__(self, version: int) -> None:
self.tap_bip32_paths: Dict[bytes, Tuple[Set[bytes], KeyOriginInfo]] = {}
self.tap_internal_key = b""
self.tap_merkle_root = b""
self.musig2_participant_pubkeys: Dict[bytes, List[bytes]] = {}
self.musig2_pub_nonces: Dict[Tuple[bytes, bytes, Optional[bytes]], bytes] = {}
self.musig2_partial_sigs: Dict[Tuple[bytes, bytes, Optional[bytes]], bytes] = {}
self.unknown: Dict[bytes, bytes] = {}

self.version: int = version
Expand Down Expand Up @@ -153,6 +159,9 @@ def set_null(self) -> None:
self.sequence = None
self.time_locktime = None
self.height_locktime = None
self.musig2_participant_pubkeys.clear()
self.musig2_pub_nonces.clear()
self.musig2_partial_sigs.clear()
self.unknown.clear()

def deserialize(self, f: Readable) -> None:
Expand Down Expand Up @@ -351,6 +360,51 @@ def deserialize(self, f: Readable) -> None:
self.tap_merkle_root = deser_string(f)
if len(self.tap_merkle_root) != 32:
raise PSBTSerializationError("Input Taproot merkle root is not 32 bytes")
elif key_type == PartiallySignedInput.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS:
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, input Musig2 participant pubkeys already provided")
elif len(key) != 1 + 33:
raise PSBTSerializationError("Input Musig2 aggregate compressed pubkey is not 33 bytes")

pubkeys_cat = deser_string(f)
if len(pubkeys_cat) == 0:
raise PSBTSerializationError("The list of compressed pubkeys for Musig2 cannot be empty")
if (len(pubkeys_cat) % 33) != 0:
raise PSBTSerializationError("The compressed pubkeys for Musig2 must be exactly 33 bytes long")
pubkeys = []
for i in range(0, len(pubkeys_cat), 33):
pubkeys.append(pubkeys_cat[i: i + 33])

self.musig2_participant_pubkeys[key[1:]] = pubkeys
elif key_type == PartiallySignedInput.PSBT_IN_MUSIG2_PUB_NONCE:
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, Musig2 public nonce already provided")
elif len(key) not in [1 + 33 + 33, 1 + 33 + 33 + 32]:
raise PSBTSerializationError("Invalid key length for Musig2 public nonce")

providing_pubkey = key[1:1 + 33]
aggregate_pubkey = key[1 + 33:1 + 33 + 33]
tapleaf_hash = None if len(key) == 1 + 33 + 33 else key[1 + 33 + 33:]

public_nonces = deser_string(f)
if len(public_nonces) != 66:
raise PSBTSerializationError("The length of the public nonces in Musig2 must be exactly 66 bytes")

self.musig2_pub_nonces[(providing_pubkey, aggregate_pubkey, tapleaf_hash)] = public_nonces
elif key_type == PartiallySignedInput.PSBT_IN_MUSIG2_PARTIAL_SIG:
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, Musig2 partial signature already provided")
elif len(key) not in [1 + 33 + 33, 1 + 33 + 33 + 32]:
raise PSBTSerializationError("Invalid key length for Musig2 partial signature")

providing_pubkey = key[1:1 + 33]
aggregate_pubkey = key[1 + 33:1 + 33 + 33]
tapleaf_hash = None if len(key) == 1 + 33 + 33 else key[1 + 33 + 33:]

partial_sig = deser_string(f)
if len(partial_sig) != 32:
raise PSBTSerializationError("The length of the partial signature in Musig2 must be exactly 32 bytes")
self.musig2_partial_sigs[(providing_pubkey, aggregate_pubkey, tapleaf_hash)] = partial_sig
else:
if key in self.unknown:
raise PSBTSerializationError("Duplicate key, key for unknown value already provided")
Expand Down Expand Up @@ -441,6 +495,20 @@ def serialize(self) -> bytes:
witstack = self.final_script_witness.serialize()
r += ser_string(witstack)

for pk, pubkeys in self.musig2_participant_pubkeys.items():
r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS) + pk)
r += ser_string(b''.join(pubkeys))

for (pk, aggpk, tapleaf_hash), pubnonce in self.musig2_pub_nonces.items():
key_value = pk + aggpk + (tapleaf_hash or b'')
r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_MUSIG2_PUB_NONCE) + key_value)
r += ser_string(pubnonce)

for (pk, aggpk, tapleaf_hash), partial_sig in self.musig2_partial_sigs.items():
key_value = pk + aggpk + (tapleaf_hash or b'')
r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_MUSIG2_PARTIAL_SIG) + key_value)
r += ser_string(partial_sig)

if self.version >= 2:
if len(self.prev_txid) != 0:
r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_PREVIOUS_TXID))
Expand Down Expand Up @@ -483,6 +551,7 @@ class PartiallySignedOutput:
PSBT_OUT_TAP_INTERNAL_KEY = 0x05
PSBT_OUT_TAP_TREE = 0x06
PSBT_OUT_TAP_BIP32_DERIVATION = 0x07
PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08

def __init__(self, version: int) -> None:
self.redeem_script = b""
Expand All @@ -493,6 +562,7 @@ def __init__(self, version: int) -> None:
self.tap_internal_key = b""
self.tap_tree = b""
self.tap_bip32_paths: Dict[bytes, Tuple[Set[bytes], KeyOriginInfo]] = {}
self.musig2_participant_pubkeys: Dict[bytes, List[bytes]] = {}
self.unknown: Dict[bytes, bytes] = {}

self.version: int = version
Expand All @@ -509,6 +579,7 @@ def set_null(self) -> None:
self.tap_bip32_paths.clear()
self.amount = None
self.script = b""
self.musig2_participant_pubkeys = {}
self.unknown.clear()

def deserialize(self, f: Readable) -> None:
Expand Down Expand Up @@ -589,6 +660,22 @@ def deserialize(self, f: Readable) -> None:
for i in range(0, num_hashes):
leaf_hashes.add(vs.read(32))
self.tap_bip32_paths[xonly] = (leaf_hashes, KeyOriginInfo.deserialize(vs.read()))
elif key_type == PartiallySignedOutput.PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS:
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, output Musig2 participant pubkeys already provided")
elif len(key) != 1 + 33:
raise PSBTSerializationError("Output Musig2 aggregate compressed pubkey is not 33 bytes")

pubkeys_cat = deser_string(f)
if len(pubkeys_cat) == 0:
raise PSBTSerializationError("The list of compressed pubkeys for Musig2 cannot be empty")
if (len(pubkeys_cat) % 33) != 0:
raise PSBTSerializationError("The compressed pubkeys for Musig2 must be exactly 33 bytes long")
pubkeys = []
for i in range(0, len(pubkeys_cat), 33):
pubkeys.append(pubkeys_cat[i: i + 33])

self.musig2_participant_pubkeys[key[1:]] = pubkeys
else:
if key in self.unknown:
raise PSBTSerializationError("Duplicate key, key for unknown value already provided")
Expand Down Expand Up @@ -646,6 +733,11 @@ def serialize(self) -> bytes:
value += origin.serialize()
r += ser_string(value)

for pk, pubkeys in self.musig2_participant_pubkeys.items():
r += ser_string(ser_compact_size(
PartiallySignedOutput.PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS) + pk)
r += ser_string(b''.join(pubkeys))

for key, value in sorted(self.unknown.items()):
r += ser_string(key)
r += ser_string(value)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "hwi"
version = "3.1.0"
version = "3.2.0-rc.1"
description = "A library for working with Bitcoin hardware wallets"
authors = ["Ava Chow <me@achow101.com>"]
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@
'typing-extensions>=4.4,<5.0']

extras_require = \
{':python_version >= "3.6" and python_version < "3.7"': ['dataclasses>=0.8,<0.9'],
{':python_version == "3.6"': ['dataclasses>=0.8,<0.9'],
'qt:python_version < "3.10"': ['pyside2>=5.14.0,<6.0.0']}

entry_points = \
{'console_scripts': ['hwi = hwilib._cli:main', 'hwi-qt = hwilib._gui:main']}

setup_kwargs = {
'name': 'hwi',
'version': '3.1.0',
'version': '3.2.0rc1',
'description': 'A library for working with Bitcoin hardware wallets',
'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI)\n[![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependencies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path are known, issue commands to it like so:\n\n```\n./hwi.py -t <type> -d <path> <command> <command args>\n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Documentation\n\nDocumentation for HWI can be found on [readthedocs.io](https://hwi.readthedocs.io/).\n\n### Device Support\n\nFor documentation on devices supported and how they are supported, please check the [device support page](https://hwi.readthedocs.io/en/latest/devices/index.html#support-matrix)\n\n### Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](https://hwi.readthedocs.io/en/latest/examples/bitcoin-core-usage.html).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n",
'author': 'Ava Chow',
Expand Down
Loading
Loading