mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-01-18 21:35:11 +01:00
bolt11: support payment_metadata.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
2526e804f7
commit
e01abf0b34
2
Makefile
2
Makefile
@ -24,7 +24,7 @@ CCANDIR := ccan
|
||||
|
||||
# Where we keep the BOLT RFCs
|
||||
BOLTDIR := ../lightning-rfc/
|
||||
DEFAULT_BOLTVERSION := 93909f67f6a48ee3f155a6224c182e612dd5f187
|
||||
DEFAULT_BOLTVERSION := f6c4d7604150986894bcb46d67c5c88680740b12
|
||||
# Can be overridden on cmdline.
|
||||
BOLTVERSION := $(DEFAULT_BOLTVERSION)
|
||||
|
||||
|
@ -516,6 +516,33 @@ static char *decode_9(struct bolt11 *b11,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* BOLT #11:
|
||||
*
|
||||
* `m` (27): `data_length` variable. Additional metadata to attach to
|
||||
* the payment. Note that the size of this field is limited by the
|
||||
* maximum hop payload size. Long metadata fields reduce the maximum
|
||||
* route length.
|
||||
*/
|
||||
static char *decode_m(struct bolt11 *b11,
|
||||
struct hash_u5 *hu5,
|
||||
u5 **data, size_t *data_len,
|
||||
size_t data_length,
|
||||
bool *have_m)
|
||||
{
|
||||
size_t mlen = (data_length * 5) / 8;
|
||||
|
||||
if (*have_m)
|
||||
return unknown_field(b11, hu5, data, data_len, 'm',
|
||||
data_length);
|
||||
|
||||
b11->metadata = tal_arr(b11, u8, mlen);
|
||||
pull_bits_certain(hu5, data, data_len, b11->metadata,
|
||||
data_length * 5, false);
|
||||
|
||||
*have_m = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct bolt11 *new_bolt11(const tal_t *ctx,
|
||||
const struct amount_msat *msat TAKES)
|
||||
{
|
||||
@ -535,6 +562,7 @@ struct bolt11 *new_bolt11(const tal_t *ctx,
|
||||
*/
|
||||
b11->min_final_cltv_expiry = 18;
|
||||
b11->payment_secret = NULL;
|
||||
b11->metadata = NULL;
|
||||
|
||||
if (msat)
|
||||
b11->msat = tal_dup(b11, struct amount_msat, msat);
|
||||
@ -557,7 +585,7 @@ struct bolt11 *bolt11_decode_nosig(const tal_t *ctx, const char *str,
|
||||
struct bolt11 *b11 = new_bolt11(ctx, NULL);
|
||||
struct hash_u5 hu5;
|
||||
bool have_p = false, have_d = false, have_h = false,
|
||||
have_x = false, have_c = false, have_s = false;
|
||||
have_x = false, have_c = false, have_s = false, have_m = false;
|
||||
|
||||
*have_n = false;
|
||||
b11->routes = tal_arr(b11, struct route_info *, 0);
|
||||
@ -760,6 +788,10 @@ struct bolt11 *bolt11_decode_nosig(const tal_t *ctx, const char *str,
|
||||
problem = decode_s(b11, &hu5, &data, &data_len,
|
||||
data_length, &have_s);
|
||||
break;
|
||||
case 'm':
|
||||
problem = decode_m(b11, &hu5, &data, &data_len,
|
||||
data_length, &have_m);
|
||||
break;
|
||||
default:
|
||||
unknown_field(b11, &hu5, &data, &data_len,
|
||||
bech32_charset[type], data_length);
|
||||
@ -936,6 +968,11 @@ static void encode_p(u5 **data, const struct sha256 *hash)
|
||||
push_field(data, 'p', hash, 256);
|
||||
}
|
||||
|
||||
static void encode_m(u5 **data, const u8 *metadata)
|
||||
{
|
||||
push_field(data, 'm', metadata, tal_bytelen(metadata) * CHAR_BIT);
|
||||
}
|
||||
|
||||
static void encode_d(u5 **data, const char *description)
|
||||
{
|
||||
push_field(data, 'd', description, strlen(description) * CHAR_BIT);
|
||||
@ -1011,13 +1048,20 @@ static void encode_r(u5 **data, const struct route_info *r)
|
||||
tal_free(rinfo);
|
||||
}
|
||||
|
||||
static void maybe_encode_9(u5 **data, const u8 *features)
|
||||
static void maybe_encode_9(u5 **data, const u8 *features,
|
||||
bool have_payment_metadata)
|
||||
{
|
||||
u5 *f5 = tal_arr(NULL, u5, 0);
|
||||
|
||||
for (size_t i = 0; i < tal_count(features) * CHAR_BIT; i++) {
|
||||
if (!feature_is_set(features, i))
|
||||
continue;
|
||||
|
||||
/* Don't set option_payment_metadata unless we acually use it */
|
||||
if (!have_payment_metadata
|
||||
&& COMPULSORY_FEATURE(i) == OPT_PAYMENT_METADATA)
|
||||
continue;
|
||||
|
||||
/* We expand it out so it makes a BE 5-bit/btye bitfield */
|
||||
set_feature_bit(&f5, (i / 5) * 8 + (i % 5));
|
||||
}
|
||||
@ -1136,6 +1180,9 @@ char *bolt11_encode_(const tal_t *ctx,
|
||||
if (b11->expiry != DEFAULT_X)
|
||||
encode_x(&data, b11->expiry);
|
||||
|
||||
if (b11->metadata)
|
||||
encode_m(&data, b11->metadata);
|
||||
|
||||
/* BOLT #11:
|
||||
* - MUST include one `c` field (`min_final_cltv_expiry`).
|
||||
*/
|
||||
@ -1150,7 +1197,7 @@ char *bolt11_encode_(const tal_t *ctx,
|
||||
for (size_t i = 0; i < tal_count(b11->routes); i++)
|
||||
encode_r(&data, b11->routes[i]);
|
||||
|
||||
maybe_encode_9(&data, b11->features);
|
||||
maybe_encode_9(&data, b11->features, b11->metadata != NULL);
|
||||
|
||||
list_for_each(&b11->extra_fields, extra, list)
|
||||
if (!encode_extra(&data, extra))
|
||||
|
@ -76,6 +76,9 @@ struct bolt11 {
|
||||
/* Features bitmap, if any. */
|
||||
u8 *features;
|
||||
|
||||
/* Optional metadata to send with payment. */
|
||||
u8 *metadata;
|
||||
|
||||
struct list_head extra_fields;
|
||||
};
|
||||
|
||||
|
@ -66,6 +66,8 @@ void json_add_bolt11(struct json_stream *response,
|
||||
b11->payment_secret);
|
||||
if (b11->features)
|
||||
json_add_hex_talarr(response, "features", b11->features);
|
||||
if (b11->metadata)
|
||||
json_add_hex_talarr(response, "payment_metadata", b11->metadata);
|
||||
if (tal_count(b11->fallbacks)) {
|
||||
json_array_start(response, "fallbacks");
|
||||
for (size_t i = 0; i < tal_count(b11->fallbacks); i++)
|
||||
|
@ -97,6 +97,14 @@ static const struct feature_style feature_styles[] = {
|
||||
.copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT,
|
||||
[NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT,
|
||||
[CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT } },
|
||||
{ OPT_PAYMENT_METADATA,
|
||||
.copy_style = { [INIT_FEATURE] = FEATURE_DONT_REPRESENT,
|
||||
[NODE_ANNOUNCE_FEATURE] = FEATURE_DONT_REPRESENT,
|
||||
/* Note: we don't actually set this in invoices, since
|
||||
* we don't need to use it, but if we don't set it here
|
||||
* we refuse to parse it. */
|
||||
[BOLT11_FEATURE] = FEATURE_REPRESENT,
|
||||
[CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT } },
|
||||
};
|
||||
|
||||
struct dependency {
|
||||
@ -416,7 +424,7 @@ const char *feature_name(const tal_t *ctx, size_t f)
|
||||
"option_provide_peer_backup", /* https://github.com/lightningnetwork/lightning-rfc/pull/881 */
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
"option_payment_metadata",
|
||||
NULL, /* 50/51 */
|
||||
NULL,
|
||||
"option_keysend",
|
||||
|
@ -119,6 +119,7 @@ const char *fmt_featurebits(const tal_t *ctx, const u8 *featurebits);
|
||||
* | 20/21 | `option_anchor_outputs` |... IN ...
|
||||
* | 22/23 | `option_anchors_zero_fee_htlc_tx` |... IN ...
|
||||
* | 26/27 | `option_shutdown_anysegwit` |... IN ...
|
||||
* | 48/49 | `option_payment_metadata` |... 9 ...
|
||||
*/
|
||||
#define OPT_PAYMENT_SECRET 14
|
||||
#define OPT_BASIC_MPP 16
|
||||
@ -126,6 +127,7 @@ const char *fmt_featurebits(const tal_t *ctx, const u8 *featurebits);
|
||||
#define OPT_ANCHOR_OUTPUTS 20
|
||||
#define OPT_ANCHORS_ZERO_FEE_HTLC_TX 22
|
||||
#define OPT_SHUTDOWN_ANYSEGWIT 26
|
||||
#define OPT_PAYMENT_METADATA 48
|
||||
|
||||
/* BOLT-f53ca2301232db780843e894f55d95d512f297f9 #9:
|
||||
* | 28/29 | `option_dual_fund` | ... IN9 ...
|
||||
|
@ -564,6 +564,52 @@ int main(int argc, char *argv[])
|
||||
memset(b11->payment_secret, 0x11, sizeof(*b11->payment_secret));
|
||||
test_b11("lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz", b11, NULL);
|
||||
|
||||
/* BOLT #11:
|
||||
* > ### Please send 0.01 BTC with payment metadata 0x01fafaf0
|
||||
* > lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc
|
||||
*
|
||||
* Breakdown:
|
||||
*
|
||||
* * `lnbc`: prefix, Lightning on Bitcoin mainnet
|
||||
* * `10m`: amount (10 milli-bitcoin)
|
||||
* * `1`: Bech32 separator
|
||||
* * `pvjluez`: timestamp (1496314658)
|
||||
* * `p`: payment hash
|
||||
* * `p5`: `data_length` (`p` = 1, `5` = 20; 1 * 32 + 20 == 52)
|
||||
* * `qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq`: payment hash 0001020304050607080900010203040506070809000102030405060708090102
|
||||
* * `d`: short description
|
||||
* * `p9`: `data_length` (`p` = 1, `9` = 5; 1 * 32 + 5 == 37)
|
||||
* * `wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2`: 'payment metadata inside'
|
||||
* * `m`: metadata
|
||||
* * `q8`: `data_length` (`q` = 0, `8` = 7; 0 * 32 + 7 == 7)
|
||||
* * `q8a04uq`: 0x01fafaf0
|
||||
* * `s`: payment secret
|
||||
* * `p5`: `data_length` (`p` = 1, `5` = 20; 1 * 32 + 20 == 52)
|
||||
* * `zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs`: 0x1111111111111111111111111111111111111111111111111111111111111111
|
||||
* * `9`: features
|
||||
* * `q2`: `data_length` (`q` = 0, `2` = 10; 0 * 32 + 10 == 10)
|
||||
* * `gqqqqqqsgq`: [b01000000000000000000000000000000000100000100000000] = 8 + 14 + 48
|
||||
* * `7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqp`: signature
|
||||
* * `d05wjc`: Bech32 checksum
|
||||
*/
|
||||
msatoshi = AMOUNT_MSAT(1000000000);
|
||||
b11 = new_bolt11(tmpctx, &msatoshi);
|
||||
b11->chain = chainparams_for_network("bitcoin");
|
||||
b11->timestamp = 1496314658;
|
||||
if (!hex_decode("0001020304050607080900010203040506070809000102030405060708090102",
|
||||
strlen("0001020304050607080900010203040506070809000102030405060708090102"),
|
||||
&b11->payment_hash, sizeof(b11->payment_hash)))
|
||||
abort();
|
||||
b11->receiver_id = node;
|
||||
b11->description = "payment metadata inside";
|
||||
b11->metadata = tal_hexdata(b11, "01fafaf0", strlen("01fafaf0"));
|
||||
set_feature_bit(&b11->features, 8);
|
||||
set_feature_bit(&b11->features, 14);
|
||||
set_feature_bit(&b11->features, 48);
|
||||
b11->payment_secret = tal(b11, struct secret);
|
||||
memset(b11->payment_secret, 0x11, sizeof(*b11->payment_secret));
|
||||
test_b11("lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc", b11, NULL);
|
||||
|
||||
/* BOLT #11:
|
||||
*
|
||||
* > ### Bech32 checksum is invalid.
|
||||
|
@ -163,6 +163,10 @@ int main(int argc, char *argv[])
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
if (b11->metadata)
|
||||
printf("metadata: %s\n",
|
||||
tal_hex(ctx, b11->metadata));
|
||||
|
||||
list_for_each(&b11->extra_fields, extra, list) {
|
||||
char *data = tal_arr(ctx, char, tal_count(extra->data)+1);
|
||||
|
||||
|
@ -143,6 +143,7 @@ If **type** is "bolt11 invoice", and **valid** is *true*:
|
||||
- **description_hash** (hex, optional): the hash of the description, in place of *description* (always 64 characters)
|
||||
- **payment_secret** (hex, optional): the secret to hand to the payee node (always 64 characters)
|
||||
- **features** (hex, optional): the features bitmap for this invoice
|
||||
- **payment_metadata** (hex, optional): the payment_metadata to put in the payment
|
||||
- **fallbacks** (array of objects, optional): onchain addresses:
|
||||
- **type** (string): the address type (if known) (one of "P2PKH", "P2SH", "P2WPKH", "P2WSH")
|
||||
- **hex** (hex): Raw encoded address
|
||||
@ -180,4 +181,4 @@ RESOURCES
|
||||
|
||||
Main web site: <https://github.com/ElementsProject/lightning>
|
||||
|
||||
[comment]: # ( SHA256STAMP:d05b5fc1bf230b3bbd03e2023fb0c6bbefb700f7c3cfb43512da48dbce45f005)
|
||||
[comment]: # ( SHA256STAMP:6ccf1b9195e64f897f65198a81c47bbd7e16387e4bdc74d624e2ec04a24e9873)
|
||||
|
@ -29,6 +29,7 @@ On success, an object is returned, containing:
|
||||
- **description_hash** (hex, optional): the hash of the description, in place of *description* (always 64 characters)
|
||||
- **payment_secret** (hex, optional): the secret to hand to the payee node (always 64 characters)
|
||||
- **features** (hex, optional): the features bitmap for this invoice
|
||||
- **payment_metadata** (hex, optional): the payment_metadata to put in the payment
|
||||
- **fallbacks** (array of objects, optional): onchain addresses:
|
||||
- **type** (string): the address type (if known) (one of "P2PKH", "P2SH", "P2WPKH", "P2WSH")
|
||||
- **hex** (hex): Raw encoded address
|
||||
@ -69,4 +70,4 @@ RESOURCES
|
||||
|
||||
Main web site: <https://github.com/ElementsProject/lightning>
|
||||
|
||||
[comment]: # ( SHA256STAMP:d92e1197708fff40f8ad71ccec3c0d8122d8088da1803c02bb042b09dbf2ee33)
|
||||
[comment]: # ( SHA256STAMP:17cb6c66c75e907f3a2583d702aec2fc6e5a7b6026d05a3ed9957304799c9aef)
|
||||
|
@ -746,6 +746,8 @@
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {},
|
||||
"valid": {},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"description": "the BIP173 name for the currency"
|
||||
@ -804,6 +806,10 @@
|
||||
"type": "hex",
|
||||
"description": "the features bitmap for this invoice"
|
||||
},
|
||||
"payment_metadata": {
|
||||
"type": "hex",
|
||||
"description": "the payment_metadata to put in the payment"
|
||||
},
|
||||
"fallbacks": {
|
||||
"type": "array",
|
||||
"description": "onchain addresses",
|
||||
|
@ -70,6 +70,10 @@
|
||||
"type": "hex",
|
||||
"description": "the features bitmap for this invoice"
|
||||
},
|
||||
"payment_metadata": {
|
||||
"type": "hex",
|
||||
"description": "the payment_metadata to put in the payment"
|
||||
},
|
||||
"fallbacks": {
|
||||
"type": "array",
|
||||
"description": "onchain addresses",
|
||||
|
@ -823,6 +823,7 @@ static struct feature_set *default_features(const tal_t *ctx)
|
||||
OPTIONAL_FEATURE(OPT_GOSSIP_QUERIES_EX),
|
||||
OPTIONAL_FEATURE(OPT_STATIC_REMOTEKEY),
|
||||
OPTIONAL_FEATURE(OPT_SHUTDOWN_ANYSEGWIT),
|
||||
OPTIONAL_FEATURE(OPT_PAYMENT_METADATA),
|
||||
#if EXPERIMENTAL_FEATURES
|
||||
OPTIONAL_FEATURE(OPT_ANCHOR_OUTPUTS),
|
||||
OPTIONAL_FEATURE(OPT_QUIESCE),
|
||||
|
@ -5236,3 +5236,9 @@ def test_pay_manual_exclude(node_factory, bitcoind):
|
||||
# Exclude direct channel id
|
||||
with pytest.raises(RpcError, match=r'is not reachable directly and all routehints were unusable.'):
|
||||
l2.rpc.pay(inv, exclude=[scid23])
|
||||
|
||||
|
||||
def test_pay_bolt11_metadata(node_factory, bitcoind):
|
||||
l1 = node_factory.get_node()
|
||||
|
||||
l1.rpc.decode('lnbcrt10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqcqpjsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqrk6hdutpaetmm3afjn0vfczgeyv0cy739rr939kwd4h5j3khxcskhgf59eaqy8wyq82tsnaqc5y32ed4jg34jw7rmeva9u6kfhymawgptmy5f6')
|
||||
|
@ -1,9 +1,9 @@
|
||||
--- wire/extracted_onion_wire_csv 2020-03-25 10:24:12.861645774 +1030
|
||||
+++ - 2020-03-26 13:47:13.498294435 +1030
|
||||
@@ -8,6 +8,30 @@
|
||||
tlvtype,tlv_payload,payment_data,8
|
||||
tlvdata,tlv_payload,payment_data,payment_secret,byte,32
|
||||
tlvdata,tlv_payload,payment_data,total_msat,tu64,
|
||||
tlvtype,tlv_payload,payment_metadata,16
|
||||
tlvdata,tlv_payload,payment_metadata,payment_metadata,byte,...
|
||||
+tlvtype,obs2_onionmsg_payload,reply_path,2
|
||||
+tlvdata,obs2_onionmsg_payload,reply_path,first_node_id,point,
|
||||
+tlvdata,obs2_onionmsg_payload,reply_path,blinding,point,
|
||||
|
@ -2,10 +2,12 @@
|
||||
+++ - 2020-03-20 15:11:55.763880895 +1030
|
||||
--- wire/onion_exp_wire.csv.~1~ 2021-11-17 10:56:59.947630815 +1030
|
||||
+++ wire/onion_exp_wire.csv 2021-11-17 10:59:39.304581244 +1030
|
||||
@@ -8,6 +8,10 @@
|
||||
@@ -8,8 +8,12 @@
|
||||
tlvtype,tlv_payload,payment_data,8
|
||||
tlvdata,tlv_payload,payment_data,payment_secret,byte,32
|
||||
tlvdata,tlv_payload,payment_data,total_msat,tu64,
|
||||
tlvtype,tlv_payload,payment_metadata,16
|
||||
tlvdata,tlv_payload,payment_metadata,payment_metadata,byte,...
|
||||
+tlvtype,tlv_payload,encrypted_recipient_data,10
|
||||
+tlvdata,tlv_payload,encrypted_recipient_data,encrypted_data,byte,...
|
||||
+tlvtype,tlv_payload,blinding_point,12
|
||||
|
@ -8,6 +8,8 @@ tlvdata,tlv_payload,short_channel_id,short_channel_id,short_channel_id,
|
||||
tlvtype,tlv_payload,payment_data,8
|
||||
tlvdata,tlv_payload,payment_data,payment_secret,byte,32
|
||||
tlvdata,tlv_payload,payment_data,total_msat,tu64,
|
||||
tlvtype,tlv_payload,payment_metadata,16
|
||||
tlvdata,tlv_payload,payment_metadata,payment_metadata,byte,...
|
||||
tlvtype,obs2_onionmsg_payload,reply_path,2
|
||||
tlvdata,obs2_onionmsg_payload,reply_path,first_node_id,point,
|
||||
tlvdata,obs2_onionmsg_payload,reply_path,blinding,point,
|
||||
|
|
Loading…
Reference in New Issue
Block a user