Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lightningd: Add signinvoice to sign a BOLT11 invoice. #5697

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .msggen.json
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,12 @@
"SetchannelResponse": {
"SetChannel.channels[]": 1
},
"SigninvoiceRequest": {
"SignInvoice.invstring": 1
},
"SigninvoiceResponse": {
"SignInvoice.bolt11": 1
},
"SignmessageRequest": {
"SignMessage.message": 1
},
Expand Down
9 changes: 9 additions & 0 deletions cln-grpc/proto/node.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions cln-grpc/src/convert.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions cln-grpc/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,38 @@ async fn set_channel(

}

async fn sign_invoice(
&self,
request: tonic::Request<pb::SigninvoiceRequest>,
) -> Result<tonic::Response<pb::SigninvoiceResponse>, tonic::Status> {
let req = request.into_inner();
let req: requests::SigninvoiceRequest = req.into();
debug!("Client asked for sign_invoice");
trace!("sign_invoice request: {:?}", req);
let mut rpc = ClnRpc::new(&self.rpc_path)
.await
.map_err(|e| Status::new(Code::Internal, e.to_string()))?;
let result = rpc.call(Request::SignInvoice(req))
.await
.map_err(|e| Status::new(
Code::Unknown,
format!("Error calling method SignInvoice: {:?}", e)))?;
match result {
Response::SignInvoice(r) => {
trace!("sign_invoice response: {:?}", r);
Ok(tonic::Response::new(r.into()))
},
r => Err(Status::new(
Code::Internal,
format!(
"Unexpected result {:?} to method call SignInvoice",
r
)
)),
}

}

async fn sign_message(
&self,
request: tonic::Request<pb::SignmessageRequest>,
Expand Down
33 changes: 33 additions & 0 deletions cln-rpc/src/model.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions contrib/msggen/msggen/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def load_jsonrpc_service(schema_dir: str):
# "sendinvoice",
# "sendonionmessage",
"SetChannel",
"SignInvoice",
"SignMessage",
# "unreserveinputs",
# "waitblockheight",
Expand Down
6 changes: 6 additions & 0 deletions contrib/pyln-testing/pyln/testing/grpc2py.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,12 @@ def setchannel2py(m):
})


def signinvoice2py(m):
return remove_default({
"bolt11": m.bolt11, # PrimitiveField in generate_composite
})


