Skip to content

decode raises ValueError, ReadError or UnicodeDecodeError instead of Bolt11Exception #58

Open

Description

@Nuhiat-Arefin

decode raises ValueError, ReadError or UnicodeDecodeError instead of a Bolt11Exception subclass on an x, c or f field with data_length 0, truncated data or a non-UTF-8 d field. This happens on main at 51bd3b97, the v2.2.0 tag, and in release 2.1.1.

The test at tests/test_decode_exceptions.py builds the first two inputs with the example key and embeds the last three:

import pytest
from bitstring import Bits

from bolt11 import Bolt11, Fallback, Tags, decode, encode
from bolt11.exceptions import Bolt11Exception

# BOLT 11 example private key, from 11-payment-encoding.md.
PRIVATE_KEY = "e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734"


def _signed_with(extra):
    """A minimal invoice signed with the example key, plus one extra field."""
    invoice = Bolt11(
        currency="bc",
        amount_msat=250_000_000,
        date=1496314658,
        tags=Tags.from_dict(
            {
                "payment_hash": (
                    "0001020304050607080900010203040506070809000102030405060708090102"
                ),
                "payment_secret": (
                    "1111111111111111111111111111111111111111111111111111111111111111"
                ),
                "description": "1 cup coffee",
                **extra,
            }
        ),
    )
    return encode(invoice, PRIVATE_KEY)


# encode writes data_length 0 for an expiry of 0 and for an empty fallback.
X_DATA_LENGTH_0 = _signed_with({"expire_time": 0})
F_DATA_LENGTH_0 = _signed_with({"fallback": Fallback(Bits(), "bc")})

# Byte layouts encode cannot produce, embedded as literals.
SIGNATURE_ALONE = (
    "lntb9678785340p1qqqqqqq0edrv5l46sr6mcupz93w7kjlmujdwp4ufc42p6l7x5ru2j87"
    "hvr85362rdqn23h24tyk8c9x8hsuu8jhezu26wwqlvz55dcdqxwkr6p"
)
D_LENGTH_PAST_END = (
    "lnbc2500u1pvjluezdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6nat07l32sgac4"
    "tnrndh8d44qlp6uv7mv8x4sxt87pn05a4medx6s6dpfcydezlus22seyghffflu4hgpd9tc"
    "n30zmy9xgyl97j00mhcpwsnm6a"
)
D_NOT_UTF8 = (
    "lnbc1961qqqqqqqdq2udhkven9v5utw390d9t34c43q7sktxf4k37jzau5yl6aqnqha9pg3"
    "yvhjrzlg5nu2nxmyrc6eh9t4lanjq6sgng8sjxqhmy2u4tkwdn62u2jp8vjqqkq70wr"
)


class TestDecodeExceptions:
    """
    A reader MUST fail malformed invoices with a Bolt11Exception, as decode does
    for a bad checksum or a short signature.
    https://github.com/lightning/bolts/blob/master/11-payment-encoding.md#requirements-3
    """

    @pytest.mark.parametrize(
        "invoice",
        [
            X_DATA_LENGTH_0,
            F_DATA_LENGTH_0,
            SIGNATURE_ALONE,
            D_LENGTH_PAST_END,
            D_NOT_UTF8,
        ],
        ids=[
            "x-data-length-0",
            "f-data-length-0",
            "signature-alone",
            "d-length-past-end",
            "d-not-utf8",
        ],
    )
    def test_decode_raises_bolt11_exception(self, invoice):
        with pytest.raises(Bolt11Exception):
            decode(invoice)

The command python -m pytest tests/test_decode_exceptions.py fails every case:

collected 5 items

tests/test_decode_exceptions.py FFFFF

___ test_decode_raises_bolt11_exception[x-data-length-0] ___
bolt11/decode.py:120: in decode
    tagdata.uint,
E   ValueError: Cannot interpret a zero length bitstring as an integer.
___ test_decode_raises_bolt11_exception[f-data-length-0] ___
bolt11/decode.py:132: in decode
    Fallback.from_bitstring(tagdata, currency),
bolt11/models/fallback.py:29: in from_bitstring
    wver = data[0:5].uint
E   ValueError: Cannot interpret a zero length bitstring as an integer.
___ test_decode_raises_bolt11_exception[signature-alone] ___
bolt11/decode.py:53: in decode
    timestamp = data_part.read(35).uint
E   bitstring.exceptions.ReadError: Cannot read 35 bits, only 0 available.
___ test_decode_raises_bolt11_exception[d-length-past-end] ___
bolt11/decode.py:26: in _pull_tagged
    return (CHARSET[tag], stream.read(length * 5), stream)
E   bitstring.exceptions.ReadError: Cannot read 315 bits, only 190 available.
___ test_decode_raises_bolt11_exception[d-not-utf8] ___
bolt11/decode.py:110: in decode
    trim_to_bytes(tagdata).decode(),
E   UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe3 in position 0: invalid continuation byte

5 failed

An x, c or f field with data_length 0 raises bitstring's ValueError at decode.py#L117-L128 and fallback.py#L29. A data part too short for the 35-bit timestamp, or a field whose data_length runs past the end of the data, raises bitstring.exceptions.ReadError at decode.py#L53 and decode.py#L23-L26. A d field that is not valid UTF-8 raises UnicodeDecodeError at decode.py#L110, and callers that catch Bolt11Exception miss all three.

Each input above should raise a Bolt11Exception, as decode does for a bad checksum or a short signature.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions