offers: signatures are now optional.

As per latest spec revision.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-EXPERIMENTAL: BOLT12 offers can now be unsigned, for really short QR codes.
This commit is contained in:
Rusty Russell 2021-07-02 09:41:34 +09:30
parent 6336d39a95
commit f9fe814ea3
20 changed files with 130 additions and 61 deletions

View file

@ -166,27 +166,6 @@ struct tlv_offer *offer_decode(const tal_t *ctx,
const struct feature_set *our_features,
const struct chainparams *must_be_chain,
char **fail)
{
struct tlv_offer *offer;
offer = offer_decode_nosig(ctx, b12, b12len,
our_features, must_be_chain, fail);
if (offer) {
*fail = check_signature(ctx, offer->fields,
"offer", "signature",
offer->node_id, offer->signature);
if (*fail)
offer = tal_free(offer);
}
return offer;
}
struct tlv_offer *offer_decode_nosig(const tal_t *ctx,
const char *b12, size_t b12len,
const struct feature_set *our_features,
const struct chainparams *must_be_chain,
char **fail)
{
struct tlv_offer *offer = tlv_offer_new(ctx);
const u8 *data;
@ -208,6 +187,19 @@ struct tlv_offer *offer_decode_nosig(const tal_t *ctx,
if (*fail)
return tal_free(offer);
/* BOLT-offers #12:
* - if `signature` is present, but is not a valid signature using
* `node_id` as described in [Signature Calculation](#signature-calculation):
* - MUST NOT respond to the offer.
*/
if (offer->signature) {
*fail = check_signature(ctx, offer->fields,
"offer", "signature",
offer->node_id, offer->signature);
if (*fail)
return tal_free(offer);
}
return offer;
}

View file

@ -33,20 +33,13 @@ char *offer_encode(const tal_t *ctx, const struct tlv_offer *bolt12_tlv);
* @must_be_chain: if non-NULL, chain to enforce.
* @fail: pointer to descriptive error string, set if this returns NULL.
*
* Note: checks signature!
* Note: checks signature if present.
*/
struct tlv_offer *offer_decode(const tal_t *ctx, const char *b12, size_t b12len,
const struct feature_set *our_features,
const struct chainparams *must_be_chain,
char **fail);
/* Variant which does not check signature */
struct tlv_offer *offer_decode_nosig(const tal_t *ctx,
const char *b12, size_t b12len,
const struct feature_set *our_features,
const struct chainparams *must_be_chain,
char **fail);
/**
* invrequest_encode - encode this complete bolt12 invreq TLV into text.
*/

View file

@ -481,8 +481,8 @@ int main(int argc, char *argv[])
if (streq(hrp, "lno")) {
const struct tlv_offer *offer
= offer_decode_nosig(ctx, argv[2], strlen(argv[2]),
NULL, NULL, &fail);
= offer_decode(ctx, argv[2], strlen(argv[2]),
NULL, NULL, &fail);
if (!offer)
errx(ERROR_BAD_DECODE, "Bad offer: %s", fail);
@ -517,7 +517,7 @@ int main(int argc, char *argv[])
print_features(offer->features);
if (offer->paths)
print_blindedpaths(offer->paths, NULL);
if (must_have(offer, signature) && offer->node_id)
if (offer->signature && offer->node_id)
well_formed &= print_signature("offer", "signature",
offer->fields,
offer->node_id,

View file

@ -35,10 +35,10 @@ If \fBtype\fR is "bolt12 offer", and \fBvalid\fR is \fItrue\fR:
.IP \[bu]
\fBnode_id\fR (pubkey32): x-only public key of the offering node
.IP \[bu]
\fBsignature\fR (bip340sig): BIP-340 signature of the \fInode_id\fR on this offer
.IP \[bu]
\fBdescription\fR (string): the description of the purpose of the offer
.IP \[bu]
\fBsignature\fR (bip340sig, optional): BIP-340 signature of the \fInode_id\fR on this offer
.IP \[bu]
\fBchains\fR (array of hexs, optional): which blockchains this offer is for (missing implies bitcoin mainnet only):
.RS
.IP \[bu]
@ -414,4 +414,4 @@ Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:045cd00ad893483df132bdffd913049bfe43acb152a511accc9f17f87ba06a8d
\" SHA256STAMP:cd54af7c631f06b3db72848cdf90951ceb14d89b8bca981dba69244cd2ddbae5

View file

@ -27,8 +27,8 @@ On success, an object is returned, containing:
If **type** is "bolt12 offer", and **valid** is *true*:
- **offer_id** (hex): the id of this offer (merkle hash of non-signature fields) (always 64 characters)
- **node_id** (pubkey32): x-only public key of the offering node
- **signature** (bip340sig): BIP-340 signature of the *node_id* on this offer
- **description** (string): the description of the purpose of the offer
- **signature** (bip340sig, optional): BIP-340 signature of the *node_id* on this offer
- **chains** (array of hexs, optional): which blockchains this offer is for (missing implies bitcoin mainnet only):
- the genesis blockhash (always 64 characters)
- **currency** (string, optional): ISO 4217 code of the currency (missing implies Bitcoin) (always 3 characters)
@ -183,4 +183,4 @@ RESOURCES
Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:f0adee97f3b5776059252703efee1b8e244c1141f9f3dd5fe73e3d7ed4d59ab4)
[comment]: # ( SHA256STAMP:8ca0b9178b8ea6575cd80291001263dc27f721664648086a7c1a02efcb545ee7)

View file

@ -48,6 +48,8 @@ On success, an object is returned, containing:
.IP \[bu]
\fBbolt12\fR (string): The bolt12 string representing this offer
.IP \[bu]
\fBbolt12_unsigned\fR (string): The bolt12 string representing this offer, without signature
.IP \[bu]
\fBused\fR (boolean): Whether the offer has had an invoice paid / payment made
.IP \[bu]
\fBlabel\fR (string, optional): The label provided when offer was created
@ -78,4 +80,4 @@ Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:03b1a12409ad02eca1543d99bafdfbd2d7d2c869e182fba513e3f93fb48a7664
\" SHA256STAMP:2383f44a3bcc1023d6619f5ead4c27d7b2bd9a52bd64d00142fd26658a49dd32

View file

@ -40,6 +40,7 @@ On success, an object is returned, containing:
- **active** (boolean): Whether the offer can produce invoices/payments (always "false")
- **single_use** (boolean): Whether the offer is disabled after first successful use
- **bolt12** (string): The bolt12 string representing this offer
- **bolt12_unsigned** (string): The bolt12 string representing this offer, without signature
- **used** (boolean): Whether the offer has had an invoice paid / payment made
- **label** (string, optional): The label provided when offer was created
[comment]: # (GENERATE-FROM-SCHEMA-END)
@ -72,4 +73,4 @@ RESOURCES
---------
Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:abf340bf35dcefd42fba609b3ae95adb2f74bb5766c68e174a9b8f9114c80202)
[comment]: # ( SHA256STAMP:5b96eca3e35f6c556b93db1743c617b59e69058c9421ece9cc99a9c8814c176b)

View file

@ -41,6 +41,8 @@ On success, an object containing \fBoffers\fR is returned\. It is an array of o
.IP \[bu]
\fBbolt12\fR (string): the bolt12 encoding of the offer
.IP \[bu]
\fBbolt12_unsigned\fR (string): the bolt12 encoding of the offer, without signature
.IP \[bu]
\fBused\fR (boolean): True if an associated invoice has been paid
.IP \[bu]
\fBlabel\fR (string, optional): the (optional) user-specified label
@ -82,4 +84,4 @@ Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:7e359219084e648a629b0d43774db17bbfbe693074b817fa5890b7c8bccd1429
\" SHA256STAMP:bf83e52cc56aaef876eb1039a6fbbcd9d2e15703c54f33163176c7ba41523261

View file

@ -35,6 +35,7 @@ On success, an object containing **offers** is returned. It is an array of obje
- **active** (boolean): whether this can still be used
- **single_use** (boolean): whether this expires as soon as it's paid
- **bolt12** (string): the bolt12 encoding of the offer
- **bolt12_unsigned** (string): the bolt12 encoding of the offer, without signature
- **used** (boolean): True if an associated invoice has been paid
- **label** (string, optional): the (optional) user-specified label
[comment]: # (GENERATE-FROM-SCHEMA-END)
@ -78,4 +79,4 @@ RESOURCES
---------
Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:58e9f5aa5808e19e3be151b1c1f1215ec23953b60ab294418aa57426c5bcbd46)
[comment]: # ( SHA256STAMP:e2b8508c98e6161c45fca91bceb273e6c2865cec0e1761dde85f8f2dd6670491)

9
doc/lightning-offer.7 generated
View file

@ -15,6 +15,11 @@ creating one or more invoices\. It automatically enables the processing of
an incoming invoice_request, and issuing of invoices\.
Note that it creates two variants of the offer: a signed and an
unsigned one (which is smaller)\. Wallets should accept both: the
current specification allows either\.
The \fIamount\fR parameter can be the string "any", which creates an offer
that can be paid with any amount (e\.g\. a donation)\. Otherwise it can
be a positive value in millisatoshi precision; it can be a whole
@ -112,6 +117,8 @@ On success, an object is returned, containing:
.IP \[bu]
\fBbolt12\fR (string): the bolt12 encoding of the offer
.IP \[bu]
\fBbolt12_unsigned\fR (string): the bolt12 encoding of the offer, without a signature
.IP \[bu]
\fBused\fR (boolean): True if an associated invoice has been paid (always \fIfalse\fR)
.IP \[bu]
\fBlabel\fR (string, optional): the (optional) user-specified label
@ -145,4 +152,4 @@ Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:fdc65b544ee660ef7afafb13bc727b859411df072739c4b22973fbc870d785f2
\" SHA256STAMP:f7faf86c7cb052200c3b4d6e3d6209afa54600cc7a33e1082583a2766d02a275

View file

