Rusty Russell [ARCHIVE] on Nostr: 📅 Original date posted:2017-05-04 📝 Original message: Fabrice Drouin ...
📅 Original date posted:2017-05-04
📝 Original message:
Fabrice Drouin <fabrice.drouin at acinq.fr> writes:
> Hi Rusty,
>
> Payment requests should also include a timestamp and an expiry date (they
> could be optional tagged items but I think it makes more sense to make them
> mandatory)
Excellent point. Provability definitely requires a timestamp, but the
duration could be optional.
Here's the patch I just pushed:
Subject: Add timestamp and (optional) expiry.
We take advantage of the variable length encoding for the expiry timestamp,
and 32 bits for the offer time (wake me in 2106 to update the spec).
I chose a reasonable default expiry of 1 hour; the intention is that
the software should warn if this expiry approaches.
Suggested-by: Fabrice Drouin <fabrice.drouin at acinq.fr>
Signed-off-by: Rusty Russell <rusty at rustcorp.com.au>
diff --git a/README.md b/README.md
index 3039333..6da4150 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ Human readable part:
And data part:
1. Version: 0 (5 bits)
+1. UTC timestamp in seconds-since-Unix-epoch (32 bits)
1. Payment hash (256 bits)
1. Zero or more tagged parts.
1. Signature (bitcoin-style, of SHA256(SHA256(), plus recovery byte) of above. (520 bits)
@@ -25,6 +26,7 @@ Currently defined tagged parts are:
1. h: description of purpose of payment (SHA256). This is used to commit
to an associated description which is too long to fit, such as may
be contained in a web page.
+1. x: expiry time in seconds. Default is 3600 (1 hour) if not specified.
1. f: fallback onchain-address. 20 bytes == p2pkh. 21 bytes == p2wpkh, 33 bytes == p2wsh.
1. r: extra routing information. This should be appended to the route
to allow routing to non-public nodes; there may be more
diff --git a/examples.sh b/examples.sh
index 78c2be8..f526223 100755
--- a/examples.sh
+++ b/examples.sh
@@ -11,8 +11,8 @@ echo "# Please send 10 satoshi using rhash $RHASH to me @$PUBKEY"
./lightning-address.py encode 10000 $RHASH $PRIVKEY
echo
-echo "# Please send \$3 for a cup of coffee to the same peer"
-./lightning-address.py encode --description='1 cup coffee' $((3 * 100000000000 / $CONVERSION_RATE)) $RHASH $PRIVKEY
+echo "# Please send \$3 for a cup of coffee to the same peer, within 1 minute"
+./lightning-address.py encode --description='1 cup coffee' $((3 * 100000000000 / $CONVERSION_RATE)) --expires=60 $RHASH $PRIVKEY
echo
echo "# Now send \$24 for an entire list of things (hashed)"
diff --git a/lightning-address.py b/lightning-address.py
index c43d020..7b72314 100755
--- a/lightning-address.py
+++ b/lightning-address.py
@@ -3,6 +3,7 @@ import argparse
import hashlib
import re
import sys
+import time
# Try 'pip3 install secp256k1'
import secp256k1
@@ -138,9 +139,24 @@ def u32list(val):
assert val < (1 << 32)
return bytearray([(val >> 24) & 0xff, (val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff])
+# Represent big-endian number with as many bytes as it takes.
+def varlist(val):
+ b = bytearray()
+ while val != 0:
+ b.append(val & 0xFF)
+ val = val // 256
+ b.reverse()
+ return b
+
def from_u32list(l):
return (l[0] << 24) + (l[1] << 16) + (l[2] << 8) + l[3]
+def from_varlist(l):
+ total = 0
+ for v in l:
+ total = total * 256 + v
+ return total
+
def tagged(char, l):
bits=convertbits(l, 8, 5)
assert len(bits) < (1 << 10)
@@ -169,9 +185,12 @@ def lnencode(options):
hrp = 'ln' + options.currency + amount
- # version + paymenthash
- data = [0] + convertbits(bytearray.fromhex(options.paymenthash), 8, 5)
+ # version + timestamp + paymenthash
+ now = int(time.time())
+ assert len(u32list(now) + bytearray.fromhex(options.paymenthash)) == 4 + 32
+ data = [0] + convertbits(u32list(now) + bytearray.fromhex(options.paymenthash), 8, 5)
+
for r in options.route:
pubkey,channel,fee,cltv = r.split('/')
route = bytearray.fromhex(pubkey) + bytearray.fromhex(channel) + u32list(int(fee)) + u32list(int(cltv))
@@ -183,7 +202,10 @@ def lnencode(options):
if options.description:
data = data + tagged('d', [ord(c) for c in options.description])
-
+
+ if options.expires:
+ data = data + tagged('x', varlist(options.expires))
+
if options.description_hashed:
data = data + tagged('h', hashlib.sha256(options.description_hashed.encode('utf-8')).digest())
@@ -239,14 +261,16 @@ def lndecode(options):
if options.rate:
print("(Conversion: {})".format(amount / 10**11 * float(options.rate)))
- # 32 bytes turns into 52 bytes when base32 encoded.
- if len(data) < 52:
- sys.exit("Not long enough to contain payment hash")
+ # 4 + 32 bytes turns into 58 bytes when base32 encoded.
+ if len(data) < 58:
+ sys.exit("Not long enough to contain timestamp and payment hash")
- decoded = convertbits(data[:52], 5, 8, False)
- data = data[52:]
- assert len(decoded) == 32
- print("Payment hash: {}".format(bytearray(decoded).hex()))
+ decoded = convertbits(data[:58], 5, 8, False)
+ data = data[58:]
+ assert len(decoded) == 4 + 32
+ tstamp = from_u32list(decoded[0:4])
+ print("Timestamp: {} ({})".format(tstamp, time.ctime(tstamp)))
+ print("Payment hash: {}".format(bytearray(decoded[4:]).hex()))
while len(data) > 0:
tag,tagdata,data = pull_tagged(data)
@@ -265,6 +289,8 @@ def lndecode(options):
print("Description: {}".format(''.join(chr(c) for c in tagdata)))
elif tag == 'h':
print("Description hash: {}".format(bytearray(tagdata).hex()))
+ elif tag == 'x':
+ print("Expiry (seconds): {}".format(from_varlist(tagdata)))
else:
print("UNKNOWN TAG {}: {}".format(tag, bytearray(tagdata).hex()))
@@ -286,6 +312,8 @@ parser_enc.add_argument('--description',
help='What is being purchased')
parser_enc.add_argument('--description-hashed',
help='What is being purchased (for hashing)')
+parser_enc.add_argument('--expires', type=int,
+ help='Seconds before offer expires')
parser_enc.add_argument('amount', type=int, help='Amount in millisatoshi')
parser_enc.add_argument('paymenthash', help='Payment hash (in hex)')
parser_enc.add_argument('privkey', help='Private key (in hex)')
Published at
2023-06-09 12:47:10Event JSON
{
"id": "78e03c42d9e0d11e38cf550ce40b5b455ee9f9e6313be8cfa3ff8188ea2b599d",
"pubkey": "13bd8c1c5e3b3508a07c92598647160b11ab0deef4c452098e223e443c1ca425",
"created_at": 1686314830,
"kind": 1,
"tags": [
[
"e",
"56742c88f6265e0090cd0ac17729d1bb4693d9767822b89dc939db0c32d43bbb",
"",
"root"
],
[
"e",
"86b36ea47bb28cb69600e23b6dc1fdf7e33ad825e7df3e0664de049b3eb90c57",
"",
"reply"
],
[
"p",
"81c48ba46c211bc8fdb490d1ccfb03609c7ea090f8587ddca1c990676f09cfd3"
]
],
"content": "📅 Original date posted:2017-05-04\n📝 Original message:\nFabrice Drouin \u003cfabrice.drouin at acinq.fr\u003e writes:\n\u003e Hi Rusty,\n\u003e\n\u003e Payment requests should also include a timestamp and an expiry date (they\n\u003e could be optional tagged items but I think it makes more sense to make them\n\u003e mandatory)\n\nExcellent point. Provability definitely requires a timestamp, but the\nduration could be optional.\n\nHere's the patch I just pushed:\n\nSubject: Add timestamp and (optional) expiry.\n\nWe take advantage of the variable length encoding for the expiry timestamp,\nand 32 bits for the offer time (wake me in 2106 to update the spec).\n\nI chose a reasonable default expiry of 1 hour; the intention is that\nthe software should warn if this expiry approaches.\n\nSuggested-by: Fabrice Drouin \u003cfabrice.drouin at acinq.fr\u003e\nSigned-off-by: Rusty Russell \u003crusty at rustcorp.com.au\u003e\n\ndiff --git a/README.md b/README.md\nindex 3039333..6da4150 100644\n--- a/README.md\n+++ b/README.md\n@@ -11,6 +11,7 @@ Human readable part:\n \n And data part:\n 1. Version: 0 (5 bits)\n+1. UTC timestamp in seconds-since-Unix-epoch (32 bits)\n 1. Payment hash (256 bits)\n 1. Zero or more tagged parts.\n 1. Signature (bitcoin-style, of SHA256(SHA256(), plus recovery byte) of above. (520 bits)\n@@ -25,6 +26,7 @@ Currently defined tagged parts are:\n 1. h: description of purpose of payment (SHA256). This is used to commit\n to an associated description which is too long to fit, such as may\n be contained in a web page.\n+1. x: expiry time in seconds. Default is 3600 (1 hour) if not specified.\n 1. f: fallback onchain-address. 20 bytes == p2pkh. 21 bytes == p2wpkh, 33 bytes == p2wsh.\n 1. r: extra routing information. This should be appended to the route\n to allow routing to non-public nodes; there may be more\ndiff --git a/examples.sh b/examples.sh\nindex 78c2be8..f526223 100755\n--- a/examples.sh\n+++ b/examples.sh\n@@ -11,8 +11,8 @@ echo \"# Please send 10 satoshi using rhash $RHASH to me @$PUBKEY\"\n ./lightning-address.py encode 10000 $RHASH $PRIVKEY\n echo\n \n-echo \"# Please send \\$3 for a cup of coffee to the same peer\"\n-./lightning-address.py encode --description='1 cup coffee' $((3 * 100000000000 / $CONVERSION_RATE)) $RHASH $PRIVKEY\n+echo \"# Please send \\$3 for a cup of coffee to the same peer, within 1 minute\"\n+./lightning-address.py encode --description='1 cup coffee' $((3 * 100000000000 / $CONVERSION_RATE)) --expires=60 $RHASH $PRIVKEY\n echo\n \n echo \"# Now send \\$24 for an entire list of things (hashed)\"\ndiff --git a/lightning-address.py b/lightning-address.py\nindex c43d020..7b72314 100755\n--- a/lightning-address.py\n+++ b/lightning-address.py\n@@ -3,6 +3,7 @@ import argparse\n import hashlib\n import re\n import sys\n+import time\n # Try 'pip3 install secp256k1'\n import secp256k1\n \n@@ -138,9 +139,24 @@ def u32list(val):\n assert val \u003c (1 \u003c\u003c 32)\n return bytearray([(val \u003e\u003e 24) \u0026 0xff, (val \u003e\u003e 16) \u0026 0xff, (val \u003e\u003e 8) \u0026 0xff, val \u0026 0xff])\n \n+# Represent big-endian number with as many bytes as it takes.\n+def varlist(val):\n+ b = bytearray()\n+ while val != 0:\n+ b.append(val \u0026 0xFF)\n+ val = val // 256\n+ b.reverse()\n+ return b\n+\n def from_u32list(l):\n return (l[0] \u003c\u003c 24) + (l[1] \u003c\u003c 16) + (l[2] \u003c\u003c 8) + l[3]\n \n+def from_varlist(l):\n+ total = 0\n+ for v in l:\n+ total = total * 256 + v\n+ return total\n+\n def tagged(char, l):\n bits=convertbits(l, 8, 5)\n assert len(bits) \u003c (1 \u003c\u003c 10)\n@@ -169,9 +185,12 @@ def lnencode(options):\n \n hrp = 'ln' + options.currency + amount\n \n- # version + paymenthash\n- data = [0] + convertbits(bytearray.fromhex(options.paymenthash), 8, 5)\n+ # version + timestamp + paymenthash\n+ now = int(time.time())\n+ assert len(u32list(now) + bytearray.fromhex(options.paymenthash)) == 4 + 32\n+ data = [0] + convertbits(u32list(now) + bytearray.fromhex(options.paymenthash), 8, 5)\n \n+\n for r in options.route:\n pubkey,channel,fee,cltv = r.split('/')\n route = bytearray.fromhex(pubkey) + bytearray.fromhex(channel) + u32list(int(fee)) + u32list(int(cltv))\n@@ -183,7 +202,10 @@ def lnencode(options):\n \n if options.description:\n data = data + tagged('d', [ord(c) for c in options.description])\n- \n+\n+ if options.expires:\n+ data = data + tagged('x', varlist(options.expires))\n+ \n if options.description_hashed:\n data = data + tagged('h', hashlib.sha256(options.description_hashed.encode('utf-8')).digest())\n \n@@ -239,14 +261,16 @@ def lndecode(options):\n if options.rate:\n print(\"(Conversion: {})\".format(amount / 10**11 * float(options.rate)))\n \n- # 32 bytes turns into 52 bytes when base32 encoded.\n- if len(data) \u003c 52:\n- sys.exit(\"Not long enough to contain payment hash\")\n+ # 4 + 32 bytes turns into 58 bytes when base32 encoded.\n+ if len(data) \u003c 58:\n+ sys.exit(\"Not long enough to contain timestamp and payment hash\")\n \n- decoded = convertbits(data[:52], 5, 8, False)\n- data = data[52:]\n- assert len(decoded) == 32\n- print(\"Payment hash: {}\".format(bytearray(decoded).hex()))\n+ decoded = convertbits(data[:58], 5, 8, False)\n+ data = data[58:]\n+ assert len(decoded) == 4 + 32\n+ tstamp = from_u32list(decoded[0:4])\n+ print(\"Timestamp: {} ({})\".format(tstamp, time.ctime(tstamp)))\n+ print(\"Payment hash: {}\".format(bytearray(decoded[4:]).hex()))\n \n while len(data) \u003e 0:\n tag,tagdata,data = pull_tagged(data)\n@@ -265,6 +289,8 @@ def lndecode(options):\n print(\"Description: {}\".format(''.join(chr(c) for c in tagdata)))\n elif tag == 'h':\n print(\"Description hash: {}\".format(bytearray(tagdata).hex()))\n+ elif tag == 'x':\n+ print(\"Expiry (seconds): {}\".format(from_varlist(tagdata)))\n else:\n print(\"UNKNOWN TAG {}: {}\".format(tag, bytearray(tagdata).hex()))\n \n@@ -286,6 +312,8 @@ parser_enc.add_argument('--description',\n help='What is being purchased')\n parser_enc.add_argument('--description-hashed',\n help='What is being purchased (for hashing)')\n+parser_enc.add_argument('--expires', type=int,\n+ help='Seconds before offer expires')\n parser_enc.add_argument('amount', type=int, help='Amount in millisatoshi')\n parser_enc.add_argument('paymenthash', help='Payment hash (in hex)')\n parser_enc.add_argument('privkey', help='Private key (in hex)')",
"sig": "bc55e9eb7e1ec8405223c0a5b567b4ca75c0b65ed66d4425d2c8413872440a52db9d2b20d4dad5083c21c7001280fd1ef4ea068393801112a1fba95d52dad4d3"
}