Source code for btclib_ecc.exceptions

# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.

"""Exception classes.

These exist only to tell an exception raised by btclib_ecc from one
raised by any other code: each derives from the built-in that says what
kind of failure it is, and adds nothing to it.

`BTClibEccException` is what makes that telling apart a single
`except` rather than a tuple of three a caller has to keep in step with
this hierarchy. It is inherited *beside* the built-in and not instead of
it, which is the half that matters: `BTClibEccValueError` is a
`ValueError`, so code catching the built-in catches it, and
`json.JSONDecodeError` is the standard library doing the same.

It is caught and never raised: every raise below is one of the three,
and which one answers a question the base cannot carry -- whether the
value was wrong, the type was, or neither was and a check failed anyway.
A caller with something to do about that difference names the specific
class; `except BTClibEccException` is for the caller who only needs
to know it came from here.

The two classes carrying a field hand every constructor argument to
`BaseException.__init__` and compose their message in `__str__`, which
is what `subprocess.CalledProcessError` and `UnicodeDecodeError` do, and
what makes them picklable: `BaseException.__reduce__` returns `(cls,
self.args)`, so a class whose `args` is the composed message alone is
rebuilt by calling it with one argument, and one argument is not what it
takes. That is a TypeError out of `pickle`, out of `copy.copy` and out
of `copy.deepcopy` -- and out of a `ProcessPoolExecutor`, which cannot
send the exception back and reports a broken pool instead of the failure
the worker died of.
"""

from __future__ import annotations

from typing_extensions import override

__all__ = [
    "BTClibEccException",
    "BTClibEccRuntimeError",
    "BTClibEccTypeError",
    "BTClibEccValueError",
    "BorromeanRingError",
    "InvalidContributionError",
]


[docs] class BTClibEccException(Exception): # noqa: N818 -- a kind, like Exception itself, not a leaf raised """Anything btclib_ecc raised, whatever kind of failure it is. The one name to catch for a caller who handles the standard library's exceptions anyway and needs to know which came from here. Never raised: the three below it are, and each says which kind of failure it was. """
[docs] class BTClibEccValueError(BTClibEccException, ValueError): """A value no valid input could carry; the package's usual refusal."""
[docs] class BTClibEccTypeError(BTClibEccException, TypeError): """An input of a type no conversion accepts: a caller error."""
[docs] class BTClibEccRuntimeError(BTClibEccException, RuntimeError): """A check that failed on valid inputs, e.g. a failed verification."""
[docs] class InvalidContributionError(BTClibEccRuntimeError): """A party to an interactive protocol sent a value that does not check out. Which party, and which of its contributions: `signer` is the index in the list the caller passed, None for the aggregator -- who has no index, having no key -- and `contrib` names what was wrong, one of "pubkey", "pubnonce", "aggnonce", "aggothernonce", "psig" or "adaptor". That is the whole point of the class: a multi-round protocol that merely fails leaves every participant a suspect, and the answer a caller needs is who to hold accountable and to exclude from the next attempt. A BTClibEccRuntimeError and not a BTClibEccValueError, which is the other obvious base and the one BIP327 keeps separate: its reference implementation raises ValueError for an argument that breaks a precondition -- the caller's own mistake, a 33-byte tweak -- and this for a peer misbehaving, and the MuSig2 test vectors distinguish the two case by case. Sharing a base would put the two beyond telling apart by `except`, and would let every `except ValueError` in the package swallow an accusation. """ def __init__(self, signer: int | None, contrib: str) -> None: self.signer = signer self.contrib = contrib super().__init__(signer, contrib) @override def __str__(self) -> str: who = "the aggregator" if self.signer is None else f"signer {self.signer}" return f"invalid {self.contrib} from {who}"
[docs] class BorromeanRingError(BTClibEccRuntimeError): """A borromean ring signature check failed, and where names it. `ring` is the index into `pubk_rings` and `position` the index within that ring: an e-value landing on zero, and the point at infinity its one-in-n neighbour lands a ring's nonce or its `r` on instead, each happen at one ring and one position, and `btclib_ecc.ecc.borromean.sign` and `assert_as_valid` already have both in hand at every one of their raises. Naming them is what tells a caller building on this primitive which key rejected the signature rather than only that the whole thing did. Both are None for the one failure with no ring of its own: the final `e0` not matching what every ring converges on is a property of the whole signature, not of any single ring in it. A BTClibEccRuntimeError and not `InvalidContributionError`: that class names a party to an interactive multi-round protocol -- MuSig2 -- and which of its contributions was wrong, where a borromean ring signature is not interactive and has no parties to accuse, only positions in a signature that either close their ring or do not. A BTClibEccRuntimeError still, so code catching that keeps catching this, as `verify` already does. """ def __init__(self, message: str, ring: int | None, position: int | None) -> None: self.ring = ring self.position = position super().__init__(message, ring, position) @override def __str__(self) -> str: if self.ring is None: return str(self.args[0]) return f"{self.args[0]} (ring {self.ring}, position {self.position})"