Skip to content

verify_hrp accepts invalid amounts in the human-readable part #60

Open

Description

@Nuhiat-Arefin

verify_hrp accepts invalid amounts in the human-readable part, so lnbc+25m decodes with no amount, lnbc64=n decodes as 64 BTC, and the official sub-millisatoshi vector decodes with amount_msat 250000000. This happens on main at 51bd3b97, which is the v2.2.0 tag, and in release 2.1.1.

The test at tests/test_hrp_amount.py asserts that verify_hrp rejects the three human-readable parts and that decode rejects the three invoices:

import pytest

from bolt11 import decode
from bolt11.exceptions import Bolt11Exception, Bolt11HrpInvalidException
from bolt11.utils import verify_hrp

# The byte layout of a malformed human-readable part cannot be produced by
# encode, so these inputs stay literals.

# "lnbc+25m": the '+' ends the amount match and the rest of the hrp is ignored.
NO_AMOUNT = (
    "lnbc+25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5"
    "vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg"
    "s9qrsgqdm5y5lfv65tuksj8caqm3f7p6jqdyrj68h8l98yhp7v5ees9yugjaar2eulvh7wq02a"
    "cc5mm7kec5nfwj2dqmxsw3ph4mcrw404d42cq8n48kc"
)
# "lnbc64=n": the amount reads as 64 bitcoin and the '=n' is ignored.
SIXTY_FOUR_BTC = (
    "lnbc64=n1pllllllpp5yu9lx69d839gzwd4s9k6ykl3h2y62pkh7lt4mlv74c7ehahufylssp5"
    "gcrp86sukx8e6rgv5suwq3muss5e5wg66pc78s44tfmamhvtzc5qhp58yjmdan79s6qqdhdzgy"
    "nm4zwqd5d7xmw5fk98klysy043l2ahrqsm8xpnjdjtca6mq67huyl5dqv2yltr2efd67x06mjh"
    "9zlmfgamyyyy3a26uw7wxe3kqdq32n650muz490glrppsuhjd3guwt8manpqwqqst47cg"
)
# BOLT 11 vector "Invalid sub-millisatoshi precision.", amount 2500000001p.
SUB_MILLISATOSHI = (
    "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqf"
    "qypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg"
    "3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx"
    "3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x"
)


class TestInvalidHrpAmount:
    """
    A reader MUST fail an amount that contains a non-digit or is followed by
    anything except a multiplier, and a `p` amount whose last digit is not 0.
    https://github.com/lightning/bolts/blob/master/11-payment-encoding.md#human-readable-part
    """

    @pytest.mark.parametrize(
        "hrp",
        ["lnbc+25m", "lnbc64=n", "lnbc2500000001p"],
        ids=["trailing-plus", "trailing-equals", "sub-millisatoshi"],
    )
    def test_verify_hrp_rejects_invalid_amount(self, hrp):
        with pytest.raises(Bolt11HrpInvalidException):
            verify_hrp(hrp)

    @pytest.mark.parametrize(
        "invoice",
        [NO_AMOUNT, SIXTY_FOUR_BTC, SUB_MILLISATOSHI],
        ids=["no-amount", "sixty-four-btc", "sub-millisatoshi"],
    )
    def test_decode_rejects_invalid_hrp_amount(self, invoice):
        with pytest.raises(Bolt11Exception):
            decode(invoice)

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

collected 6 items

tests/test_hrp_amount.py FFFFFF

___ test_verify_hrp_rejects_invalid_amount[trailing-plus] ___
E   Failed: DID NOT RAISE Bolt11HrpInvalidException
___ test_verify_hrp_rejects_invalid_amount[trailing-equals] ___
E   Failed: DID NOT RAISE Bolt11HrpInvalidException
___ test_verify_hrp_rejects_invalid_amount[sub-millisatoshi] ___
E   Failed: DID NOT RAISE Bolt11HrpInvalidException
___ test_decode_rejects_invalid_hrp_amount[no-amount] ___
E   Failed: DID NOT RAISE Bolt11Exception
___ test_decode_rejects_invalid_hrp_amount[sixty-four-btc] ___
E   Failed: DID NOT RAISE Bolt11Exception
___ test_decode_rejects_invalid_hrp_amount[sub-millisatoshi] ___
E   Failed: DID NOT RAISE Bolt11Exception

6 failed

verify_hrp reads the amount with match(r"ln(bcrt|bc|tbs|tb)(\w+)?", hrp) at utils.py#L10. The pattern has no end anchor, so the match stops at the first character that is not a word character and nothing checks the rest of the hrp. amount_to_msat does not check the last digit of a p amount, and MilliSatoshi.from_btc truncates at types.py#L25-L27.

BOLT 11 states the Human-Readable Part reader requirements at L80-L87:

if amount contains a non-digit OR is followed by anything except a multiplier (see table above): MUST fail the payment.

if multiplier is p and the last decimal of amount is not 0: MUST fail the payment.

decode should raise a Bolt11Exception for each of the three inputs.

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