diff --git a/electrum/lnmsg.py b/electrum/lnmsg.py index e8ad41d24fa9..957a3bead4ca 100644 --- a/electrum/lnmsg.py +++ b/electrum/lnmsg.py @@ -5,17 +5,21 @@ from types import MappingProxyType from collections import OrderedDict +import electrum_ecc as ecc + +from . import bitcoin from .lnutil import OnionFailureCodeMetaFlag +from .util import chunks class FailedToParseMsg(Exception): msg_type_int: Optional[int] = None msg_type_name: Optional[str] = None + class UnknownMsgType(FailedToParseMsg): pass class UnknownOptionalMsgType(UnknownMsgType): pass class UnknownMandatoryMsgType(UnknownMsgType): pass - class MalformedMsg(FailedToParseMsg): pass class UnknownMsgFieldType(MalformedMsg): pass class UnexpectedEndOfStream(MalformedMsg): pass @@ -24,6 +28,7 @@ class UnknownMandatoryTLVRecordType(MalformedMsg): pass class MsgTrailingGarbage(MalformedMsg): pass class MsgInvalidFieldOrder(MalformedMsg): pass class UnexpectedFieldSizeForEncoder(MalformedMsg): pass +class MsgInvalidSignature(MalformedMsg): pass def _num_remaining_bytes_to_read(fd: io.BytesIO) -> int: @@ -94,7 +99,7 @@ def _read_primitive_field( fd: io.BytesIO, field_type: str, count: Union[int, str] -) -> Union[bytes, int]: +) -> Union[bytes, int, str]: if not fd: raise Exception() if isinstance(count, int): @@ -150,6 +155,8 @@ def _read_primitive_field( type_len = 32 elif field_type == 'signature': type_len = 64 + elif field_type == 'bip340sig': + type_len = 64 elif field_type == 'point': type_len = 33 elif field_type == 'short_channel_id': @@ -166,6 +173,9 @@ def _read_primitive_field( if len(buf) != type_len: raise UnexpectedEndOfStream() return buf + elif field_type == 'utf8': + if count != '...': + raise Exception(f"utf8 fields can only have unbounded count") if count == "...": total_len = -1 # read all @@ -177,6 +187,20 @@ def _read_primitive_field( buf = fd.read(total_len) if total_len >= 0 and len(buf) != total_len: raise UnexpectedEndOfStream() + + if field_type == 'utf8': + try: + return buf.decode('utf-8') + except UnicodeDecodeError as e: + raise MalformedMsg(f'invalid utf-8: {buf.hex()}') from e + + if field_type == 'point': + for point in chunks(buf, type_len): + try: + ecc.ECPubkey(b=point) + except ecc.keys.InvalidECPointException as e: + raise MalformedMsg(f"invalid point: {point.hex()}") from e + return buf @@ -186,7 +210,7 @@ def _write_primitive_field( fd: io.BytesIO, field_type: str, count: Union[int, str], - value: Union[bytes, int] + value: Union[bytes, int, str] ) -> None: if not fd: raise Exception() @@ -246,6 +270,8 @@ def _write_primitive_field( type_len = 32 elif field_type == 'signature': type_len = 64 + elif field_type == 'bip340sig': + type_len = 64 elif field_type == 'point': type_len = 33 elif field_type == 'short_channel_id': @@ -258,6 +284,10 @@ def _write_primitive_field( type_len = 33 # point else: raise Exception(f"invalid sciddir_or_pubkey, prefix byte not in range 0-3") + elif field_type == 'utf8': + if count != '...': + raise Exception(f"utf8 fields can only have unbounded count") + value = value.encode('utf-8') total_len = -1 if count != "...": if type_len is None: @@ -274,12 +304,16 @@ def _write_primitive_field( raise Exception(f"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?") -def _read_tlv_record(*, fd: io.BytesIO) -> Tuple[int, bytes]: +def _read_tlv_record(*, fd: io.BytesIO) -> Tuple[int, bytes, bytes]: if not fd: raise Exception() + pos_start = fd.tell() tlv_type = _read_primitive_field(fd=fd, field_type="bigsize", count=1) tlv_len = _read_primitive_field(fd=fd, field_type="bigsize", count=1) tlv_val = _read_primitive_field(fd=fd, field_type="byte", count=tlv_len) - return tlv_type, tlv_val + pos_end = fd.tell() + fd.seek(pos_start) + rawbytes = fd.read(pos_end - pos_start) + return tlv_type, tlv_val, rawbytes def _write_tlv_record(*, fd: io.BytesIO, tlv_type: int, tlv_val: bytes) -> None: @@ -321,6 +355,46 @@ def _parse_msgtype_intvalue_for_onion_wire(value: str) -> int: return msg_type_int +def _tlv_merkle_root(tlvs: List[Sequence[bytes]]) -> bytes: + first_tlv = None + tlv_merkle_nodes = [] + + for tlvt, tlv in tlvs: + if first_tlv is None: + first_tlv = tlv + tlv_val = tlv + tlv_record_type = write_bigsize_int(tlvt) + merkle_leaf_hash = bitcoin.bip340_tagged_hash(b'LnLeaf', tlv_val) + merkle_nonce = bitcoin.bip340_tagged_hash(b'LnNonce' + first_tlv, tlv_record_type) + + # ascending order + msg = merkle_leaf_hash + merkle_nonce if merkle_leaf_hash < merkle_nonce else merkle_nonce + merkle_leaf_hash + merkle_node_hash = bitcoin.bip340_tagged_hash(b'LnBranch', msg) + + tlv_merkle_nodes.append(merkle_node_hash) + + while len(tlv_merkle_nodes) > 1: + target = [] + for chunk in chunks(tlv_merkle_nodes, 2): + if len(chunk) == 1: + target.append(chunk[0]) + else: + msg = chunk[0] + chunk[1] if chunk[0] < chunk[1] else chunk[1] + chunk[0] + merkle_node_hash = bitcoin.bip340_tagged_hash(b'LnBranch', msg) + target.append(merkle_node_hash) + tlv_merkle_nodes = target + + return tlv_merkle_nodes[0] + + +def _is_bolt12_signature_tlv_type(tlv_type: int) -> bool: + """ + bolt12: each form is signed using one or more *signature TLV elements*: TLV + types 240 through 1000 (inclusive) + """ + return tlv_type in range(240, 1001) + + class LNSerializer: def __init__(self, *, name: str = 'peer_wire'): @@ -331,6 +405,7 @@ def __init__(self, *, name: str = 'peer_wire'): self.in_tlv_stream_get_tlv_record_scheme_from_type = {} # type: Dict[str, Dict[int, List[Sequence[str]]]] self.in_tlv_stream_get_record_type_from_name = {} # type: Dict[str, Dict[str, int]] self.in_tlv_stream_get_record_name_from_type = {} # type: Dict[str, Dict[int, str]] + self.in_tlv_stream_signature_tlv_records = {} # type: Dict[str, Dict[int, str]] self.subtypes = {} # type: Dict[str, Dict[str, Sequence[str]]] @@ -393,7 +468,17 @@ def __init__(self, *, name: str = 'peer_wire'): assert fieldname not in self.subtypes[subtypename], f"duplicate field definition for {fieldname} for subtype {subtypename}" self.subtypes[subtypename][fieldname] = tuple(row) else: - pass # TODO + pass # TODO: raise? + + for stream_name, scheme_map in self.in_tlv_stream_get_tlv_record_scheme_from_type.items(): + sig_records = {} + for tlv_type, scheme in scheme_map.items(): + for row in scheme: + if row[0] == 'tlvdata' and row[4] == 'bip340sig': + assert _is_bolt12_signature_tlv_type(tlv_type), f"bip340sig field outside bolt 12 range: {stream_name=} {tlv_type=}" + sig_records[tlv_type] = row[3] # e.g. 240: 'sig' + break + self.in_tlv_stream_signature_tlv_records[stream_name] = sig_records # e.g. 'invoice_request': {240: 'sig'} def write_field( self, @@ -495,14 +580,42 @@ def read_field( count=subtype_field_count) parsedlist.append(parsed) + # fd might contain more bytes, but we got passed a count. break when we have 'count' items. + # (e.g. nested complex types) + if isinstance(count, int) and len(parsedlist) == count: + break + return parsedlist if count == '...' or count > 1 else parsedlist[0] - def write_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str, **kwargs) -> None: + def write_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str, signing_key: Optional[bytes] = None, **kwargs) -> None: + sign_over_tlvs = [] + sig_tlv_type, sig_tlv_record_name = None, None + sig_records = self.in_tlv_stream_signature_tlv_records.get(tlv_stream_name, {}) + if signing_key is not None: + sig_tlv_types = list(sig_records.keys()) # e.g. [240] ('signature') + if len(sig_tlv_types) != 1: + raise NotImplementedError + sig_tlv_type = sig_tlv_types[0] + sig_tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][sig_tlv_type] + assert sig_tlv_record_name not in kwargs, f"pass either existing {sig_tlv_record_name} or signing_key" + scheme_map = self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name] for tlv_record_type, scheme in scheme_map.items(): # note: tlv_record_type is monotonically increasing tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type] + + is_signature_record = tlv_record_type in sig_records if tlv_record_name not in kwargs: - continue + # skip record_name if not in kwargs, unless we need to generate it + if not is_signature_record or signing_key is None: + continue + # calculate signature over previously serialized tlv records + # and store in kwargs for inclusion in tlv stream + merkle_root = _tlv_merkle_root(sign_over_tlvs) + priv = ecc.ECPrivkey(signing_key) + tag = b'lightning' + tlv_stream_name.encode('ascii') + sig_tlv_record_name.encode('ascii') + signature = priv.schnorr_sign(bitcoin.bip340_tagged_hash(tag, merkle_root)) + kwargs[tlv_record_name] = {sig_records[tlv_record_type]: signature} # e.g. 'signature': {'sig': } + with io.BytesIO() as tlv_record_fd: for row in scheme: if row[0] == "tlvtype": @@ -525,14 +638,29 @@ def write_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str, **kwargs) -> value=field_value) else: raise Exception(f"unexpected row in scheme: {row!r}") - _write_tlv_record(fd=fd, tlv_type=tlv_record_type, tlv_val=tlv_record_fd.getvalue()) - def read_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str) -> Dict[str, Dict[str, Any]]: + tlv_val = tlv_record_fd.getvalue() + + _write_tlv_record(fd=fd, tlv_type=tlv_record_type, tlv_val=tlv_val) + + # signature TLVs are excluded from the bolt 12 merkle root + if signing_key is not None and not is_signature_record: + with io.BytesIO() as tlvfd: + _write_tlv_record(fd=tlvfd, tlv_type=tlv_record_type, tlv_val=tlv_val) + sign_over_tlvs.append((tlv_record_type, tlvfd.getvalue())) + + def read_tlv_stream(self, *, + fd: io.BytesIO, + tlv_stream_name: str, + signing_key_path: Optional[Sequence[str]] = None) -> Dict[str, Dict[str, Any]]: + sign_over_tlvs = [] + signature_record = None # type: Optional[tuple[int, str]] # (tlv_type, tlv_record_name) parsed = {} # type: Dict[str, Dict[str, Any]] scheme_map = self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name] + sig_records = self.in_tlv_stream_signature_tlv_records.get(tlv_stream_name, {}) last_seen_tlv_record_type = -1 # type: int while _num_remaining_bytes_to_read(fd) > 0: - tlv_record_type, tlv_record_val = _read_tlv_record(fd=fd) + tlv_record_type, tlv_record_val, rawbytes = _read_tlv_record(fd=fd) if not (tlv_record_type > last_seen_tlv_record_type): raise MsgInvalidFieldOrder(f"TLV records must be monotonically increasing by type. " f"cur: {tlv_record_type}. prev: {last_seen_tlv_record_type}") @@ -545,8 +673,20 @@ def read_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str) -> Dict[str, raise UnknownMandatoryTLVRecordType(f"{tlv_stream_name}/{tlv_record_type}") from None else: # unknown "odd" type: skip it + if signing_key_path and not _is_bolt12_signature_tlv_type(tlv_record_type): + sign_over_tlvs.append((tlv_record_type, rawbytes)) continue tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type] + + # collect tlvs for deferred signature check + if signing_key_path: + if tlv_record_type in sig_records: + if signature_record is not None: + raise MalformedMsg(f"multiple signatures in {tlv_stream_name=} not supported") + signature_record = (tlv_record_type, tlv_record_name) + else: + sign_over_tlvs.append((tlv_record_type, rawbytes)) + parsed[tlv_record_name] = {} with io.BytesIO(tlv_record_val) as tlv_record_fd: for row in scheme: @@ -573,6 +713,25 @@ def read_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str) -> Dict[str, raise Exception(f"unexpected row in scheme: {row!r}") if _num_remaining_bytes_to_read(tlv_record_fd) > 0: raise MsgTrailingGarbage(f"TLV record ({tlv_stream_name}/{tlv_record_name}) has extra trailing garbage") + + if signing_key_path: + if signature_record is None: + raise MalformedMsg(f"expected signature in {tlv_stream_name}") + sig_tlv_type, sig_tlv_record_name = signature_record + merkle_root = _tlv_merkle_root(sign_over_tlvs) + signing_pubkey = parsed + for key in signing_key_path: # walk signing_key_path + signing_pubkey = signing_pubkey[key] + assert isinstance(signing_pubkey, bytes) + sig_field_name = sig_records[sig_tlv_type] + sig_bytes = parsed[sig_tlv_record_name][sig_field_name] + if not isinstance(sig_bytes, bytes) or len(sig_bytes) != 64: + raise MsgInvalidSignature(f"invalid signature data in {tlv_stream_name}/{sig_tlv_record_name}: {sig_bytes=}") + tag = b'lightning' + tlv_stream_name.encode('ascii') + sig_tlv_record_name.encode('ascii') + tagh = bitcoin.bip340_tagged_hash(tag, merkle_root) + if not ecc.ECPubkey(signing_pubkey).schnorr_verify(sig_bytes, tagh): + raise MsgInvalidSignature(f"invalid signature in {'.'.join(signing_key_path)}") + return parsed def encode_msg(self, msg_type: str, **kwargs) -> bytes: diff --git a/electrum/lnwire/onion_wire.csv b/electrum/lnwire/onion_wire.csv index 06b55da1b85e..c3e015a5cb07 100644 --- a/electrum/lnwire/onion_wire.csv +++ b/electrum/lnwire/onion_wire.csv @@ -116,3 +116,141 @@ subtype,blinded_path_hop subtypedata,blinded_path_hop,blinded_node_id,point, subtypedata,blinded_path_hop,enclen,u16, subtypedata,blinded_path_hop,encrypted_recipient_data,byte,enclen +tlvtype,offer,offer_chains,2 +tlvdata,offer,offer_chains,chains,chain_hash,... +tlvtype,offer,offer_metadata,4 +tlvdata,offer,offer_metadata,data,byte,... +tlvtype,offer,offer_currency,6 +tlvdata,offer,offer_currency,iso4217,utf8,... +tlvtype,offer,offer_amount,8 +tlvdata,offer,offer_amount,amount,tu64, +tlvtype,offer,offer_description,10 +tlvdata,offer,offer_description,description,utf8,... +tlvtype,offer,offer_features,12 +tlvdata,offer,offer_features,features,byte,... +tlvtype,offer,offer_absolute_expiry,14 +tlvdata,offer,offer_absolute_expiry,seconds_from_epoch,tu64, +tlvtype,offer,offer_paths,16 +tlvdata,offer,offer_paths,paths,blinded_path,... +tlvtype,offer,offer_issuer,18 +tlvdata,offer,offer_issuer,issuer,utf8,... +tlvtype,offer,offer_quantity_max,20 +tlvdata,offer,offer_quantity_max,max,tu64, +tlvtype,offer,offer_issuer_id,22 +tlvdata,offer,offer_issuer_id,id,point, +tlvtype,invoice_request,invreq_metadata,0 +tlvdata,invoice_request,invreq_metadata,blob,byte,... +tlvtype,invoice_request,offer_chains,2 +tlvdata,invoice_request,offer_chains,chains,chain_hash,... +tlvtype,invoice_request,offer_metadata,4 +tlvdata,invoice_request,offer_metadata,data,byte,... +tlvtype,invoice_request,offer_currency,6 +tlvdata,invoice_request,offer_currency,iso4217,utf8,... +tlvtype,invoice_request,offer_amount,8 +tlvdata,invoice_request,offer_amount,amount,tu64, +tlvtype,invoice_request,offer_description,10 +tlvdata,invoice_request,offer_description,description,utf8,... +tlvtype,invoice_request,offer_features,12 +tlvdata,invoice_request,offer_features,features,byte,... +tlvtype,invoice_request,offer_absolute_expiry,14 +tlvdata,invoice_request,offer_absolute_expiry,seconds_from_epoch,tu64, +tlvtype,invoice_request,offer_paths,16 +tlvdata,invoice_request,offer_paths,paths,blinded_path,... +tlvtype,invoice_request,offer_issuer,18 +tlvdata,invoice_request,offer_issuer,issuer,utf8,... +tlvtype,invoice_request,offer_quantity_max,20 +tlvdata,invoice_request,offer_quantity_max,max,tu64, +tlvtype,invoice_request,offer_issuer_id,22 +tlvdata,invoice_request,offer_issuer_id,id,point, +tlvtype,invoice_request,invreq_chain,80 +tlvdata,invoice_request,invreq_chain,chain,chain_hash, +tlvtype,invoice_request,invreq_amount,82 +tlvdata,invoice_request,invreq_amount,msat,tu64, +tlvtype,invoice_request,invreq_features,84 +tlvdata,invoice_request,invreq_features,features,byte,... +tlvtype,invoice_request,invreq_quantity,86 +tlvdata,invoice_request,invreq_quantity,quantity,tu64, +tlvtype,invoice_request,invreq_payer_id,88 +tlvdata,invoice_request,invreq_payer_id,key,point, +tlvtype,invoice_request,invreq_payer_note,89 +tlvdata,invoice_request,invreq_payer_note,note,utf8,... +tlvtype,invoice_request,invreq_paths,90 +tlvdata,invoice_request,invreq_paths,paths,blinded_path,... +tlvtype,invoice_request,signature,240 +tlvdata,invoice_request,signature,sig,bip340sig, +tlvtype,invoice,invreq_metadata,0 +tlvdata,invoice,invreq_metadata,blob,byte,... +tlvtype,invoice,offer_chains,2 +tlvdata,invoice,offer_chains,chains,chain_hash,... +tlvtype,invoice,offer_metadata,4 +tlvdata,invoice,offer_metadata,data,byte,... +tlvtype,invoice,offer_currency,6 +tlvdata,invoice,offer_currency,iso4217,utf8,... +tlvtype,invoice,offer_amount,8 +tlvdata,invoice,offer_amount,amount,tu64, +tlvtype,invoice,offer_description,10 +tlvdata,invoice,offer_description,description,utf8,... +tlvtype,invoice,offer_features,12 +tlvdata,invoice,offer_features,features,byte,... +tlvtype,invoice,offer_absolute_expiry,14 +tlvdata,invoice,offer_absolute_expiry,seconds_from_epoch,tu64, +tlvtype,invoice,offer_paths,16 +tlvdata,invoice,offer_paths,paths,blinded_path,... +tlvtype,invoice,offer_issuer,18 +tlvdata,invoice,offer_issuer,issuer,utf8,... +tlvtype,invoice,offer_quantity_max,20 +tlvdata,invoice,offer_quantity_max,max,tu64, +tlvtype,invoice,offer_issuer_id,22 +tlvdata,invoice,offer_issuer_id,id,point, +tlvtype,invoice,invreq_chain,80 +tlvdata,invoice,invreq_chain,chain,chain_hash, +tlvtype,invoice,invreq_amount,82 +tlvdata,invoice,invreq_amount,msat,tu64, +tlvtype,invoice,invreq_features,84 +tlvdata,invoice,invreq_features,features,byte,... +tlvtype,invoice,invreq_quantity,86 +tlvdata,invoice,invreq_quantity,quantity,tu64, +tlvtype,invoice,invreq_payer_id,88 +tlvdata,invoice,invreq_payer_id,key,point, +tlvtype,invoice,invreq_payer_note,89 +tlvdata,invoice,invreq_payer_note,note,utf8,... +tlvtype,invoice,invreq_paths,90 +tlvdata,invoice,invreq_paths,paths,blinded_path,... +tlvtype,invoice,invoice_paths,160 +tlvdata,invoice,invoice_paths,paths,blinded_path,... +tlvtype,invoice,invoice_blindedpay,162 +tlvdata,invoice,invoice_blindedpay,payinfo,blinded_payinfo,... +tlvtype,invoice,invoice_created_at,164 +tlvdata,invoice,invoice_created_at,timestamp,tu64, +tlvtype,invoice,invoice_relative_expiry,166 +tlvdata,invoice,invoice_relative_expiry,seconds_from_creation,tu32, +tlvtype,invoice,invoice_payment_hash,168 +tlvdata,invoice,invoice_payment_hash,payment_hash,sha256, +tlvtype,invoice,invoice_amount,170 +tlvdata,invoice,invoice_amount,msat,tu64, +tlvtype,invoice,invoice_fallbacks,172 +tlvdata,invoice,invoice_fallbacks,fallbacks,fallback_address,... +tlvtype,invoice,invoice_features,174 +tlvdata,invoice,invoice_features,features,byte,... +tlvtype,invoice,invoice_node_id,176 +tlvdata,invoice,invoice_node_id,node_id,point, +tlvtype,invoice,signature,240 +tlvdata,invoice,signature,sig,bip340sig, +subtype,blinded_payinfo +subtypedata,blinded_payinfo,fee_base_msat,u32, +subtypedata,blinded_payinfo,fee_proportional_millionths,u32, +subtypedata,blinded_payinfo,cltv_expiry_delta,u16, +subtypedata,blinded_payinfo,htlc_minimum_msat,u64, +subtypedata,blinded_payinfo,htlc_maximum_msat,u64, +subtypedata,blinded_payinfo,flen,u16, +subtypedata,blinded_payinfo,features,byte,flen +subtype,fallback_address +subtypedata,fallback_address,version,byte, +subtypedata,fallback_address,len,u16, +subtypedata,fallback_address,address,byte,len +tlvtype,invoice_error,erroneous_field,1 +tlvdata,invoice_error,erroneous_field,tlv_fieldnum,tu64, +tlvtype,invoice_error,suggested_value,3 +tlvdata,invoice_error,suggested_value,value,byte,... +tlvtype,invoice_error,error,5 +tlvdata,invoice_error,error,msg,utf8,... diff --git a/tests/bolt12-signature-test.json b/tests/bolt12-signature-test.json new file mode 100644 index 000000000000..00d327071e66 --- /dev/null +++ b/tests/bolt12-signature-test.json @@ -0,0 +1,137 @@ +[ + { + "comment": "Simple n1 test, tlv1 = 1000", + "tlv": "n1", + "first-tlv": "010203e8", + "leaves": [ + { + "H(`LnLeaf`,010203e8)": "67a2a995433890d8fe0c18a1765ad19e98f1fcfeff14c13a45bbc80964a78cf7", + "H(`LnNonce`|first-tlv,tlv1-type)": "255a95f5b6b3c6997e2838dc4d9348807fb6da8eb7bbc02d30662d144718b6aa", + "H(`LnBranch`,leaf+nonce)": "b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93" + } + ], + "branches": [], + "merkle": "b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93" + }, + { + "comment": "n1 test, tlv1 = 1000, tlv2 = 1x2x3", + "tlv": "n1", + "first-tlv": "010203e8", + "leaves": [ + { + "H(`LnLeaf`,010203e8)": "67a2a995433890d8fe0c18a1765ad19e98f1fcfeff14c13a45bbc80964a78cf7", + "H(`LnNonce`|first-tlv,tlv1-type)": "255a95f5b6b3c6997e2838dc4d9348807fb6da8eb7bbc02d30662d144718b6aa", + "H(`LnBranch`,leaf+nonce)": "b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93" + }, + { + "H(`LnLeaf`,02080000010000020003)": "cc04567fcbff60d4de87afe5142de16b7401531300554838b2d1117341a4ea8d", + "H(`LnNonce`|first-tlv,tlv2-type)": "12bc15565410d8e3251a6fb1c53a2d360f39a9f65afb8403ef875016e34ff678", + "H(`LnBranch`,leaf+nonce)": "19d6ecfa3be88d29c30e56167f58526d7695dfac9cb95e1256deb222c92db4d0" + } + ], + "branches": [ + { + "desc": "1: tlv1+nonce and tlv2+nonce", + "H(`LnBranch`,19d6ecfa3be88d29c30e56167f58526d7695dfac9cb95e1256deb222c92db4d0b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93)": "c3774abbf4815aa54ccaa026bff6581f01f3be5fe814c620a252534f434bc0d1" + } + ], + "merkle": "c3774abbf4815aa54ccaa026bff6581f01f3be5fe814c620a252534f434bc0d1" + }, + { + "comment": "n1 test, tlv1 = 1000, tlv2 = 1x2x3, tlv3 = 0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518, 1, 2", + "tlv": "n1", + "first-tlv": "010203e8", + "leaves": [ + { + "H(`LnLeaf`,010203e8)": "67a2a995433890d8fe0c18a1765ad19e98f1fcfeff14c13a45bbc80964a78cf7", + "H(`LnNonce`|first-tlv,1)": "255a95f5b6b3c6997e2838dc4d9348807fb6da8eb7bbc02d30662d144718b6aa", + "H(`LnBranch`,leaf+nonce)": "b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93" + }, + { + "H(`LnLeaf`,02080000010000020003)": "cc04567fcbff60d4de87afe5142de16b7401531300554838b2d1117341a4ea8d", + "H(`LnNonce`|first-tlv,2)": "12bc15565410d8e3251a6fb1c53a2d360f39a9f65afb8403ef875016e34ff678", + "H(`LnBranch`,leaf+nonce)": "19d6ecfa3be88d29c30e56167f58526d7695dfac9cb95e1256deb222c92db4d0" + }, + { + "H(`LnLeaf`,03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002)": "47da319b36d61a006e0dbcf6642fe4c822c33a6131af67dfa9293b089c5cbd27", + "H(`LnNonce`|first-tlv,3)": "068cf6e9d2db9258a6c1d3304a8f2e9d4d046ea711664c9a96960234f707a084", + "H(`LnBranch`,leaf+nonce)": "7c879819c09f1525e7bc69b84f7928180de584f92c846e01fa2daf5b17e32967" + } + ], + "branches": [ + { + "desc": "1: tlv1+nonce and tlv2+nonce", + "H(`LnBranch`,19d6ecfa3be88d29c30e56167f58526d7695dfac9cb95e1256deb222c92db4d0b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93)": "c3774abbf4815aa54ccaa026bff6581f01f3be5fe814c620a252534f434bc0d1" + }, + { + "desc": "1 and tlv3+nonce", + "H(`LnBranch`,7c879819c09f1525e7bc69b84f7928180de584f92c846e01fa2daf5b17e32967c3774abbf4815aa54ccaa026bff6581f01f3be5fe814c620a252534f434bc0d1)": "ab2e79b1283b0b31e0b035258de23782df6b89a38cfa7237bde69aed1a658c5d" + } + ], + "merkle": "ab2e79b1283b0b31e0b035258de23782df6b89a38cfa7237bde69aed1a658c5d" + }, + { + "comment": "invoice_request test: offer_issuer_id = Alice (privkey 0x414141...), offer_description = 'A Mathematical Treatise', offer_amount = 100, offer_currency = 'USD', invreq_payer_id = Bob (privkey 0x424242...), invreq_metadata = 0x0000000000000000", + "bolt12": "lnr1qqyqqqqqqqqqqqqqqcp4256ypqqkgzshgysy6ct5dpjk6ct5d93kzmpq23ex2ct5d9ek293pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpjkppqvjx204vgdzgsqpvcp4mldl3plscny0rt707gvpdh6ndydfacz43euzqhrurageg3n7kafgsek6gz3e9w52parv8gs2hlxzk95tzeswywffxlkeyhml0hh46kndmwf4m6xma3tkq2lu04qz3slje2rfthc89vss", + "tlv": "invoice_request", + "first-tlv": "00080000000000000000", + "leaves": [ + { + "H(`LnLeaf`,00080000000000000000)": "cd45d50b8dbb73ba995f92aa48be7c2909331998cb070572f5499bae338a03c6", + "H(`LnNonce`|first-tlv,0)": "edc13c82e89b213a5641b27f0c06c5f31ea948a0cc2fd6495120cc8590cac3f5", + "H(`LnBranch`,leaf+nonce)": "5ced451fad76ab7edc8084b84c8b5086df195b2a503c25b371e6850a280c94ab" + }, + { + "H(`LnLeaf`,0603555344)": "ae61bfe63f8fc81b7a02a962182a5b5e01501365806481d52fbdfbca915266fa", + "H(`LnNonce`|first-tlv,6)": "cc9fc57ce5e82252b6cc8908a93f012b13294a82132768e36dd767b3c3c289e8", + "H(`LnBranch`,leaf+nonce)": "a2ea87a666c1524d25132ff59883c96a118728ff76595d239f5806143e3e9c9e" + }, + { + "H(`LnLeaf`,080164)": "b4f3adb8ca4f4a4c0e7cd9e0b1cafe8634cf8a864e1a730868bdda39fbd3e336", + "H(`LnNonce`|first-tlv,8)": "376180f1ef3b7973ba4989f9391502bd78a1a8a54929fe9adcaec1dd2bfec648", + "H(`LnBranch`,leaf+nonce)": "fa0bb4f0fa2f2625c63eec9bf3a29c9aa304e64d5aa44d38e050a6bd7d6fc5c0" + }, + { + "H(`LnLeaf`,0a1741204d617468656d61746963616c205472656174697365)": "7007775409456c33c47bddd7ce946ecd5a82035f1d5a529cc90e84d146f75a6e", + "H(`LnNonce`|first-tlv,10)": "01926a0c38b4ec71d76b116eeb81ea7999706fdce24a7f5b9d67bf867fd0c4d8", + "H(`LnBranch`,leaf+nonce)": "349379beebd68fd72296e76cb2ae28554b35fa9234853956b81b24c008783230" + }, + { + "H(`LnLeaf`,162102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619)": "bdde38b7b58fa74acee1e943bbc32c04306368cb2aa513856f53f45be461051b", + "H(`LnNonce`|first-tlv,22)": "2e571571c7dd0739dbc4180bb96b7652b055f9e97f80d37337c96689990fdbaa", + "H(`LnBranch`,leaf+nonce)": "384853c9811863028876088ce34e75d784ac027fd564f103ea972cdf96236e47" + }, + { + "H(`LnLeaf`,58210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c)": "f3b92382531e261e16a0f35d65f314ae622306bbb1b206fee00d80153b76eea3", + "H(`LnNonce`|first-tlv,88)": "c31a695332d176217470b705cde5c8cd71cdb611e1f26c5a98f14c0d935c97bd", + "H(`LnBranch`,leaf+nonce)": "73e067757513706491e0da4e8077112e606da55c04239ad13ab609bc82907600" + } + ], + "branches": [ + { + "desc": "1: metadata+nonce and currency+nonce", + "H(`LnBranch`,5ced451fad76ab7edc8084b84c8b5086df195b2a503c25b371e6850a280c94aba2ea87a666c1524d25132ff59883c96a118728ff76595d239f5806143e3e9c9e)": "f0aa4611039a3a8a90dc8331fa75c9acf433be7285cac0983902aaaa8f66aaa9" + }, + { + "desc": "2: amount+nonce and descripton+nonce", + "H(`LnBranch`,349379beebd68fd72296e76cb2ae28554b35fa9234853956b81b24c008783230fa0bb4f0fa2f2625c63eec9bf3a29c9aa304e64d5aa44d38e050a6bd7d6fc5c0)": "92e6478159d6763b19c5d03a8a834e179116f89e0cec700049e5ce921f8c400e" + }, + { + "desc": "3: 1 and 2", + "H(`LnBranch`,92e6478159d6763b19c5d03a8a834e179116f89e0cec700049e5ce921f8c400ef0aa4611039a3a8a90dc8331fa75c9acf433be7285cac0983902aaaa8f66aaa9)": "432097bd1a848ab41eee3695a2c5932c4aea987b27b1a61e58ac950ecce1214a" + }, + { + "desc": "4: node_id+nonce and payer_id+nonce", + "H(`LnBranch`,384853c9811863028876088ce34e75d784ac027fd564f103ea972cdf96236e4773e067757513706491e0da4e8077112e606da55c04239ad13ab609bc82907600)": "2ac9b0261d644027939d9a7bd055cb2468b79d92c6811d56a300c6b8ff97c14d" + }, + { + "desc": "5: 3 and 4", + "H(`LnBranch`,2ac9b0261d644027939d9a7bd055cb2468b79d92c6811d56a300c6b8ff97c14d432097bd1a848ab41eee3695a2c5932c4aea987b27b1a61e58ac950ecce1214a)": "608407c18ad9a94d9ea2bcdbe170b6c20c462a7833a197621c916f78cf18e624" + } + ], + "merkle": "608407c18ad9a94d9ea2bcdbe170b6c20c462a7833a197621c916f78cf18e624", + "signature_tag": "lightninginvoice_requestsignature", + "H(signature_tag,merkle)": "aefe3aa88a69772c246dcaef75ed3e7566c08ecc4e9f995233526a5651fc34cd", + "signature": "b8f83ea3288cfd6ea510cdb481472575141e8d8744157f98562d162cc1c472526fdb24befefbdebab4dbb726bbd1b7d8aec057f8fa805187e5950d2bbe0e5642" + } +] diff --git a/tests/test_lnmsg.py b/tests/test_lnmsg.py index b7ca81798444..63bde575c3c9 100644 --- a/tests/test_lnmsg.py +++ b/tests/test_lnmsg.py @@ -1,12 +1,13 @@ import io +import os from electrum.lnmsg import (read_bigsize_int, write_bigsize_int, FieldEncodingNotMinimal, UnexpectedEndOfStream, LNSerializer, UnknownMandatoryTLVRecordType, MalformedMsg, MsgTrailingGarbage, MsgInvalidFieldOrder, encode_msg, decode_msg, UnexpectedFieldSizeForEncoder, OnionWireSerializer, - UnknownMsgType) + UnknownMsgType, _tlv_merkle_root, _read_tlv_record) from electrum.lnonion import OnionRoutingFailure -from electrum.util import bfh +from electrum.util import bfh, read_json_file from electrum.lnutil import ShortChannelID, LnFeatures from electrum.channel_db import NodeInfo from electrum import constants @@ -116,9 +117,8 @@ def test_read_tlv_stream_tests1(self): lnser.read_tlv_stream(fd=io.BytesIO(bfh("0329023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb0000000000000001")), tlv_stream_name="n1") with self.assertRaises(UnexpectedEndOfStream): lnser.read_tlv_stream(fd=io.BytesIO(bfh("0330023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb000000000000000100000000000001")), tlv_stream_name="n1") - # check if ECC point is valid?... skip for now. - #with self.assertRaises(Exception): - # lnser.read_tlv_stream(fd=io.BytesIO(bfh("0331043da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb00000000000000010000000000000002")), tlv_stream_name="n1") + with self.assertRaises(MalformedMsg): # check if ECC point is valid + lnser.read_tlv_stream(fd=io.BytesIO(bfh("0331043da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb00000000000000010000000000000002")), tlv_stream_name="n1") with self.assertRaises(MsgTrailingGarbage): lnser.read_tlv_stream(fd=io.BytesIO(bfh("0332023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb0000000000000001000000000000000001")), tlv_stream_name="n1") with self.assertRaises(UnexpectedEndOfStream): @@ -413,3 +413,26 @@ def test_address_parsing_and_serialization(self): self.assertEqual(paf(taf(host, port)), [(host, port)]) for input_, output in valid_inputs_with_defined_output: self.assertEqual(paf(taf(*input_)), output) + + def test_bolt12_merkle_root_test_vectors(self): + """Tests against the test vector file from the bolts repository.""" + test_vector_file = os.path.join(os.path.dirname(__file__), "bolt12-signature-test.json") + vectors = read_json_file(test_vector_file) + leaf_key_prefix = "H(`LnLeaf`," + + def _extract_tlvs(vector): + tlvs = [] + for leaf in vector["leaves"]: + leaf_tlv_hex = next( # "H(`LnLeaf`,010203e8)" <- extract the tlv hex + k[len(leaf_key_prefix):-1] for k in leaf if k.startswith(leaf_key_prefix) + ) + tlv_bytes = bfh(leaf_tlv_hex) + tlv_type, _, _ = _read_tlv_record(fd=io.BytesIO(tlv_bytes)) + tlvs.append((tlv_type, tlv_bytes)) + return tlvs + + for vector in vectors: + with self.subTest(comment=vector["comment"]): + tlvs = _extract_tlvs(vector) + merkle_root = _tlv_merkle_root(tlvs) + self.assertEqual(vector["merkle"], merkle_root.hex())