@ -15,6 +15,10 @@ The **offer** RPC command creates an offer, which is a precursor to
creating one or more invoices. It automatically enables the processing of
an incoming invoice_request, and issuing of invoices.
Note that it creates two variants of the offer: a signed and an
unsigned one (which is smaller). Wallets should accept both: the
current specification allows either.
The *amount* parameter can be the string "any", which creates an offer
that can be paid with any amount (e.g. a donation). Otherwise it can
be a positive value in millisatoshi precision; it can be a whole
@ -95,6 +99,7 @@ On success, an object is returned, containing:
- **active** (boolean): whether this can still be used (always *true*)
- **single_use** (boolean): whether this expires as soon as it's paid (reflects the *single_use* parameter)
- **bolt12** (string): the bolt12 encoding of the offer
- **bolt12_unsigned** (string): the bolt12 encoding of the offer, without a signature
- **used** (boolean): True if an associated invoice has been paid (always *false*)
- **label** (string, optional): the (optional) user-specified label
[comment]: # (GENERATE-FROM-SCHEMA-END)
@ -123,4 +128,4 @@ RESOURCES
Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:20c0be7bad73fcf71ccae61c2c5a112c8602216d9d2e9f647f8273fdf4e3ed8b)
[comment]: # ( SHA256STAMP:0f9cfd3cc68aaba20af0eee763c93b475016619d960e3f5bbc0b762a809f0fef)

View file

@ -17,6 +17,11 @@ offer)\. It automatically enables the accepting and payment of
corresponding invoice message (we will only pay once, however!)\.
Note that it creates two variants of the offer: a signed and an
unsigned one (which is smaller)\. Wallets should accept both: the
current specification allows either\.
The \fIamount\fR parameter can be the string "any", which creates an offer
that can be paid with any amount (e\.g\. a donation)\. Otherwise it can
be a positive value in millisatoshi precision; it can be a whole
@ -64,6 +69,8 @@ On success, an object is returned, containing:
.IP \[bu]
\fBbolt12\fR (string): the bolt12 encoding of the offer
.IP \[bu]
\fBbolt12_unsigned\fR (string): the bolt12 encoding of the offer, without a signature
.IP \[bu]
\fBused\fR (boolean): True if an incoming invoice has been paid (always \fIfalse\fR)
.IP \[bu]
\fBlabel\fR (string, optional): the (optional) user-specified label
@ -106,4 +113,4 @@ Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:823219aff5dc06ab3b810442048b6cf733210c3eae80567327dc396e5f7987c8
\" SHA256STAMP:3de11c6de7905322d9ef748981fc1d4f9ca91f4be46d76af6e9124572853047d

View file

@ -18,6 +18,10 @@ send an invoice for us to pay (technically, this is referred to as a
offer). It automatically enables the accepting and payment of
corresponding invoice message (we will only pay once, however!).
Note that it creates two variants of the offer: a signed and an
unsigned one (which is smaller). Wallets should accept both: the
current specification allows either.
The *amount* parameter can be the string "any", which creates an offer
that can be paid with any amount (e.g. a donation). Otherwise it can
be a positive value in millisatoshi precision; it can be a whole
@ -55,6 +59,7 @@ On success, an object is returned, containing:
- **active** (boolean): whether this will pay a matching incoming invoice (always *true*)
- **single_use** (boolean): whether this expires as soon as it's paid out (always *true*)
- **bolt12** (string): the bolt12 encoding of the offer
- **bolt12_unsigned** (string): the bolt12 encoding of the offer, without a signature
- **used** (boolean): True if an incoming invoice has been paid (always *false*)
- **label** (string, optional): the (optional) user-specified label
[comment]: # (GENERATE-FROM-SCHEMA-END)
@ -92,4 +97,4 @@ RESOURCES
Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:6db3fdba07f376e697326b3bf1bd74c013084a459cb9f4fe76d23fce58bd58fe)
[comment]: # ( SHA256STAMP:2b7e7b543a88a10dbfbca2508e034af79f43ed0845abdb9df1fdf7e28ee33c26)

View file

@ -28,7 +28,7 @@
}
},
"then": {
"required": [ "offer_id", "node_id", "signature", "description" ],
"required": [ "offer_id", "node_id", "description" ],
"additionalProperties": false,
"properties": {
"type": { },

View file

@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [ "offer_id", "active", "single_use", "bolt12", "used" ],
"required": [ "offer_id", "active", "single_use", "bolt12", "bolt12_unsigned", "used" ],
"additionalProperties": false,
"properties": {
"offer_id": {
@ -23,6 +23,10 @@
"type": "string",
"description": "The bolt12 string representing this offer"
},
"bolt12_unsigned": {
"type": "string",
"description": "The bolt12 string representing this offer, without signature"
},
"used": {
"type": "boolean",
"description": "Whether the offer has had an invoice paid / payment made"

View file

@ -9,7 +9,7 @@
"items": {
"type": "object",
"additionalProperties": false,
"required": [ "offer_id", "active", "single_use", "bolt12", "used" ],
"required": [ "offer_id", "active", "single_use", "bolt12", "bolt12_unsigned", "used" ],
"properties": {
"offer_id": {
"type": "hex",
@ -29,6 +29,10 @@
"type": "string",
"description": "the bolt12 encoding of the offer"
},
"bolt12_unsigned": {
"type": "string",
"description": "the bolt12 encoding of the offer, without signature"
},
"used": {
"type": "boolean",
"description": "True if an associated invoice has been paid"

View file

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [ "offer_id", "active", "single_use", "bolt12", "used" ],
"required": [ "offer_id", "active", "single_use", "bolt12", "bolt12_unsigned", "used" ],
"properties": {
"offer_id": {
"type": "hex",
@ -23,6 +23,10 @@
"type": "string",
"description": "the bolt12 encoding of the offer"
},
"bolt12_unsigned": {
"type": "string",
"description": "the bolt12 encoding of the offer, without a signature"
},
"used": {
"type": "boolean",
"enum": [ false ],

View file

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [ "offer_id", "active", "single_use", "bolt12", "used" ],
"required": [ "offer_id", "active", "single_use", "bolt12", "bolt12_unsigned", "used" ],
"properties": {
"offer_id": {
"type": "hex",
@ -24,6 +24,10 @@
"type": "string",
"description": "the bolt12 encoding of the offer"
},
"bolt12_unsigned": {
"type": "string",
"description": "the bolt12 encoding of the offer, without a signature"
},
"used": {
"type": "boolean",
"enum": [ false ],

View file

@ -14,6 +14,7 @@
static void json_populate_offer(struct json_stream *response,
const struct sha256 *offer_id,
const char *b12,
const char *b12_nosig,
const struct json_escape *label,
enum offer_status status)
{
@ -21,6 +22,8 @@ static void json_populate_offer(struct json_stream *response,
json_add_bool(response, "active", offer_status_active(status));
json_add_bool(response, "single_use", offer_status_single(status));
json_add_string(response, "bolt12", b12);
if (b12_nosig)
json_add_string(response, "bolt12_unsigned", b12_nosig);
json_add_bool(response, "used", offer_status_used(status));
if (label)
json_add_escaped_string(response, "label", label);
@ -33,9 +36,9 @@ static struct command_result *param_b12_offer(struct command *cmd,
struct tlv_offer **offer)
{
char *fail;
*offer = offer_decode_nosig(cmd, buffer + tok->start,
tok->end - tok->start,
cmd->ld->our_features, chainparams, &fail);
*offer = offer_decode(cmd, buffer + tok->start,
tok->end - tok->start,
cmd->ld->our_features, chainparams, &fail);
if (!*offer)
return command_fail_badparam(cmd, name, buffer, tok, fail);
if ((*offer)->signature)
@ -83,7 +86,7 @@ static struct command_result *json_createoffer(struct command *cmd,
struct json_escape *label;
struct tlv_offer *offer;
struct sha256 merkle;
const char *b12str;
const char *b12str, *b12str_nosig;
bool *single_use;
enum offer_status status;
struct pubkey32 key;
@ -112,9 +115,11 @@ static struct command_result *json_createoffer(struct command *cmd,
OFFER_ALREADY_EXISTS,
"Duplicate offer");
}
offer->signature = tal_free(offer->signature);
b12str_nosig = offer_encode(cmd, offer);
response = json_stream_success(cmd);
json_populate_offer(response, &merkle, b12str, label, status);
json_populate_offer(response, &merkle, b12str, b12str_nosig, label, status);
return command_success(cmd, response);
}
@ -126,6 +131,25 @@ static const struct json_command createoffer_command = {
};
AUTODATA(json_command, &createoffer_command);
/* We store strings in the db, so removing signatures is easiest by conversion */
static const char *offer_str_nosig(const tal_t *ctx,
struct lightningd *ld,
const char *b12str)
{
char *fail;
struct tlv_offer *offer = offer_decode(tmpctx, b12str, strlen(b12str),
ld->our_features, chainparams,
&fail);
if (!offer) {
log_broken(ld->log, "Cannot reparse offerstr from db %s: %s",
b12str, fail);
return NULL;
}
offer->signature = tal_free(offer->signature);
return offer_encode(ctx, offer);
}
static struct command_result *json_listoffers(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
@ -153,7 +177,9 @@ static struct command_result *json_listoffers(struct command *cmd,
if (b12 && offer_status_active(status) >= *active_only) {
json_object_start(response, NULL);
json_populate_offer(response,
offer_id, b12, label, status);
offer_id, b12,
offer_str_nosig(tmpctx, cmd->ld, b12),
label, status);
json_object_end(response);
}
} else {
@ -168,7 +194,10 @@ static struct command_result *json_listoffers(struct command *cmd,
if (offer_status_active(status) >= *active_only) {
json_object_start(response, NULL);
json_populate_offer(response,
&id, b12, label, status);
&id, b12,
offer_str_nosig(tmpctx,
cmd->ld, b12),
label, status);
json_object_end(response);
}
}
@ -213,7 +242,10 @@ static struct command_result *json_disableoffer(struct command *cmd,
status = wallet_offer_disable(wallet, offer_id, status);
response = json_stream_success(cmd);
json_populate_offer(response, offer_id, b12, label, status);
json_populate_offer(response, offer_id, b12,
offer_str_nosig(tmpctx,
cmd->ld, b12),
label, status);
return command_success(cmd, response);
}

View file

@ -3823,6 +3823,7 @@ def test_offer(node_factory, bitcoind):
offer = only_one(l1.rpc.call('listoffers', [ret['offer_id']])['offers'])
assert offer['bolt12'] == ret['bolt12']
assert offer['bolt12_unsigned'] == ret['bolt12_unsigned']
assert offer['offer_id'] == ret['offer_id']
output = subprocess.check_output([bolt12tool, 'decode',
@ -3831,6 +3832,12 @@ def test_offer(node_factory, bitcoind):
assert 'amount' not in output
else:
assert 'amount' in output
output = subprocess.check_output([bolt12tool, 'decode',
offer['bolt12_unsigned']]).decode('ASCII')
if amount == 'any':
assert 'amount' not in output
else:
assert 'amount' in output
# Try wrong amount precision:
with pytest.raises(RpcError, match='Currency AUD requires 2 minor units'):
@ -3985,7 +3992,7 @@ def test_fetchinvoice(node_factory, bitcoind):
'description': 'simple test'})
inv1 = l1.rpc.call('fetchinvoice', {'offer': offer1['bolt12']})
inv2 = l1.rpc.call('fetchinvoice', {'offer': offer1['bolt12']})
inv2 = l1.rpc.call('fetchinvoice', {'offer': offer1['bolt12_unsigned']})
assert inv1 != inv2
assert 'next_period' not in inv1
assert 'next_period' not in inv2
@ -4244,9 +4251,8 @@ def test_sendinvoice(node_factory, bitcoind):
assert only_one(l1.rpc.call('listoffers', [offer['offer_id']])['offers'])['used'] is False
# sendinvoice should work.
out = l2.rpc.call('sendinvoice', {'offer': offer['bolt12'],
out = l2.rpc.call('sendinvoice', {'offer': offer['bolt12_unsigned'],
'label': 'test sendinvoice 1'})
print(out)
assert out['label'] == 'test sendinvoice 1'
assert out['description'] == 'simple test'
assert 'bolt12' in out