From f90b0c7fdbc20327c3613427b8da590f90e68e3c Mon Sep 17 00:00:00 2001 From: Carl Dong Date: Mon, 7 Nov 2022 12:20:19 -0500 Subject: [PATCH 1/4] doc: Correct `createinvoice`'s `invstring` description The existing description is incorrect. `createinvoice` doesn't actually work when supplied with a custom-encoded bolt11 invoice without the final 520 signature bits appended. If a users tries to do so, some of their tagged fields will be incorrectly truncated. `createinvoice` actually expects that the signatures are there, and it simply ignores them. See common/bolt11.c's bolt11_decode_nosig: /* BOLT #11: * * The data part of a Lightning invoice consists of multiple sections: * * 1. `timestamp`: seconds-since-1970 (35 bits, big-endian) * 1. zero or more tagged parts * 1. `signature`: Bitcoin-style signature of above (520 bits) */ if (!pull_uint(&hu5, &data, &data_len, &b11->timestamp, 35)) return decode_fail(b11, fail, "Can't get 35-bit timestamp"); > while (data_len > 520 / 5) { const char *problem = NULL; u64 type, data_length; --- doc/lightning-createinvoice.7.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/lightning-createinvoice.7.md b/doc/lightning-createinvoice.7.md index f841a7f95775..0d62d70afe6f 100644 --- a/doc/lightning-createinvoice.7.md +++ b/doc/lightning-createinvoice.7.md @@ -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). From 4b7080f23f9821a976dbacef0f7a3beb4949a971 Mon Sep 17 00:00:00 2001 From: Carl Dong Date: Sun, 6 Nov 2022 19:45:47 -0500 Subject: [PATCH 2/4] lightningd: Add `signinvoice` to sign a BOLT11 invoice. Though there's already a `createinvoice` command, there are usecases where a user may want to sign an invoice that they don't yet have the preimage to. For example, they may have an htlc_accepted plugin that pays to obtain the preimage from someone else and returns a `{ "result": "resolve", ... }`. This RPC command addresses this usecase without overly complicating the semantics of the existing `createinvoice` command. Changelog-Added: JSON-RPC: `signinvoice` new command to sign BOLT11 invoices. --- doc/Makefile | 1 + doc/index.rst | 1 + doc/lightning-signinvoice.7.md | 51 ++++++++++++++++++++++++++ doc/schemas/signinvoice.request.json | 15 ++++++++ doc/schemas/signinvoice.schema.json | 14 ++++++++ lightningd/invoice.c | 54 ++++++++++++++++++++++++++++ tests/test_invoices.py | 13 +++++++ 7 files changed, 149 insertions(+) create mode 100644 doc/lightning-signinvoice.7.md create mode 100644 doc/schemas/signinvoice.request.json create mode 100644 doc/schemas/signinvoice.schema.json diff --git a/doc/Makefile b/doc/Makefile index 560e6782dc45..a2e158e3da2f 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -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 \ diff --git a/doc/index.rst b/doc/index.rst index b0d8ae79971f..1be476a414e3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -116,6 +116,7 @@ Core Lightning Documentation lightning-sendpay lightning-sendpsbt lightning-setchannel + lightning-signinvoice lightning-signmessage lightning-signpsbt lightning-sql diff --git a/doc/lightning-signinvoice.7.md b/doc/lightning-signinvoice.7.md new file mode 100644 index 000000000000..8faebbef9f9b --- /dev/null +++ b/doc/lightning-signinvoice.7.md @@ -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 <> 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: + +[comment]: # ( SHA256STAMP:9348784bd3daaed1cd35b29b2e5c91ea17bc8e11bf5bb6e1de9a098241cb74d6) diff --git a/doc/schemas/signinvoice.request.json b/doc/schemas/signinvoice.request.json new file mode 100644 index 000000000000..40b8e3f46a55 --- /dev/null +++ b/doc/schemas/signinvoice.request.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "added": "v23.02", + "required": [ + "invstring" + ], + "properties": { + "invstring": { + "type": "string", + "description": "" + } + } +} diff --git a/doc/schemas/signinvoice.schema.json b/doc/schemas/signinvoice.schema.json new file mode 100644 index 000000000000..bf9be4741211 --- /dev/null +++ b/doc/schemas/signinvoice.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "bolt11" + ], + "properties": { + "bolt11": { + "type": "string", + "description": "the bolt11 string" + } + } +} diff --git a/lightningd/invoice.c b/lightningd/invoice.c index 1c890931b329..d83eb658e57e 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -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", + 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); diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 7428827cff08..83aa56d2d5c5 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -533,6 +533,19 @@ def test_waitanyinvoice(node_factory, executor): l2.rpc.waitanyinvoice('non-number') +def test_signinvoice(node_factory, executor): + # Setup + 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. From 863c32d1cae2a1d89031eddf3f35574a53f421fa Mon Sep 17 00:00:00 2001 From: Carl Dong Date: Mon, 6 Feb 2023 12:14:10 -0500 Subject: [PATCH 3/4] msggen: Enable SignInvoice --- contrib/msggen/msggen/utils/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/msggen/msggen/utils/utils.py b/contrib/msggen/msggen/utils/utils.py index 4ef75d26cc2b..bb109ebc6860 100644 --- a/contrib/msggen/msggen/utils/utils.py +++ b/contrib/msggen/msggen/utils/utils.py @@ -97,6 +97,7 @@ def load_jsonrpc_service(schema_dir: str): # "sendinvoice", # "sendonionmessage", "SetChannel", + "SignInvoice", "SignMessage", # "unreserveinputs", # "waitblockheight", From 813a93e04131234cb95ff0e74f77870791d4a1db Mon Sep 17 00:00:00 2001 From: Carl Dong Date: Mon, 6 Feb 2023 12:14:54 -0500 Subject: [PATCH 4/4] msggen: Regenerate for addition of SignInvoice Performed using: PYTHONPATH=contrib/msggen python3 contrib/msggen/msggen/__main__.py --- .msggen.json | 6 ++++ cln-grpc/proto/node.proto | 9 ++++++ cln-grpc/src/convert.rs | 18 +++++++++++ cln-grpc/src/server.rs | 32 +++++++++++++++++++ cln-rpc/src/model.rs | 33 ++++++++++++++++++++ contrib/pyln-testing/pyln/testing/grpc2py.py | 6 ++++ 6 files changed, 104 insertions(+) diff --git a/.msggen.json b/.msggen.json index 311d64ee04d9..9e3c3971dc04 100644 --- a/.msggen.json +++ b/.msggen.json @@ -1006,6 +1006,12 @@ "SetchannelResponse": { "SetChannel.channels[]": 1 }, + "SigninvoiceRequest": { + "SignInvoice.invstring": 1 + }, + "SigninvoiceResponse": { + "SignInvoice.bolt11": 1 + }, "SignmessageRequest": { "SignMessage.message": 1 }, diff --git a/cln-grpc/proto/node.proto b/cln-grpc/proto/node.proto index 981ae294649f..74f5dae3c010 100644 --- a/cln-grpc/proto/node.proto +++ b/cln-grpc/proto/node.proto @@ -53,6 +53,7 @@ service Node { rpc ListPays(ListpaysRequest) returns (ListpaysResponse) {} rpc Ping(PingRequest) returns (PingResponse) {} rpc SetChannel(SetchannelRequest) returns (SetchannelResponse) {} + rpc SignInvoice(SigninvoiceRequest) returns (SigninvoiceResponse) {} rpc SignMessage(SignmessageRequest) returns (SignmessageResponse) {} rpc Stop(StopRequest) returns (StopResponse) {} } @@ -1323,6 +1324,14 @@ message SetchannelChannels { optional string warning_htlcmax_too_high = 9; } +message SigninvoiceRequest { + string invstring = 1; +} + +message SigninvoiceResponse { + string bolt11 = 1; +} + message SignmessageRequest { string message = 1; } diff --git a/cln-grpc/src/convert.rs b/cln-grpc/src/convert.rs index f727f78b03ac..fdb1aa10aaa6 100644 --- a/cln-grpc/src/convert.rs +++ b/cln-grpc/src/convert.rs @@ -1101,6 +1101,15 @@ impl From for pb::SetchannelResponse { } } +#[allow(unused_variables)] +impl From for pb::SigninvoiceResponse { + fn from(c: responses::SigninvoiceResponse) -> Self { + Self { + bolt11: c.bolt11, // Rule #2 for type string + } + } +} + #[allow(unused_variables)] impl From for pb::SignmessageResponse { fn from(c: responses::SignmessageResponse) -> Self { @@ -1690,6 +1699,15 @@ impl From for requests::SetchannelRequest { } } +#[allow(unused_variables)] +impl From for requests::SigninvoiceRequest { + fn from(c: pb::SigninvoiceRequest) -> Self { + Self { + invstring: c.invstring, // Rule #1 for type string + } + } +} + #[allow(unused_variables)] impl From for requests::SignmessageRequest { fn from(c: pb::SignmessageRequest) -> Self { diff --git a/cln-grpc/src/server.rs b/cln-grpc/src/server.rs index 7759ca0a6a3a..78938d62f340 100644 --- a/cln-grpc/src/server.rs +++ b/cln-grpc/src/server.rs @@ -1466,6 +1466,38 @@ async fn set_channel( } +async fn sign_invoice( + &self, + request: tonic::Request, +) -> Result, 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, diff --git a/cln-rpc/src/model.rs b/cln-rpc/src/model.rs index bf4faeba21e1..a8938ba5361a 100644 --- a/cln-rpc/src/model.rs +++ b/cln-rpc/src/model.rs @@ -61,6 +61,7 @@ pub enum Request { ListPays(requests::ListpaysRequest), Ping(requests::PingRequest), SetChannel(requests::SetchannelRequest), + SignInvoice(requests::SigninvoiceRequest), SignMessage(requests::SignmessageRequest), Stop(requests::StopRequest), } @@ -114,6 +115,7 @@ pub enum Response { ListPays(responses::ListpaysResponse), Ping(responses::PingResponse), SetChannel(responses::SetchannelResponse), + SignInvoice(responses::SigninvoiceResponse), SignMessage(responses::SignmessageResponse), Stop(responses::StopResponse), } @@ -1241,6 +1243,21 @@ pub mod requests { type Response = super::responses::SetchannelResponse; } + #[derive(Clone, Debug, Deserialize, Serialize)] + pub struct SigninvoiceRequest { + pub invstring: String, + } + + impl From for Request { + fn from(r: SigninvoiceRequest) -> Self { + Request::SignInvoice(r) + } + } + + impl IntoRequest for SigninvoiceRequest { + type Response = super::responses::SigninvoiceResponse; + } + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct SignmessageRequest { pub message: String, @@ -3517,6 +3534,22 @@ pub mod responses { } } + #[derive(Clone, Debug, Deserialize, Serialize)] + pub struct SigninvoiceResponse { + pub bolt11: String, + } + + impl TryFrom for SigninvoiceResponse { + type Error = super::TryFromResponseError; + + fn try_from(response: Response) -> Result { + match response { + Response::SignInvoice(response) => Ok(response), + _ => Err(TryFromResponseError) + } + } + } + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct SignmessageResponse { pub signature: String, diff --git a/contrib/pyln-testing/pyln/testing/grpc2py.py b/contrib/pyln-testing/pyln/testing/grpc2py.py index 28b366c8bdda..1f18b1451368 100644 --- a/contrib/pyln-testing/pyln/testing/grpc2py.py +++ b/contrib/pyln-testing/pyln/testing/grpc2py.py @@ -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