URI: 
       tTrampoline routing: - add support for trampoline forwarding - add regtest with trampoline payment - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit cf818fe08cdb1dcfc5651d9c2ea3edeee68c7e3e
   DIR parent ded449233ebf7cd863bc63da9af19c5561813128
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Tue,  9 Feb 2021 15:09:27 +0100
       
       Trampoline routing:
        - add support for trampoline forwarding
        - add regtest with trampoline payment
       
       Diffstat:
         M electrum/lnonion.py                 |      10 ++++++----
         M electrum/lnpeer.py                  |     121 +++++++++++++++++++++++++++++--
         M electrum/lnworker.py                |      22 +++++++++++++++++-----
         M electrum/tests/regtest.py           |       3 +++
         M electrum/tests/regtest/regtest.sh   |      26 ++++++++++++++++++++++++++
       
       5 files changed, 166 insertions(+), 16 deletions(-)
       ---
   DIR diff --git a/electrum/lnonion.py b/electrum/lnonion.py
       t@@ -349,7 +349,8 @@ class ProcessedOnionPacket(NamedTuple):
        def process_onion_packet(
                onion_packet: OnionPacket,
                associated_data: bytes,
       -        our_onion_private_key: bytes) -> ProcessedOnionPacket:
       +        our_onion_private_key: bytes,
       +        is_trampoline=False) -> ProcessedOnionPacket:
            if not ecc.ECPubkey.is_pubkey_bytes(onion_packet.public_key):
                raise InvalidOnionPubkey()
            shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key)
       t@@ -362,8 +363,9 @@ def process_onion_packet(
                raise InvalidOnionMac()
            # peel an onion layer off
            rho_key = get_bolt04_onion_key(b'rho', shared_secret)
       -    stream_bytes = generate_cipher_stream(rho_key, 2 * HOPS_DATA_SIZE)
       -    padded_header = onion_packet.hops_data + bytes(HOPS_DATA_SIZE)
       +    data_size = TRAMPOLINE_HOPS_DATA_SIZE if is_trampoline else HOPS_DATA_SIZE
       +    stream_bytes = generate_cipher_stream(rho_key, 2 * data_size)
       +    padded_header = onion_packet.hops_data + bytes(data_size)
            next_hops_data = xor_bytes(padded_header, stream_bytes)
            next_hops_data_fd = io.BytesIO(next_hops_data)
            hop_data = OnionHopsDataSingle.from_fd(next_hops_data_fd)
       t@@ -386,7 +388,7 @@ def process_onion_packet(
            next_public_key = next_public_key_int.get_public_key_bytes()
            next_onion_packet = OnionPacket(
                public_key=next_public_key,
       -        hops_data=next_hops_data_fd.read(HOPS_DATA_SIZE),
       +        hops_data=next_hops_data_fd.read(data_size),
                hmac=hop_data.hmac)
            if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):
                # we are the destination / exit node
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -1196,7 +1196,8 @@ class Peer(Logger):
                self.send_message("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs))
        
            def pay(self, *, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
       -            payment_hash: bytes, min_final_cltv_expiry: int, payment_secret: bytes = None) -> UpdateAddHtlc:
       +            payment_hash: bytes, min_final_cltv_expiry: int,
       +            payment_secret: bytes = None, fwd_trampoline_onion=None) -> UpdateAddHtlc:
                assert amount_msat > 0, "amount_msat is not greater zero"
                assert len(route) > 0
                if not chan.can_send_update_add_htlc():
       t@@ -1227,6 +1228,25 @@ class Peer(Logger):
                        if route_edge.invoice_routing_info:
                            hops_data[i].payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info}
        
       +                # only for final, legacy
       +                if i == num_hops - 2:
       +                    self.logger.info(f'adding payment secret for legacy trampoline')
       +                    hops_data[i].payload["payment_data"] = {
       +                        "payment_secret":payment_secret,
       +                        "total_msat": amount_msat,
       +                    }
       +
       +        # if we are forwarding a trampoline payment, add trampoline onion
       +        if fwd_trampoline_onion:
       +            self.logger.info(f'adding trampoline onion to final payload')
       +            trampoline_payload = hops_data[num_hops-2].payload
       +            trampoline_payload["trampoline_onion_packet"] = {
       +                "version": fwd_trampoline_onion.version,
       +                "public_key": fwd_trampoline_onion.public_key,
       +                "hops_data": fwd_trampoline_onion.hops_data,
       +                "hmac": fwd_trampoline_onion.hmac
       +            }
       +
                # create trampoline onion
                for i in range(num_hops):
                    route_edge = route[i]
       t@@ -1424,6 +1444,62 @@ class Peer(Logger):
                    raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
                return next_chan_scid, next_htlc.htlc_id
        
       +    def maybe_forward_trampoline(
       +            self, *,
       +            chan: Channel,
       +            htlc: UpdateAddHtlc,
       +            trampoline_onion: ProcessedOnionPacket):
       +
       +        payload = trampoline_onion.hop_data.payload
       +        payment_hash = htlc.payment_hash
       +        try:
       +            outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"]
       +            payment_secret = payload["payment_data"]["payment_secret"]
       +            amt_to_forward = payload["amt_to_forward"]["amt_to_forward"]
       +            cltv_from_onion = payload["outgoing_cltv_value"]["outgoing_cltv_value"]
       +            if "invoice_features" in payload:
       +                self.logger.info('forward_trampoline: legacy')
       +                next_trampoline_onion = None
       +                invoice_features = payload["invoice_features"]["invoice_features"]
       +                invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
       +            else:
       +                self.logger.info('forward_trampoline: end-to-end')
       +                invoice_features = 0
       +                next_trampoline_onion = trampoline_onion.next_packet
       +        except Exception as e:
       +            self.logger.exception('')
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       +
       +        trampoline_cltv_delta = htlc.cltv_expiry - cltv_from_onion
       +        trampoline_fee = htlc.amount_msat - amt_to_forward
       +
       +        @log_exceptions
       +        async def forward_trampoline_payment():
       +            try:
       +                await self.lnworker.pay_to_node(
       +                    node_pubkey=outgoing_node_id,
       +                    payment_hash=payment_hash,
       +                    payment_secret=payment_secret,
       +                    amount_to_pay=amt_to_forward,
       +                    min_cltv_expiry=cltv_from_onion,
       +                    r_tags=[],
       +                    t_tags=[],
       +                    invoice_features=invoice_features,
       +                    trampoline_onion=next_trampoline_onion,
       +                    trampoline_fee=trampoline_fee,
       +                    trampoline_cltv_delta=trampoline_cltv_delta,
       +                    attempts=1)
       +            except OnionRoutingFailure as e:
       +                # FIXME: cannot use payment_hash as key
       +                self.lnworker.trampoline_forwarding_failures[payment_hash] = e
       +            except PaymentFailure as e:
       +                # FIXME: adapt the error code
       +                error_reason = OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
       +                self.lnworker.trampoline_forwarding_failures[payment_hash] = error_reason
       +
       +        asyncio.ensure_future(forward_trampoline_payment())
       +
       +
            def maybe_fulfill_htlc(
                    self, *,
                    chan: Channel,
       t@@ -1444,10 +1520,12 @@ class Peer(Logger):
                    cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
                except:
                    raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       -        if cltv_from_onion != htlc.cltv_expiry:
       -            raise OnionRoutingFailure(
       -                code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
       -                data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
       +
       +        if not is_trampoline:
       +            if cltv_from_onion != htlc.cltv_expiry:
       +                raise OnionRoutingFailure(
       +                    code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
       +                    data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
                try:
                    amt_to_forward = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
                except:
       t@@ -1462,6 +1540,10 @@ class Peer(Logger):
                        code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
                        data=total_msat.to_bytes(8, byteorder="big"))
        
       +        outgoing_node_id = processed_onion.hop_data.payload.get("outgoing_node_id")
       +        if is_trampoline and outgoing_node_id:
       +            return
       +
                # if there is a trampoline_onion, perform the above checks on it
                if processed_onion.trampoline_onion_packet:
                    trampoline_onion = process_onion_packet(
       t@@ -1787,6 +1869,27 @@ class Peer(Logger):
                        chan=chan,
                        htlc=htlc,
                        processed_onion=processed_onion)
       +            # trampoline forwarding
       +            if not preimage and processed_onion.trampoline_onion_packet:
       +                if not forwarding_info:
       +                    trampoline_onion = self.process_onion_packet(
       +                        processed_onion.trampoline_onion_packet,
       +                        htlc.payment_hash,
       +                        onion_packet_bytes,
       +                        is_trampoline=True)
       +                    self.maybe_forward_trampoline(
       +                        chan=chan,
       +                        htlc=htlc,
       +                        trampoline_onion=trampoline_onion)
       +                    # we return True so that this code gets executed only once
       +                    return None, True, None
       +                else:
       +                    preimage = self.lnworker.get_preimage(payment_hash)
       +                    error_reason = self.lnworker.trampoline_forwarding_failures.pop(payment_hash, None)
       +                    if error_reason:
       +                        self.logger.info(f'trampoline forwarding failure {error_reason}')
       +                        raise error_reason
       +
                elif not forwarding_info:
                    next_chan_id, next_htlc_id = self.maybe_forward_htlc(
                        chan=chan,
       t@@ -1810,10 +1913,14 @@ class Peer(Logger):
                    return preimage, None, None
                return None, None, None
        
       -    def process_onion_packet(self, onion_packet, payment_hash, onion_packet_bytes):
       +    def process_onion_packet(self, onion_packet, payment_hash, onion_packet_bytes, is_trampoline=False):
                failure_data = sha256(onion_packet_bytes)
                try:
       -            processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey)
       +            processed_onion = process_onion_packet(
       +                onion_packet,
       +                associated_data=payment_hash,
       +                our_onion_private_key=self.privkey,
       +                is_trampoline=is_trampoline)
                except UnsupportedOnionPacketVersion:
                    raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data)
                except InvalidOnionPubkey:
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -660,6 +660,8 @@ class LNWallet(LNWorker):
                for payment_hash in self.get_payments(status='inflight').keys():
                    self.set_invoice_status(payment_hash.hex(), PR_INFLIGHT)
        
       +        self.trampoline_forwarding_failures = {} # todo: should be persisted
       +
            @property
            def channels(self) -> Mapping[bytes, Channel]:
                """Returns a read-only copy of channels."""
       t@@ -1063,8 +1065,16 @@ class LNWallet(LNWorker):
        
            async def pay_to_node(
                    self, node_pubkey, payment_hash, payment_secret, amount_to_pay,
       -            min_cltv_expiry, r_tags, t_tags, invoice_features, *, attempts: int = 1,
       -            full_path: LNPaymentPath = None):
       +            min_cltv_expiry, r_tags, t_tags, invoice_features, *,
       +            attempts: int = 1, full_path: LNPaymentPath=None,
       +            trampoline_onion=None, trampoline_fee=None, trampoline_cltv_delta=None):
       +
       +        if trampoline_onion:
       +            # todo: compare to the fee of the actual route we found
       +            if trampoline_fee < 1000:
       +                raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
       +            if trampoline_cltv_delta < 576:
       +                raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
        
                self.logs[payment_hash.hex()] = log = []
                amount_inflight = 0 # what we sent in htlcs
       t@@ -1084,7 +1094,7 @@ class LNWallet(LNWorker):
                            routes = [(route, amount_to_send)]
                        # 2. send htlcs
                        for route, amount_msat in routes:
       -                    await self.pay_to_route(route, amount_msat, payment_hash, payment_secret, min_cltv_expiry)
       +                    await self.pay_to_route(route, amount_msat, payment_hash, payment_secret, min_cltv_expiry, trampoline_onion)
                            amount_inflight += amount_msat
                        util.trigger_callback('invoice_status', self.wallet, payment_hash.hex())
                    # 3. await a queue
       t@@ -1101,7 +1111,7 @@ class LNWallet(LNWorker):
                    self.handle_error_code_from_failed_htlc(htlc_log)
        
        
       -    async def pay_to_route(self, route: LNPaymentRoute, amount_msat:int, payment_hash:bytes, payment_secret:bytes, min_cltv_expiry:int):
       +    async def pay_to_route(self, route: LNPaymentRoute, amount_msat:int, payment_hash:bytes, payment_secret:bytes, min_cltv_expiry:int, trampoline_onion:bytes =None):
                # send a single htlc
                short_channel_id = route[0].short_channel_id
                chan = self.get_channel_by_short_id(short_channel_id)
       t@@ -1115,7 +1125,8 @@ class LNWallet(LNWorker):
                    amount_msat=amount_msat,
                    payment_hash=payment_hash,
                    min_final_cltv_expiry=min_cltv_expiry,
       -            payment_secret=payment_secret)
       +            payment_secret=payment_secret,
       +            fwd_trampoline_onion=trampoline_onion)
                self.htlc_routes[(payment_hash, short_channel_id, htlc.htlc_id)] = route
                util.trigger_callback('htlc_added', chan, htlc, SENT)
        
       t@@ -1383,6 +1394,7 @@ class LNWallet(LNWorker):
                channels = list(self.channels.values())
                scid_to_my_channels = {chan.short_channel_id: chan for chan in channels
                                       if chan.short_channel_id is not None}
       +
                blacklist = self.network.channel_blacklist.get_current_list()
                for private_route in r_tags:
                    if len(private_route) == 0:
   DIR diff --git a/electrum/tests/regtest.py b/electrum/tests/regtest.py
       t@@ -58,5 +58,8 @@ class TestLightningABC(TestLightning):
            def test_forwarding(self):
                self.run_shell(['forwarding'])
        
       +    def test_trampoline(self):
       +        self.run_shell(['trampoline'])
       +
            def test_watchtower(self):
                self.run_shell(['watchtower'])
   DIR diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh
       t@@ -128,6 +128,32 @@ if [[ $1 == "forwarding" ]]; then
            $carol close_channel $chan2
        fi
        
       +if [[ $1 == "trampoline" ]]; then
       +    $alice stop
       +    $alice setconfig -o use_gossip False
       +    $alice daemon -d
       +    $alice load_wallet
       +    sleep 1
       +    $bob setconfig lightning_forward_payments true
       +    bob_node=$($bob nodeid)
       +    channel_id1=$($alice open_channel $bob_node 0.002 --push_amount 0.001)
       +    channel_id2=$($carol open_channel $bob_node 0.002 --push_amount 0.001)
       +    echo "mining 3 blocks"
       +    new_blocks 3
       +    sleep 10 # time for channelDB
       +    request=$($carol add_lightning_request 0.0001 -m "blah" | jq -r ".invoice")
       +    $alice lnpay --attempts=2 $request
       +    carol_balance=$($carol list_channels | jq -r '.[0].local_balance')
       +    echo "carol balance: $carol_balance"
       +    if [[ $carol_balance != 110000 ]]; then
       +        exit 1
       +    fi
       +    chan1=$($alice list_channels | jq -r ".[0].channel_point")
       +    chan2=$($carol list_channels | jq -r ".[0].channel_point")
       +    $alice close_channel $chan1
       +    $carol close_channel $chan2
       +fi
       +
        # alice sends two payments, then broadcast ctx after first payment.
        # thus, bob needs to redeem both to_local and to_remote