Skip to content

Instantly share code, notes, and snippets.

@t-bast
Last active May 26, 2023 13:17
Show Gist options
  • Save t-bast/b1371d357a2c5f3e8c09514a62db7079 to your computer and use it in GitHub Desktop.
Save t-bast/b1371d357a2c5f3e8c09514a62db7079 to your computer and use it in GitHub Desktop.
CLTV expiry usage in blinded paths

CLTV expiry for blinded paths

Let's consider the following setup:

  • E creates a blinded path C->D->E
  • E computes a cltv_expiry_delta for the blinded path:
    • E starts by adding the cltv_expiry_delta of every hop
    • E sets cltv_expiry_delta = 10 in the encrypted_recipient_data for C
    • E sets cltv_expiry_delta = 15 in the encrypted_recipient_data for D
    • E adds its min_final_expiry_delta (let's use 5)
    • cltv_expiry_delta = 10 + 15 + 5 = 30
                                                          blinded_path: expiry_delta = 30
                                        +---------------------------------------------------------------+
+---+         +---+   expiry_delta = 20 | +---+   expiry_delta = 10   +---+   expiry_delta = 15   +---+ |
| A |---------| B |-----------------------| C |-----------------------| D |-----------------------| E | |
+---+         +---+                     | +---+                       +---+                       +---+ |
                                        +---------------------------------------------------------------+

A wants to pay E using that blinded path, and finds a path to C through B. A computes the cltv_expiry for each HTLC:

  • She chooses a random number of blocks to add for privacy: sender_delta = 15
  • In the onion for E:
    • sets outgoing_cltv_value = block_height_when_sending_payment + sender_delta
  • In the onion for D (blinded):
    • does not set outgoing_cltv_value
    • sets encrypted_recipient_data provided by the recipient for D (e.g. in a Bolt 12 invoice)
  • In the onion for C (introduction node of the blinded path):
    • does not set outgoing_cltv_value
    • sets encrypted_recipient_data provided by the recipient for C (e.g. in a Bolt 12 invoice)
  • In the onion for B (not blinded):
    • sets outgoing_cltv_value = block_height_when_sending_payment + 15 + blinded_path_expiry_delta = block_height_when_sending_payment + 45
  • In the HTLC for B:
    • sets cltv_expiry = block_height_when_sending_payment + 45 + bc_expiry_delta = block_height_when_sending_payment + 65

A sends the HTLC to B (honest scenario):

  • B receives the HTLC with cltv_expiry = block_height_when_sending_payment + 65:
    • B verifies that the onion's outgoing_cltv_value is valid for its configured cltv_expiry_delta
    • B forwards the HTLC to C with cltv_expiry = block_height_when_sending_payment + 45
  • C receives the HTLC with cltv_expiry = block_height_when_sending_payment + 45:
    • the onion does not contain outgoing_cltv_value
    • but the encrypted_recipient_data specifies cltv_expiry_delta = 10
    • C forwards the HTLC to D with cltv_expiry = block_height_when_sending_payment + 35
  • D receives the HTLC with cltv_expiry = block_height_when_sending_payment + 35
    • the onion does not contain outgoing_cltv_value
    • but the encrypted_recipient_data specifies cltv_expiry_delta = 15
    • D forwards the HTLC to E with cltv_expiry = block_height_when_sending_payment + 20
  • E receives the HTLC with cltv_expiry = block_height_when_sending_payment + 20
    • the onion contains outgoing_cltv_value = block_height_when_sending_payment + 20
    • E verifies that cltv_expiry >= outgoing_cltv_value -> OK
    • E verifies that cltv_expiry >= current_block_height + min_final_cltv_expiry_delta:
      • note that the current_block_height may be different from block_height_when_sending_payment
      • the inequality is then block_height_when_sending_payment + sender_delta >= current_block_height
      • which is fine as long as A's sender_delta covers the block height difference

If C uses a lower cltv_expiry_delta than requested by E, this should be allowed:

  • C receives the HTLC with cltv_expiry = block_height_when_sending_payment + 45:
    • the encrypted_recipient_data specifies cltv_expiry_delta = 10
    • C should forward the HTLC to D with cltv_expiry = block_height_when_sending_payment + 35
    • C instead forwards the HTLC to D with cltv_expiry = block_height_when_sending_payment + 40
  • D receives the HTLC with cltv_expiry = block_height_when_sending_payment + 40
    • the encrypted_recipient_data specifies cltv_expiry_delta = 15
    • D forwards the HTLC to E with cltv_expiry = block_height_when_sending_payment + 25
  • E receives the HTLC with cltv_expiry = block_height_when_sending_payment + 25
    • the onion contains outgoing_cltv_value = block_height_when_sending_payment + 20
    • E verifies that cltv_expiry >= outgoing_cltv_value -> OK
    • E verifies that cltv_expiry >= current_block_height + min_final_cltv_expiry_delta:
      • note that the current_block_height may be different from block_height_when_sending_payment
      • the inequality is then block_height_when_sending_payment + 20 >= current_block_height -> OK

If C uses a higher cltv_expiry_delta than requested by E, this is a cheating attempt:

  • C receives the HTLC with cltv_expiry = block_height_when_sending_payment + 45:
    • the encrypted_recipient_data specifies cltv_expiry_delta = 10
    • C should forward the HTLC to D with cltv_expiry = block_height_when_sending_payment + 35
    • C instead forwards the HTLC to D with cltv_expiry = block_height_when_sending_payment + 30
  • D receives the HTLC with cltv_expiry = block_height_when_sending_payment + 30
    • the encrypted_recipient_data specifies cltv_expiry_delta = 15
    • D forwards the HTLC to E with cltv_expiry = block_height_when_sending_payment + 15
  • E receives the HTLC with cltv_expiry = block_height_when_sending_payment + 15
    • the onion contains outgoing_cltv_value = block_height_when_sending_payment + 20
    • E verifies that cltv_expiry >= outgoing_cltv_value -> NOT OK
    • E verifies that cltv_expiry >= current_block_height + min_final_cltv_expiry_delta -> OK
    • E rejects the payment

We don't need to detail the cases where other intermediate nodes try to cheat: the effect is exactly the same.

If A user a higher cltv_expiry_delta for the blinded path, this should be allowed:

  • A creates the HTLC to B:
    • In the onion for E, A sets outgoing_cltv_value = block_height_when_sending_payment
    • A uses cltv_expiry_delta = 40 instead of 30 for the blinded path
    • In the onion for B, A sets outgoing_cltv_value = block_height_when_sending_payment + 40
    • In the HTLC for B, A sets cltv_expiry = block_height_when_sending_payment + 40 + bc_expiry_delta = block_height_when_sending_payment + 60
  • B receives the HTLC with cltv_expiry = block_height_when_sending_payment + 60:
    • B forwards the HTLC to C with cltv_expiry = block_height_when_sending_payment + 40
  • C receives the HTLC with cltv_expiry = block_height_when_sending_payment + 40:
    • the onion does not contain outgoing_cltv_value
    • but the encrypted_recipient_data specifies cltv_expiry_delta = 10
    • C forwards the HTLC to D with cltv_expiry = block_height_when_sending_payment + 30
  • D receives the HTLC with cltv_expiry = block_height_when_sending_payment + 30
    • the onion does not contain outgoing_cltv_value
    • but the encrypted_recipient_data specifies cltv_expiry_delta = 15
    • D forwards the HTLC to E with cltv_expiry = block_height_when_sending_payment + 15
  • E receives the HTLC with cltv_expiry = block_height_when_sending_payment + 15
    • the onion contains outgoing_cltv_value = block_height_when_sending_payment
    • E verifies that cltv_expiry >= outgoing_cltv_value -> OK
    • E verifies that cltv_expiry >= current_block_height + min_final_cltv_expiry_delta -> OK

If A uses a lower cltv_expiry_delta for the blinded path, this is a cheating attempt:

  • A creates the HTLC to B:
    • In the onion for E, A sets outgoing_cltv_value = block_height_when_sending_payment + sender_delta
    • A uses cltv_expiry_delta = 25 instead of 30 for the blinded path
    • In the onion for B, A sets outgoing_cltv_value = block_height_when_sending_payment + sender_delta + 25
    • In the HTLC for B, A sets cltv_expiry = block_height_when_sending_payment + sender_delta + 25 + bc_expiry_delta = block_height_when_sending_payment + sender_delta + 45
  • B receives the HTLC with cltv_expiry = block_height_when_sending_payment + sender_delta + 45:
    • B forwards the HTLC to C with cltv_expiry = block_height_when_sending_payment + sender_delta + 25
  • C receives the HTLC with cltv_expiry = block_height_when_sending_payment + sender_delta + 25:
    • the onion does not contain outgoing_cltv_value
    • but the encrypted_recipient_data specifies cltv_expiry_delta = 10
    • C forwards the HTLC to D with cltv_expiry = block_height_when_sending_payment + sender_delta + 15
  • D receives the HTLC with cltv_expiry = block_height_when_sending_payment + sender_delta + 15
    • the onion does not contain outgoing_cltv_value
    • but the encrypted_recipient_data specifies cltv_expiry_delta = 15
    • D forwards the HTLC to E with cltv_expiry = block_height_when_sending_payment + sender_delta
  • E receives the HTLC with cltv_expiry = block_height_when_sending_payment + sender_delta
    • the onion contains outgoing_cltv_value = block_height_when_sending_payment + sender_delta
    • E verifies that cltv_expiry >= outgoing_cltv_value -> OK
    • E verifies that cltv_expiry >= current_block_height + min_final_cltv_expiry_delta:
      • which is equivalent to sender_delta >= current_block_height - block_height_when_sending_payment + min_final_cltv_expiry_delta
      • this lets A discover the min_final_cltv_expiry_delta used by E, but this isn't a private information anyway
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment