Skip to content

Commit 862f76b

Browse files
committed
partial merge 1: f321x's "lightning: refactor htlc switch"
split-off from #10230
2 parents 2ec6c3b + 32aa6ab commit 862f76b

File tree

11 files changed

+229
-169
lines changed

11 files changed

+229
-169
lines changed

electrum/commands.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,7 +1438,7 @@ async def settle_hold_invoice(self, preimage: str, wallet: Abstract_Wallet = Non
14381438
assert payment_hash in wallet.lnworker.payment_info, \
14391439
f"Couldn't find lightning invoice for {payment_hash=}"
14401440
assert payment_hash in wallet.lnworker.dont_settle_htlcs, f"Invoice {payment_hash=} not a hold invoice?"
1441-
assert wallet.lnworker.is_accepted_mpp(bfh(payment_hash)), \
1441+
assert wallet.lnworker.is_complete_mpp(bfh(payment_hash)), \
14421442
f"MPP incomplete, cannot settle hold invoice {payment_hash} yet"
14431443
info: Optional['PaymentInfo'] = wallet.lnworker.get_payment_info(bfh(payment_hash))
14441444
assert (wallet.lnworker.get_payment_mpp_amount_msat(bfh(payment_hash)) or 0) >= (info.amount_msat or 0)
@@ -1465,7 +1465,7 @@ async def cancel_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet =
14651465
wallet.lnworker.set_payment_status(bfh(payment_hash), PR_UNPAID)
14661466
wallet.lnworker.delete_payment_info(payment_hash)
14671467
wallet.set_label(payment_hash, None)
1468-
while wallet.lnworker.is_accepted_mpp(bfh(payment_hash)):
1468+
while wallet.lnworker.is_complete_mpp(bfh(payment_hash)):
14691469
# wait until the htlcs got failed so the payment won't get settled accidentally in a race
14701470
await asyncio.sleep(0.1)
14711471
del wallet.lnworker.dont_settle_htlcs[payment_hash]
@@ -1490,18 +1490,18 @@ async def check_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet =
14901490
"""
14911491
assert len(payment_hash) == 64, f"Invalid payment_hash length: {len(payment_hash)} != 64"
14921492
info: Optional['PaymentInfo'] = wallet.lnworker.get_payment_info(bfh(payment_hash))
1493-
is_accepted_mpp: bool = wallet.lnworker.is_accepted_mpp(bfh(payment_hash))
1493+
is_complete_mpp: bool = wallet.lnworker.is_complete_mpp(bfh(payment_hash))
14941494
amount_sat = (wallet.lnworker.get_payment_mpp_amount_msat(bfh(payment_hash)) or 0) // 1000
14951495
result = {
14961496
"status": "unknown",
14971497
"received_amount_sat": amount_sat,
14981498
}
14991499
if info is None:
15001500
pass
1501-
elif not is_accepted_mpp and not wallet.lnworker.get_preimage_hex(payment_hash):
1502-
# is_accepted_mpp is False for settled payments
1501+
elif not is_complete_mpp and not wallet.lnworker.get_preimage_hex(payment_hash):
1502+
# is_complete_mpp is False for settled payments
15031503
result["status"] = "unpaid"
1504-
elif is_accepted_mpp and payment_hash in wallet.lnworker.dont_settle_htlcs:
1504+
elif is_complete_mpp and payment_hash in wallet.lnworker.dont_settle_htlcs:
15051505
result["status"] = "paid"
15061506
payment_key: str = wallet.lnworker._get_payment_key(bfh(payment_hash)).hex()
15071507
htlc_status = wallet.lnworker.received_mpp_htlcs[payment_key]

electrum/lnchannel.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1919
# THE SOFTWARE.
20+
import dataclasses
2021
import enum
2122
from collections import defaultdict
2223
from enum import IntEnum, Enum
@@ -1202,12 +1203,10 @@ def add_htlc(self, htlc: UpdateAddHtlc) -> UpdateAddHtlc:
12021203
"""Adds a new LOCAL HTLC to the channel.
12031204
Action must be initiated by LOCAL.
12041205
"""
1205-
if isinstance(htlc, dict): # legacy conversion # FIXME remove
1206-
htlc = UpdateAddHtlc(**htlc)
12071206
assert isinstance(htlc, UpdateAddHtlc)
12081207
self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=htlc.amount_msat)
12091208
if htlc.htlc_id is None:
1210-
htlc = attr.evolve(htlc, htlc_id=self.hm.get_next_htlc_id(LOCAL))
1209+
htlc = dataclasses.replace(htlc, htlc_id=self.hm.get_next_htlc_id(LOCAL))
12111210
with self.db_lock:
12121211
self.hm.send_htlc(htlc)
12131212
self.logger.info("add_htlc")
@@ -1217,15 +1216,13 @@ def receive_htlc(self, htlc: UpdateAddHtlc, onion_packet:bytes = None) -> Update
12171216
"""Adds a new REMOTE HTLC to the channel.
12181217
Action must be initiated by REMOTE.
12191218
"""
1220-
if isinstance(htlc, dict): # legacy conversion # FIXME remove
1221-
htlc = UpdateAddHtlc(**htlc)
12221219
assert isinstance(htlc, UpdateAddHtlc)
12231220
try:
12241221
self._assert_can_add_htlc(htlc_proposer=REMOTE, amount_msat=htlc.amount_msat)
12251222
except PaymentFailure as e:
12261223
raise RemoteMisbehaving(e) from e
12271224
if htlc.htlc_id is None: # used in unit tests
1228-
htlc = attr.evolve(htlc, htlc_id=self.hm.get_next_htlc_id(REMOTE))
1225+
htlc = dataclasses.replace(htlc, htlc_id=self.hm.get_next_htlc_id(REMOTE))
12291226
with self.db_lock:
12301227
self.hm.recv_htlc(htlc)
12311228
if onion_packet:

electrum/lnonion.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, OnionFailureCodeMetaFlag)
3737
from .lnmsg import OnionWireSerializer, read_bigsize_int, write_bigsize_int
3838
from . import lnmsg
39+
from . import util
3940

4041
if TYPE_CHECKING:
4142
from .lnrouter import LNPaymentRoute
@@ -113,11 +114,11 @@ def __repr__(self):
113114

114115
class OnionPacket:
115116

116-
def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes):
117+
def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes, version: int = 0):
117118
assert len(public_key) == 33
118119
assert len(hops_data) in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]
119120
assert len(hmac) == PER_HOP_HMAC_SIZE
120-
self.version = 0
121+
self.version = version
121122
self.public_key = public_key
122123
self.hops_data = hops_data # also called RoutingInfo in bolt-04
123124
self.hmac = hmac
@@ -140,13 +141,11 @@ def to_bytes(self) -> bytes:
140141
def from_bytes(cls, b: bytes):
141142
if len(b) - 66 not in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]:
142143
raise Exception('unexpected length {}'.format(len(b)))
143-
version = b[0]
144-
if version != 0:
145-
raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version))
146144
return OnionPacket(
147145
public_key=b[1:34],
148146
hops_data=b[34:-32],
149-
hmac=b[-32:]
147+
hmac=b[-32:],
148+
version=b[0],
150149
)
151150

152151

@@ -361,6 +360,9 @@ def process_onion_packet(
361360
associated_data: bytes = b'',
362361
is_trampoline=False,
363362
tlv_stream_name='payload') -> ProcessedOnionPacket:
363+
# TODO: check Onion features ( PERM|NODE|3 (required_node_feature_missing )
364+
if onion_packet.version != 0:
365+
raise UnsupportedOnionPacketVersion()
364366
if not ecc.ECPubkey.is_pubkey_bytes(onion_packet.public_key):
365367
raise InvalidOnionPubkey()
366368
shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key)
@@ -369,7 +371,7 @@ def process_onion_packet(
369371
calculated_mac = hmac_oneshot(
370372
mu_key, msg=onion_packet.hops_data+associated_data,
371373
digest=hashlib.sha256)
372-
if onion_packet.hmac != calculated_mac:
374+
if not util.constant_time_compare(onion_packet.hmac, calculated_mac):
373375
raise InvalidOnionMac()
374376
# peel an onion layer off
375377
rho_key = get_bolt04_onion_key(b'rho', shared_secret)
@@ -484,23 +486,38 @@ def obfuscate_onion_error(error_packet, their_public_key, our_onion_private_key)
484486

485487
def _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
486488
session_key: bytes) -> Tuple[bytes, int]:
487-
"""Returns the decoded error bytes, and the index of the sender of the error."""
489+
"""
490+
Returns the decoded error bytes, and the index of the sender of the error.
491+
https://github.com/lightning/bolts/blob/14272b1bd9361750cfdb3e5d35740889a6b510b5/04-onion-routing.md?plain=1#L1096
492+
"""
488493
num_hops = len(payment_path_pubkeys)
489494
hop_shared_secrets, _ = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
490-
for i in range(num_hops):
491-
ammag_key = get_bolt04_onion_key(b'ammag', hop_shared_secrets[i])
492-
um_key = get_bolt04_onion_key(b'um', hop_shared_secrets[i])
495+
result = None
496+
dummy_secret = bytes(32)
497+
# SHOULD continue decrypting, until the loop has been repeated 27 times
498+
for i in range(27):
499+
if i < num_hops:
500+
ammag_key = get_bolt04_onion_key(b'ammag', hop_shared_secrets[i])
501+
um_key = get_bolt04_onion_key(b'um', hop_shared_secrets[i])
502+
else:
503+
# SHOULD use constant `ammag` and `um` keys to obfuscate the route length.
504+
ammag_key = get_bolt04_onion_key(b'ammag', dummy_secret)
505+
um_key = get_bolt04_onion_key(b'um', dummy_secret)
506+
493507
stream_bytes = generate_cipher_stream(ammag_key, len(error_packet))
494508
error_packet = xor_bytes(error_packet, stream_bytes)
495509
hmac_computed = hmac_oneshot(um_key, msg=error_packet[32:], digest=hashlib.sha256)
496510
hmac_found = error_packet[:32]
497-
if hmac_computed == hmac_found:
498-
return error_packet, i
511+
if util.constant_time_compare(hmac_found, hmac_computed) and i < num_hops:
512+
result = error_packet, i
513+
514+
if result is not None:
515+
return result
499516
raise FailedToDecodeOnionError()
500517

501518

502519
def decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
503-
session_key: bytes) -> (OnionRoutingFailure, int):
520+
session_key: bytes) -> Tuple[OnionRoutingFailure, int]:
504521
"""Returns the failure message, and the index of the sender of the error."""
505522
decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key)
506523
failure_msg = get_failure_msg_from_onion_error(decrypted_error)

electrum/lnpeer.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from .crypto import sha256, sha256d, privkey_to_pubkey
2525
from . import bitcoin, util
2626
from . import constants
27-
from .util import (bfh, log_exceptions, ignore_exceptions, chunks, OldTaskGroup,
27+
from .util import (log_exceptions, ignore_exceptions, chunks, OldTaskGroup,
2828
UnrelatedTransactionException, error_text_bytes_to_safe_str, AsyncHangDetector,
2929
NoDynamicFeeEstimates, event_listener, EventListener)
3030
from . import transaction
@@ -36,7 +36,7 @@
3636
OnionPacket, construct_onion_error, obfuscate_onion_error, OnionRoutingFailure,
3737
ProcessedOnionPacket, UnsupportedOnionPacketVersion, InvalidOnionMac, InvalidOnionPubkey,
3838
OnionFailureCodeMetaFlag)
39-
from .lnchannel import Channel, RevokeAndAck, RemoteCtnTooFarInFuture, ChannelState, PeerState, ChanCloseOption, CF_ANNOUNCE_CHANNEL
39+
from .lnchannel import Channel, RevokeAndAck, ChannelState, PeerState, ChanCloseOption, CF_ANNOUNCE_CHANNEL
4040
from . import lnutil
4141
from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, ChannelConfig,
4242
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
@@ -45,19 +45,17 @@
4545
LOCAL, REMOTE, HTLCOwner,
4646
ln_compare_features, MIN_FINAL_CLTV_DELTA_ACCEPTED,
4747
RemoteMisbehaving, ShortChannelID,
48-
IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage,
49-
ChannelType, LNProtocolWarning, validate_features,
48+
IncompatibleLightningFeatures, ChannelType, LNProtocolWarning, validate_features,
5049
IncompatibleOrInsaneFeatures, FeeBudgetExceeded,
51-
GossipForwardingMessage, GossipTimestampFilter)
52-
from .lnutil import FeeUpdate, channel_id_from_funding_tx, PaymentFeeBudget
53-
from .lnutil import serialize_htlc_key, Keypair
50+
GossipForwardingMessage, GossipTimestampFilter, channel_id_from_funding_tx,
51+
PaymentFeeBudget, serialize_htlc_key, Keypair, RecvMPPResolution)
5452
from .lntransport import LNTransport, LNTransportBase, LightningPeerConnectionClosed, HandshakeFailed
5553
from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg
5654
from .interface import GracefulDisconnect
5755
from .lnrouter import fee_for_edge_msat
5856
from .json_db import StoredDict
5957
from .invoices import PR_PAID
60-
from .fee_policy import FEE_LN_ETA_TARGET, FEE_LN_MINIMUM_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING
58+
from .fee_policy import FEE_LN_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING
6159
from .trampoline import decode_routing_info
6260

6361
if TYPE_CHECKING:
@@ -525,12 +523,13 @@ async def wrapper_func(self, *args, **kwargs):
525523
@handle_disconnect
526524
async def main_loop(self):
527525
async with self.taskgroup as group:
528-
await group.spawn(self.htlc_switch())
529526
await group.spawn(self._message_loop())
530527
await group.spawn(self._query_gossip())
531528
await group.spawn(self._process_gossip())
532529
await group.spawn(self._send_own_gossip())
533530
await group.spawn(self._forward_gossip())
531+
if self.network.lngossip != self.lnworker:
532+
await group.spawn(self.htlc_switch())
534533

535534
async def _process_gossip(self):
536535
while True:
@@ -2467,7 +2466,6 @@ def check_mpp_is_waiting(
24672466
exc_incorrect_or_unknown_pd: OnionRoutingFailure,
24682467
log_fail_reason: Callable[[str], None],
24692468
) -> bool:
2470-
from .lnworker import RecvMPPResolution
24712469
mpp_resolution = self.lnworker.check_mpp_status(
24722470
payment_secret=payment_secret,
24732471
short_channel_id=short_channel_id,
@@ -2482,7 +2480,7 @@ def check_mpp_is_waiting(
24822480
elif mpp_resolution == RecvMPPResolution.FAILED:
24832481
log_fail_reason(f"mpp_resolution is FAILED")
24842482
raise exc_incorrect_or_unknown_pd
2485-
elif mpp_resolution == RecvMPPResolution.ACCEPTED:
2483+
elif mpp_resolution == RecvMPPResolution.COMPLETE:
24862484
return False
24872485
else:
24882486
raise Exception(f"unexpected {mpp_resolution=}")
@@ -2592,11 +2590,9 @@ def log_fail_reason(reason: str):
25922590
raise exc_incorrect_or_unknown_pd
25932591

25942592
preimage = self.lnworker.get_preimage(payment_hash)
2595-
expected_payment_secrets = [self.lnworker.get_payment_secret(htlc.payment_hash)]
2596-
if preimage:
2597-
expected_payment_secrets.append(derive_payment_secret_from_payment_preimage(preimage)) # legacy secret for old invoices
2598-
if payment_secret_from_onion not in expected_payment_secrets:
2599-
log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {expected_payment_secrets[0].hex()}')
2593+
expected_payment_secret = self.lnworker.get_payment_secret(htlc.payment_hash)
2594+
if payment_secret_from_onion != expected_payment_secret:
2595+
log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {expected_payment_secret.hex()}')
26002596
raise exc_incorrect_or_unknown_pd
26012597
invoice_msat = info.amount_msat
26022598
if channel_opening_fee:

electrum/lnsweep.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ def txs_htlc(
432432
ctn=ctn)
433433
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
434434
if direction == RECEIVED:
435-
if not chan.lnworker.is_accepted_mpp(htlc.payment_hash):
435+
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
436436
# do not redeem this, it might publish the preimage of an incomplete MPP
437437
continue
438438
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
@@ -727,7 +727,7 @@ def tx_htlc(
727727
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
728728
is_received_htlc = direction == RECEIVED
729729
if not is_received_htlc and not is_revocation:
730-
if not chan.lnworker.is_accepted_mpp(htlc.payment_hash):
730+
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
731731
# do not redeem this, it might publish the preimage of an incomplete MPP
732732
continue
733733
preimage = chan.lnworker.get_preimage(htlc.payment_hash)

0 commit comments

Comments
 (0)