def signmessage2py(m):
return remove_default({
"signature": hexlify(m.signature), # PrimitiveField in generate_composite
Expand Down
1 change: 1 addition & 0 deletions doc/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ MANPAGES := doc/lightning-cli.1 \
doc/lightning-sendpay.7 \
doc/lightning-setchannel.7 \
doc/lightning-sendcustommsg.7 \
doc/lightning-signinvoice.7 \
doc/lightning-signmessage.7 \
doc/lightning-staticbackup.7 \
doc/lightning-txprepare.7 \
Expand Down
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Core Lightning Documentation
lightning-sendpay <lightning-sendpay.7.md>
lightning-sendpsbt <lightning-sendpsbt.7.md>
lightning-setchannel <lightning-setchannel.7.md>
lightning-signinvoice <lightning-signinvoice.7.md>
lightning-signmessage <lightning-signmessage.7.md>
lightning-signpsbt <lightning-signpsbt.7.md>
lightning-sql <lightning-sql.7.md>
Expand Down
4 changes: 2 additions & 2 deletions doc/lightning-createinvoice.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ DESCRIPTION
The **createinvoice** RPC command signs and saves an invoice into the
database.

The *invstring* parameter is of bolt11 form, but without the final
signature appended. Minimal sanity checks are done. (Note: if
The *invstring* parameter is of bolt11 form, but the final signature
is ignored. Minimal sanity checks are done. (Note: if
**experimental-offers** is enabled, *invstring* can actually be an
unsigned bolt12 invoice).

Expand Down
51 changes: 51 additions & 0 deletions doc/lightning-signinvoice.7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
lightning-signinvoice -- Low-level invoice signing
=====================================================

SYNOPSIS
--------

**signinvoice** *invstring*

DESCRIPTION
-----------

The **signinvoice** RPC command signs an invoice. Unlike
**createinvoice** it does not save the invoice into the database and
thus does not require the preimage.

The *invstring* parameter is of bolt11 form, but the final signature
is ignored. Minimal sanity checks are done.

RETURN VALUE
------------

[comment]: # (GENERATE-FROM-SCHEMA-START)
On success, an object is returned, containing:

- **bolt11** (string): the bolt11 string

[comment]: # (GENERATE-FROM-SCHEMA-END)

On failure, an error is returned.

The following error codes may occur:
- -1: Catchall nonspecific error.

AUTHOR
------

Carl Dong <<contact@carldong.me>> is mainly responsible.

SEE ALSO
--------

lightning-createinvoice(7), lightning-invoice(7), lightning-listinvoices(7),
lightning-delinvoice(7), lightning-getroute(7), lightning-sendpay(7),
lightning-offer(7).

RESOURCES
---------

Main web site: <https://github.com/ElementsProject/lightning>

[comment]: # ( SHA256STAMP:9348784bd3daaed1cd35b29b2e5c91ea17bc8e11bf5bb6e1de9a098241cb74d6)
15 changes: 15 additions & 0 deletions doc/schemas/signinvoice.request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"added": "v23.02",
"required": [
"invstring"
vincenzopalazzo marked this conversation as resolved.
Show resolved Hide resolved
],
"properties": {
"invstring": {
"type": "string",
"description": ""
}
}
}
14 changes: 14 additions & 0 deletions doc/schemas/signinvoice.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [
"bolt11"
],
Comment on lines +5 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bolt12 invoice born already signed iirc, so maybe here the options can be:

  • Support only the bolt11 and return an error in case of bolt12;
  • Support both, and return a warning in case of bolt12 (and if I'm not wrong regarding the bolt12 invoice) that tells that the invoice is already signed. This warning could be useful also in case of bolt11 is already signed?

"properties": {
"bolt11": {
"type": "string",
"description": "the bolt11 string"
}
}
}
54 changes: 54 additions & 0 deletions lightningd/invoice.c
Original file line number Diff line number Diff line change
Expand Up @@ -1897,3 +1897,57 @@ static const struct json_command preapprovekeysend_command = {
"Ask the HSM to preapprove a keysend payment."
};
AUTODATA(json_command, &preapprovekeysend_command);

static struct command_result *json_signinvoice(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
const jsmntok_t *params)
{
const char *invstring;
struct json_stream *response;
struct bolt11 *b11;
struct sha256 hash;
const u5 *sig;
bool have_n;
char *fail;

if (!param(cmd, buffer, params,
p_req("invstring", param_string, &invstring),
NULL))
return command_param_failed();

b11 = bolt11_decode_nosig(cmd, invstring, cmd->ld->our_features,
NULL, chainparams, &hash, &sig, &have_n,
&fail);

if (!b11)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Unparsable invoice '%s': %s",
vincenzopalazzo marked this conversation as resolved.
Show resolved Hide resolved
invstring, fail);

/* This adds the signature */
char *b11enc = bolt11_encode(cmd, b11, have_n,
hsm_sign_b11, cmd->ld);

/* BOLT #11:
* A writer:
*...
* - MUST include either exactly one `d` or exactly one `h` field.
*/
if (!b11->description && !b11->description_hash)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Missing description in invoice");

response = json_stream_success(cmd);
json_add_invstring(response, b11enc);
return command_success(cmd, response);
}

static const struct json_command signinvoice_command = {
"signinvoice",
"payment",
json_signinvoice,
"Lowlevel command to sign invoice {invstring}."
};

AUTODATA(json_command, &signinvoice_command);
13 changes: 13 additions & 0 deletions tests/test_invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,19 @@ def test_waitanyinvoice(node_factory, executor):
l2.rpc.waitanyinvoice('non-number')


def test_signinvoice(node_factory, executor):
# Setup
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Setup
""" Make sure that the signinvoice work as expected while re-sign a previous invoice """
# Setup

This is a personal preference, that I find useful in case of refactoring the test in the future

l1, l2 = node_factory.line_graph(2)

# Create an invoice for l1
inv1 = l1.rpc.invoice(1000, 'inv1', 'inv1')['bolt11']
assert l1.rpc.decodepay(inv1)['payee'] == l1.info['id']

# Have l2 re-sign the invoice
inv2 = l2.rpc.signinvoice(inv1)['bolt11']
assert l1.rpc.decodepay(inv2)['payee'] == l2.info['id']


def test_waitanyinvoice_reversed(node_factory, executor):
"""Test waiting for invoices, where they are paid in reverse order
to when they are created.
Expand Down