From 6e94da34f2bac13f39b8c4cd199da90e9dc05d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20Sj=C3=B6berg?= Date: Wed, 8 Feb 2023 00:11:58 +0100 Subject: [PATCH] LNURL-pay request forwarding via LN P2P messages --- config/config_TEMPLATE.ts | 1 + config/interface.ts | 5 + package.json | 2 +- proto/{rpc.proto => lightning.proto} | 802 ++++++++++++++++++++++++++- src/api/pay.ts | 177 +++++- src/utils/common.ts | 23 +- src/utils/grpc.ts | 2 +- src/utils/lnd-api.ts | 32 ++ 8 files changed, 1008 insertions(+), 36 deletions(-) rename proto/{rpc.proto => lightning.proto} (81%) diff --git a/config/config_TEMPLATE.ts b/config/config_TEMPLATE.ts index ac6c869..fbe91e6 100644 --- a/config/config_TEMPLATE.ts +++ b/config/config_TEMPLATE.ts @@ -13,6 +13,7 @@ const config: Config = { adminMacaroon: "~/path/to/lnd/admin.macaroon", }, singlePaymentForwardWithdrawLimit: 5, + disableCustodial: false, }; export default config; diff --git a/config/interface.ts b/config/interface.ts index 74d3854..6e85231 100644 --- a/config/interface.ts +++ b/config/interface.ts @@ -30,4 +30,9 @@ export interface Config { // The number of single payment withdrawals (in contrast to batch withdrawal) we allow. // If exceeded, batch withdrawal is enforced. singlePaymentForwardWithdrawLimit: number; + + // Disable the custodial part of Lightning Box. + // This requires users to be online at the time of the payment request. + // Otherwise the request will immediately fail. + disableCustodial: boolean; } diff --git a/package.json b/package.json index 576f02f..5b11507 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "postbuild": "cp src/proto.js dist", "start": "rm -rf dist && npm run build && node dist/src/server.js", "watch": "concurrently \"tsc -p tsconfig.json -w\" \"nodemon -w dist dist/server.js\"", - "proto": "pbjs --force-long -t static-module -o src/proto.js proto/rpc.proto proto/router.proto && pbts -o src/proto.d.ts src/proto.js", + "proto": "pbjs --force-long -t static-module -o src/proto.js proto/lightning.proto proto/router.proto && pbts -o src/proto.d.ts src/proto.js", "test": "jest tests", "test:coverage": "jest --coverage --coveragePathIgnorePatterns \"proto\\.js|mocks\" tests" }, diff --git a/proto/rpc.proto b/proto/lightning.proto similarity index 81% rename from proto/rpc.proto rename to proto/lightning.proto index 7f5ff92..8fadfb5 100644 --- a/proto/rpc.proto +++ b/proto/lightning.proto @@ -200,6 +200,16 @@ service Lightning { */ rpc OpenChannel (OpenChannelRequest) returns (stream OpenStatusUpdate); + /* lncli: `batchopenchannel` + BatchOpenChannel attempts to open multiple single-funded channels in a + single transaction in an atomic way. This means either all channel open + requests succeed at once or all attempts are aborted if any of them fail. + This is the safer variant of using PSBTs to manually fund a batch of + channels through the OpenChannel RPC. + */ + rpc BatchOpenChannel (BatchOpenChannelRequest) + returns (BatchOpenChannelResponse); + /* FundingStateStep is an advanced funding related call that allows the caller to either execute some preparatory steps for a funding workflow, or @@ -330,7 +340,14 @@ service Lightning { rpc ListPayments (ListPaymentsRequest) returns (ListPaymentsResponse); /* - DeleteAllPayments deletes all outgoing payments from DB. + DeletePayment deletes an outgoing payment from DB. Note that it will not + attempt to delete an In-Flight payment, since that would be unsafe. + */ + rpc DeletePayment (DeletePaymentRequest) returns (DeletePaymentResponse); + + /* + DeleteAllPayments deletes all outgoing payments from DB. Note that it will + not attempt to delete In-Flight payments, since that would be unsafe. */ rpc DeleteAllPayments (DeleteAllPaymentsRequest) returns (DeleteAllPaymentsResponse); @@ -426,8 +443,9 @@ service Lightning { /* lncli: `fwdinghistory` ForwardingHistory allows the caller to query the htlcswitch for a record of all HTLCs forwarded within the target time range, and integer offset - within that time range. If no time-range is specified, then the first chunk - of the past 24 hrs of forwarding history are returned. + within that time range, for a maximum number of events. If no maximum number + of events is specified, up to 100 events will be returned. If no time-range + is specified, then events will be returned in the order that they occured. A list of forwarding events are returned. The size of each forwarding event is 40 bytes, and the max message size able to be returned in gRPC is 4 MiB. @@ -514,6 +532,79 @@ service Lightning { */ rpc ListPermissions (ListPermissionsRequest) returns (ListPermissionsResponse); + + /* + CheckMacaroonPermissions checks whether a request follows the constraints + imposed on the macaroon and that the macaroon is authorized to follow the + provided permissions. + */ + rpc CheckMacaroonPermissions (CheckMacPermRequest) + returns (CheckMacPermResponse); + + /* + RegisterRPCMiddleware adds a new gRPC middleware to the interceptor chain. A + gRPC middleware is software component external to lnd that aims to add + additional business logic to lnd by observing/intercepting/validating + incoming gRPC client requests and (if needed) replacing/overwriting outgoing + messages before they're sent to the client. When registering the middleware + must identify itself and indicate what custom macaroon caveats it wants to + be responsible for. Only requests that contain a macaroon with that specific + custom caveat are then sent to the middleware for inspection. The other + option is to register for the read-only mode in which all requests/responses + are forwarded for interception to the middleware but the middleware is not + allowed to modify any responses. As a security measure, _no_ middleware can + modify responses for requests made with _unencumbered_ macaroons! + */ + rpc RegisterRPCMiddleware (stream RPCMiddlewareResponse) + returns (stream RPCMiddlewareRequest); + + /* lncli: `sendcustom` + SendCustomMessage sends a custom peer message. + */ + rpc SendCustomMessage (SendCustomMessageRequest) + returns (SendCustomMessageResponse); + + /* lncli: `subscribecustom` + SubscribeCustomMessages subscribes to a stream of incoming custom peer + messages. + */ + rpc SubscribeCustomMessages (SubscribeCustomMessagesRequest) + returns (stream CustomMessage); + + /* lncli: `listaliases` + ListAliases returns the set of all aliases that have ever existed with + their confirmed SCID (if it exists) and/or the base SCID (in the case of + zero conf). + */ + rpc ListAliases (ListAliasesRequest) returns (ListAliasesResponse); +} + +message SubscribeCustomMessagesRequest { +} + +message CustomMessage { + // Peer from which the message originates + bytes peer = 1; + + // Message type. This value will be in the custom range (>= 32768). + uint32 type = 2; + + // Raw message data + bytes data = 3; +} + +message SendCustomMessageRequest { + // Peer to send the message to + bytes peer = 1; + + // Message type. This value needs to be in the custom range (>= 32768). + uint32 type = 2; + + // Raw message data. + bytes data = 3; +} + +message SendCustomMessageResponse { } message Utxo { @@ -536,6 +627,39 @@ message Utxo { int64 confirmations = 6; } +enum OutputScriptType { + SCRIPT_TYPE_PUBKEY_HASH = 0; + SCRIPT_TYPE_SCRIPT_HASH = 1; + SCRIPT_TYPE_WITNESS_V0_PUBKEY_HASH = 2; + SCRIPT_TYPE_WITNESS_V0_SCRIPT_HASH = 3; + SCRIPT_TYPE_PUBKEY = 4; + SCRIPT_TYPE_MULTISIG = 5; + SCRIPT_TYPE_NULLDATA = 6; + SCRIPT_TYPE_NON_STANDARD = 7; + SCRIPT_TYPE_WITNESS_UNKNOWN = 8; + SCRIPT_TYPE_WITNESS_V1_TAPROOT = 9; +} + +message OutputDetail { + // The type of the output + OutputScriptType output_type = 1; + + // The address + string address = 2; + + // The pkscript in hex + string pk_script = 3; + + // The output index used in the raw transaction + int64 output_index = 4; + + // The value of the output coin in satoshis + int64 amount = 5; + + // Denotes if the output is controlled by the internal wallet + bool is_our_address = 6; +} + message Transaction { // The transaction hash string tx_hash = 1; @@ -558,15 +682,23 @@ message Transaction { // Fees paid for this transaction int64 total_fees = 7; - // Addresses that received funds for this transaction - repeated string dest_addresses = 8; + // Addresses that received funds for this transaction. Deprecated as it is + // now incorporated in the output_details field. + repeated string dest_addresses = 8 [deprecated = true]; + + // Outputs that received funds for this transaction + repeated OutputDetail output_details = 11; // The raw transaction hex. string raw_tx_hex = 9; // A label that was optionally set on transaction broadcast. string label = 10; + + // PreviousOutpoints/Inputs of this transaction. + repeated PreviousOutPoint previous_outpoints = 12; } + message GetTransactionsRequest { /* The height from which to list transactions, inclusive. If this value is @@ -669,7 +801,8 @@ message SendRequest { The maximum number of satoshis that will be paid as a fee of the payment. This value can be represented either as a percentage of the amount being sent, or as a fixed amount of the maximum fee the user is willing the pay to - send the payment. + send the payment. If not specified, lnd will use a default value of 100% + fees for small amounts (<=1k sat) or 5% fees for larger amounts. */ FeeLimit fee_limit = 8; @@ -791,6 +924,17 @@ message ChannelAcceptRequest { // A bit-field which the initiator uses to specify proposed channel // behavior. uint32 channel_flags = 13; + + // The commitment type the initiator wishes to use for the proposed channel. + CommitmentType commitment_type = 14; + + // Whether the initiator wants to open a zero-conf channel via the channel + // type. + bool wants_zero_conf = 15; + + // Whether the initiator wants to use the scid-alias channel type. This is + // separate from the feature bit. + bool wants_scid_alias = 16; } message ChannelAcceptResponse { @@ -851,6 +995,13 @@ message ChannelAcceptResponse { The number of confirmations we require before we consider the channel open. */ uint32 min_accept_depth = 10; + + /* + Whether the responder wants this to be a zero-conf channel. This will fail + if either side does not have the scid-alias feature bit set. The minimum + depth field must be zero if this is true. + */ + bool zero_conf = 11; } message ChannelPoint { @@ -883,12 +1034,21 @@ message OutPoint { uint32 output_index = 3; } +message PreviousOutPoint { + // The outpoint in format txid:n. + string outpoint = 1; + + // Denotes if the outpoint is controlled by the internal wallet. + // The flag will only detect p2wkh, np2wkh and p2tr inputs as its own. + bool is_our_output = 2; +} + message LightningAddress { - // The identity pubkey of the Lightning node + // The identity pubkey of the Lightning node. string pubkey = 1; // The network location of the lightning node, e.g. `69.69.69.69:1337` or - // `localhost:10011` + // `localhost:10011`. string host = 2; } @@ -899,6 +1059,13 @@ message EstimateFeeRequest { // The target number of blocks that this transaction should be confirmed // by. int32 target_conf = 2; + + // The minimum number of confirmations each one of your outputs used for + // the transaction must satisfy. + int32 min_confs = 3; + + // Whether unconfirmed outputs should be used as inputs for the transaction. + bool spend_unconfirmed = 4; } message EstimateFeeResponse { @@ -1007,12 +1174,15 @@ message ListUnspentResponse { - `p2wkh`: Pay to witness key hash (`WITNESS_PUBKEY_HASH` = 0) - `np2wkh`: Pay to nested witness key hash (`NESTED_PUBKEY_HASH` = 1) +- `p2tr`: Pay to taproot pubkey (`TAPROOT_PUBKEY` = 4) */ enum AddressType { WITNESS_PUBKEY_HASH = 0; NESTED_PUBKEY_HASH = 1; UNUSED_WITNESS_PUBKEY_HASH = 2; UNUSED_NESTED_PUBKEY_HASH = 3; + TAPROOT_PUBKEY = 4; + UNUSED_TAPROOT_PUBKEY = 5; } message NewAddressRequest { @@ -1036,6 +1206,12 @@ message SignMessageRequest { base64. */ bytes msg = 1; + + /* + Instead of the default double-SHA256 hashing of the message before signing, + only use one round of hashing instead. + */ + bool single_hash = 2; } message SignMessageResponse { // The signature for the given message @@ -1061,11 +1237,15 @@ message VerifyMessageResponse { } message ConnectPeerRequest { - // Lightning address of the peer, in the format `@host` + /* + Lightning address of the peer to connect to. + */ LightningAddress addr = 1; - /* If set, the daemon will attempt to persistently connect to the target - * peer. Otherwise, the call will be synchronous. */ + /* + If set, the daemon will attempt to persistently connect to the target + peer. Otherwise, the call will be synchronous. + */ bool perm = 2; /* @@ -1107,11 +1287,16 @@ message HTLC { } enum CommitmentType { + /* + Returned when the commitment type isn't known or unavailable. + */ + UNKNOWN_COMMITMENT_TYPE = 0; + /* A channel using the legacy commitment format having tweaked to_remote keys. */ - LEGACY = 0; + LEGACY = 1; /* A channel that uses the modern commitment format where the key in the @@ -1119,19 +1304,23 @@ enum CommitmentType { up and recovery easier as when the channel is closed, the funds go directly to that key. */ - STATIC_REMOTE_KEY = 1; + STATIC_REMOTE_KEY = 2; /* A channel that uses a commitment format that has anchor outputs on the commitments, allowing fee bumping after a force close transaction has been broadcast. */ - ANCHORS = 2; + ANCHORS = 3; /* - Returned when the commitment type isn't known or unavailable. + A channel that uses a commitment type that builds upon the anchors + commitment format, but in addition requires a CLTV clause to spend outputs + paying to the channel initiator. This is intended for use on leased channels + to guarantee that the channel initiator has no incentives to close a leased + channel before its maturity date. */ - UNKNOWN_COMMITMENT_TYPE = 999; + SCRIPT_ENFORCED_LEASE = 4; } message ChannelConstraints { @@ -1309,6 +1498,21 @@ message Channel { // List constraints for the remote node. ChannelConstraints remote_constraints = 30; + + /* + This lists out the set of alias short channel ids that exist for a channel. + This may be empty. + */ + repeated uint64 alias_scids = 31; + + // Whether or not this is a zero-conf channel. + bool zero_conf = 32; + + // This is the confirmed / on-chain zero-conf SCID. + uint64 zero_conf_confirmed_scid = 33; + + // This is the peer SCID alias. + uint64 peer_scid_alias = 34 [jstype = JS_STRING]; } message ListChannelsRequest { @@ -1328,6 +1532,22 @@ message ListChannelsResponse { repeated Channel channels = 11; } +message AliasMap { + /* + For non-zero-conf channels, this is the confirmed SCID. Otherwise, this is + the first assigned "base" alias. + */ + uint64 base_scid = 1; + + // The set of all aliases stored for the base SCID. + repeated uint64 aliases = 2; +} +message ListAliasesRequest { +} +message ListAliasesResponse { + repeated AliasMap alias_maps = 1; +} + enum Initiator { INITIATOR_UNKNOWN = 0; INITIATOR_LOCAL = 1; @@ -1392,6 +1612,15 @@ message ChannelCloseSummary { Initiator close_initiator = 12; repeated Resolution resolutions = 13; + + /* + This lists out the set of alias short channel ids that existed for the + closed channel. This may be empty. + */ + repeated uint64 alias_scids = 14; + + // The confirmed SCID for a zero-conf channel. + uint64 zero_conf_confirmed_scid = 15 [jstype = JS_STRING]; } enum ResolutionType { @@ -1553,6 +1782,11 @@ message Peer { zero, we have not observed any flaps for this peer. */ int64 last_flap_ns = 14; + + /* + The last ping payload the peer has sent to us. + */ + bytes last_ping_payload = 15; } message TimestampedError { @@ -1655,6 +1889,11 @@ message GetInfoResponse { announcements and invoices. */ map features = 19; + + /* + Indicates whether the HTLC interceptor API is in always-on mode. + */ + bool require_htlc_interceptor = 21; } message GetRecoveryInfoRequest { @@ -1727,6 +1966,11 @@ message CloseChannelRequest { // A manual fee rate set in sat/vbyte that should be used when crafting the // closure transaction. uint64 sat_per_vbyte = 6; + + // The maximum fee rate the closer is willing to pay. + // + // NOTE: This field is only respected if we're the initiator of the channel. + uint64 max_fee_per_vbyte = 7; } message CloseStatusUpdate { @@ -1763,6 +2007,84 @@ message ReadyForPsbtFunding { bytes psbt = 3; } +message BatchOpenChannelRequest { + // The list of channels to open. + repeated BatchOpenChannel channels = 1; + + // The target number of blocks that the funding transaction should be + // confirmed by. + int32 target_conf = 2; + + // A manual fee rate set in sat/vByte that should be used when crafting the + // funding transaction. + int64 sat_per_vbyte = 3; + + // The minimum number of confirmations each one of your outputs used for + // the funding transaction must satisfy. + int32 min_confs = 4; + + // Whether unconfirmed outputs should be used as inputs for the funding + // transaction. + bool spend_unconfirmed = 5; + + // An optional label for the batch transaction, limited to 500 characters. + string label = 6; +} + +message BatchOpenChannel { + // The pubkey of the node to open a channel with. When using REST, this + // field must be encoded as base64. + bytes node_pubkey = 1; + + // The number of satoshis the wallet should commit to the channel. + int64 local_funding_amount = 2; + + // The number of satoshis to push to the remote side as part of the initial + // commitment state. + int64 push_sat = 3; + + // Whether this channel should be private, not announced to the greater + // network. + bool private = 4; + + // The minimum value in millisatoshi we will require for incoming HTLCs on + // the channel. + int64 min_htlc_msat = 5; + + // The delay we require on the remote's commitment transaction. If this is + // not set, it will be scaled automatically with the channel size. + uint32 remote_csv_delay = 6; + + /* + Close address is an optional address which specifies the address to which + funds should be paid out to upon cooperative close. This field may only be + set if the peer supports the option upfront feature bit (call listpeers + to check). The remote peer will only accept cooperative closes to this + address if it is set. + + Note: If this value is set on channel creation, you will *not* be able to + cooperatively close out to a different address. + */ + string close_address = 7; + + /* + An optional, unique identifier of 32 random bytes that will be used as the + pending channel ID to identify the channel while it is in the pre-pending + state. + */ + bytes pending_chan_id = 8; + + /* + The explicit commitment type to use. Note this field will only be used if + the remote peer supports explicit channel negotiation. + */ + CommitmentType commitment_type = 9; +} + +message BatchOpenChannelResponse { + repeated PendingUpdate pending_channels = 1; +} + message OpenChannelRequest { // A manual fee rate set in sat/vbyte that should be used when crafting the // funding transaction. @@ -1854,6 +2176,23 @@ message OpenChannelRequest { transaction. */ uint32 max_local_csv = 17; + + /* + The explicit commitment type to use. Note this field will only be used if + the remote peer supports explicit channel negotiation. + */ + CommitmentType commitment_type = 18; + + /* + If this is true, then a zero-conf channel open will be attempted. + */ + bool zero_conf = 19; + + /* + If this is true, then an option-scid-alias channel-type open will be + attempted. + */ + bool scid_alias = 20; } message OpenStatusUpdate { oneof update { @@ -1993,6 +2332,20 @@ message FundingPsbtVerify { // The pending channel ID of the channel to get the PSBT for. bytes pending_chan_id = 2; + + /* + Can only be used if the no_publish flag was set to true in the OpenChannel + call meaning that the caller is solely responsible for publishing the final + funding transaction. If skip_finalize is set to true then lnd will not wait + for a FundingPsbtFinalize state step and instead assumes that a transaction + with the same TXID as the passed in PSBT will eventually confirm. + IT IS ABSOLUTELY IMPERATIVE that the TXID of the transaction that is + eventually published does have the _same TXID_ as the verified PSBT. That + means no inputs or outputs can change, only signatures can be added. If the + TXID changes between this call and the publish step then the channel will + never be created and the funds will be in limbo. + */ + bool skip_finalize = 3; } message FundingPsbtFinalize { @@ -2097,15 +2450,21 @@ message PendingChannelsResponse { // The commitment type used by this channel. CommitmentType commitment_type = 9; + + // Total number of forwarding packages created in this channel. + int64 num_forwarding_packages = 10; + + // A set of flags showing the current state of the channel. + string chan_status_flags = 11; + + // Whether this channel is advertised to the network or not. + bool private = 12; } message PendingOpenChannel { // The pending channel PendingChannel channel = 1; - // The height at which this channel will be confirmed - uint32 confirmation_height = 2; - /* The amount calculated to be paid in fees for the current set of commitment transactions. The fee amount is persisted with the channel @@ -2124,6 +2483,9 @@ message PendingChannelsResponse { transaction. This value can later be updated once the channel is open. */ int64 fee_per_kw = 6; + + // Previously used for confirmation_height. Do not reuse. + reserved 2; } message WaitingCloseChannel { @@ -2138,6 +2500,9 @@ message PendingChannelsResponse { this point. */ Commitments commitments = 3; + + // The transaction id of the closing transaction + string closing_txid = 4; } message Commitments { @@ -2241,6 +2606,7 @@ message ChannelEventUpdate { ChannelPoint active_channel = 3; ChannelPoint inactive_channel = 4; PendingUpdate pending_open_channel = 6; + ChannelPoint fully_resolved_channel = 7; } enum UpdateType { @@ -2249,6 +2615,7 @@ message ChannelEventUpdate { ACTIVE_CHANNEL = 2; INACTIVE_CHANNEL = 3; PENDING_OPEN_CHANNEL = 4; + FULLY_RESOLVED_CHANNEL = 5; } UpdateType type = 5; @@ -2275,6 +2642,13 @@ message WalletBalanceResponse { // The unconfirmed balance of a wallet(with 0 confirmations) int64 unconfirmed_balance = 3; + // The total amount of wallet UTXOs held in outputs that are locked for + // other usage. + int64 locked_balance = 5; + + // The amount of reserve required. + int64 reserved_balance_anchor_chan = 6; + // A mapping of each wallet account's name to its balance. map account_balance = 4; } @@ -2348,7 +2722,8 @@ message QueryRoutesRequest { The maximum number of satoshis that will be paid as a fee of the payment. This value can be represented either as a percentage of the amount being sent, or as a fixed amount of the maximum fee the user is willing the pay to - send the payment. + send the payment. If not specified, lnd will use a default value of 100% + fees for small amounts (<=1k sat) or 5% fees for larger amounts. */ FeeLimit fee_limit = 5; @@ -2391,7 +2766,7 @@ message QueryRoutesRequest { An optional field that can be used to pass an arbitrary set of TLV records to a peer which understands the new records. This can be used to pass application specific data during the payment attempt. If the destination - does not support the specified recrods, and error will be returned. + does not support the specified records, an error will be returned. Record types are required to be in the custom range >= 65536. When using REST, the values must be encoded as base64. */ @@ -2421,6 +2796,12 @@ message QueryRoutesRequest { fallback. */ repeated lnrpc.FeatureBit dest_features = 17; + + /* + The time preference for this payment. Set to -1 to optimize for fees + only, to 1 to optimize for reliability only or a value inbetween for a mix. + */ + double time_pref = 18; } message NodePair { @@ -2471,7 +2852,7 @@ message Hop { output index for the channel. */ uint64 chan_id = 1 [jstype = JS_STRING]; - int64 chan_capacity = 2; + int64 chan_capacity = 2 [deprecated = true]; int64 amt_to_forward = 3 [deprecated = true]; int64 fee = 4 [deprecated = true]; uint32 expiry = 5; @@ -2489,7 +2870,7 @@ message Hop { TLV format. Note that if any custom tlv_records below are specified, then this field MUST be set to true for them to be encoded properly. */ - bool tlv_payload = 9; + bool tlv_payload = 9 [deprecated = true]; /* An optional TLV record that signals the use of an MPP payment. If present, @@ -2515,6 +2896,9 @@ message Hop { to drop off at each hop within the onion. */ map custom_records = 11; + + // The payment metadata to send along with the payment to the payee. + bytes metadata = 13; } message MPPRecord { @@ -2768,11 +3152,21 @@ message GraphTopologyUpdate { repeated ClosedChannelUpdate closed_chans = 3; } message NodeUpdate { - repeated string addresses = 1; + /* + Deprecated, use node_addresses. + */ + repeated string addresses = 1 [deprecated = true]; + string identity_key = 2; + + /* + Deprecated, use features. + */ bytes global_features = 3 [deprecated = true]; + string alias = 4; string color = 5; + repeated NodeAddress node_addresses = 7; /* Features that the node has advertised in the init message, node @@ -2829,6 +3223,10 @@ message HopHint { uint32 cltv_expiry_delta = 5; } +message SetID { + bytes set_id = 1; +} + message RouteHint { /* A list of hop hints that when chained together can assist in reaching a @@ -2837,6 +3235,20 @@ message RouteHint { repeated HopHint hop_hints = 1; } +message AMPInvoiceState { + // The state the HTLCs associated with this setID are in. + InvoiceHTLCState state = 1; + + // The settle index of this HTLC set, if the invoice state is settled. + uint64 settle_index = 2; + + // The time this HTLC set was settled expressed in unix epoch. + int64 settle_time = 3; + + // The total amount paid for the sub-invoice expressed in milli satoshis. + int64 amt_paid_msat = 5; +} + message Invoice { /* An optional memo to attach along with the invoice. Used for record keeping @@ -2858,6 +3270,7 @@ message Invoice { /* The hash of the preimage. When using REST, this field must be encoded as base64. + Note: Output only, don't specify for creating an invoice. */ bytes r_hash = 4; @@ -2875,19 +3288,30 @@ message Invoice { */ int64 value_msat = 23; - // Whether this invoice has been fulfilled + /* + Whether this invoice has been fulfilled + + The field is deprecated. Use the state field instead (compare to SETTLED). + */ bool settled = 6 [deprecated = true]; - // When this invoice was created + /* + When this invoice was created. + Note: Output only, don't specify for creating an invoice. + */ int64 creation_date = 7; - // When this invoice was settled + /* + When this invoice was settled. + Note: Output only, don't specify for creating an invoice. + */ int64 settle_date = 8; /* A bare-bones invoice for a payment within the Lightning Network. With the details of the invoice, the sender has all the data necessary to send a payment to the recipient. + Note: Output only, don't specify for creating an invoice. */ string payment_request = 9; @@ -2922,6 +3346,7 @@ message Invoice { this index making it monotonically increasing. Callers to the SubscribeInvoices call can use this to instantly get notified of all added invoices with an add_index greater than this one. + Note: Output only, don't specify for creating an invoice. */ uint64 add_index = 16; @@ -2930,6 +3355,7 @@ message Invoice { increment this index making it monotonically increasing. Callers to the SubscribeInvoices call can use this to instantly get notified of all settled invoices with an settle_index greater than this one. + Note: Output only, don't specify for creating an invoice. */ uint64 settle_index = 17; @@ -2943,6 +3369,7 @@ message Invoice { was ultimately accepted. Additionally, it's possible that the sender paid MORE that was specified in the original invoice. So we'll record that here as well. + Note: Output only, don't specify for creating an invoice. */ int64 amt_paid_sat = 19; @@ -2953,6 +3380,7 @@ message Invoice { amount was ultimately accepted. Additionally, it's possible that the sender paid MORE that was specified in the original invoice. So we'll record that here as well. + Note: Output only, don't specify for creating an invoice. */ int64 amt_paid_msat = 20; @@ -2965,27 +3393,52 @@ message Invoice { /* The state the invoice is in. + Note: Output only, don't specify for creating an invoice. */ InvoiceState state = 21; - // List of HTLCs paying to this invoice [EXPERIMENTAL]. + /* + List of HTLCs paying to this invoice [EXPERIMENTAL]. + Note: Output only, don't specify for creating an invoice. + */ repeated InvoiceHTLC htlcs = 22; - // List of features advertised on the invoice. + /* + List of features advertised on the invoice. + Note: Output only, don't specify for creating an invoice. + */ map features = 24; /* Indicates if this invoice was a spontaneous payment that arrived via keysend [EXPERIMENTAL]. + Note: Output only, don't specify for creating an invoice. */ bool is_keysend = 25; /* The payment address of this invoice. This value will be used in MPP - payments, and also for newer invoies that always require the MPP paylaod + payments, and also for newer invoices that always require the MPP payload for added end-to-end security. + Note: Output only, don't specify for creating an invoice. */ bytes payment_addr = 26; + + /* + Signals whether or not this is an AMP invoice. + */ + bool is_amp = 27; + + /* + [EXPERIMENTAL]: + + Maps a 32-byte hex-encoded set ID to the sub-invoice AMP state for the + given set ID. This field is always populated for AMP invoices, and can be + used along side LookupInvoice to obtain the HTLC information related to a + given sub-invoice. + Note: Output only, don't specify for creating an invoice. + */ + map amp_invoice_state = 28; } enum InvoiceHTLCState { @@ -3305,6 +3758,14 @@ message ListPaymentsRequest { of the returned payments is always oldest first (ascending index order). */ bool reversed = 4; + + /* + If set, all payments (complete and incomplete, independent of the + max_payments parameter) will be counted. Note that setting this to true will + increase the run time of the call significantly on systems that have a lot + of payments, as all of them have to be iterated through to be counted. + */ + bool count_total_payments = 5; } message ListPaymentsResponse { @@ -3322,6 +3783,24 @@ message ListPaymentsResponse { as the index_offset to continue seeking forwards in the next request. */ uint64 last_index_offset = 3; + + /* + Will only be set if count_total_payments in the request was set. Represents + the total number of payments (complete and incomplete, independent of the + number of payments requested in the query) currently present in the payments + database. + */ + uint64 total_num_payments = 4; +} + +message DeletePaymentRequest { + // Payment hash to delete. + bytes payment_hash = 1; + + /* + Only delete failed HTLCs from the payment, not the payment itself. + */ + bool failed_htlcs_only = 2; } message DeleteAllPaymentsRequest { @@ -3334,6 +3813,9 @@ message DeleteAllPaymentsRequest { bool failed_htlcs_only = 2; } +message DeletePaymentResponse { +} + message DeleteAllPaymentsResponse { } @@ -3341,6 +3823,13 @@ message AbandonChannelRequest { ChannelPoint channel_point = 1; bool pending_funding_shim_only = 2; + + /* + Override the requirement for being in dev mode by setting this to true and + confirming the user knows what they are doing and this is a potential foot + gun to lose funds if used on active channels. + */ + bool i_know_what_i_am_doing = 3; } message AbandonChannelResponse { @@ -3398,6 +3887,8 @@ enum FeatureBit { ANCHORS_OPT = 21; ANCHORS_ZERO_FEE_HTLC_REQ = 22; ANCHORS_ZERO_FEE_HTLC_OPT = 23; + AMP_REQ = 30; + AMP_OPT = 31; } message Feature { @@ -3460,6 +3951,9 @@ message PolicyUpdateRequest { // goes up to 6 decimal places, so 1e-6. double fee_rate = 4; + // The effective fee rate in micro-satoshis (parts per million). + uint32 fee_rate_ppm = 9; + // The required timelock delta for HTLCs forwarded over the channel. uint32 time_lock_delta = 5; @@ -3474,7 +3968,28 @@ message PolicyUpdateRequest { // If true, min_htlc_msat is applied. bool min_htlc_msat_specified = 8; } +enum UpdateFailure { + UPDATE_FAILURE_UNKNOWN = 0; + UPDATE_FAILURE_PENDING = 1; + UPDATE_FAILURE_NOT_FOUND = 2; + UPDATE_FAILURE_INTERNAL_ERR = 3; + UPDATE_FAILURE_INVALID_PARAMETER = 4; +} + +message FailedUpdate { + // The outpoint in format txid:n + OutPoint outpoint = 1; + + // Reason for the policy update failure. + UpdateFailure reason = 2; + + // A string representation of the policy update error. + string update_error = 3; +} + message PolicyUpdateResponse { + // List of failed policy updates. + repeated FailedUpdate failed_updates = 1; } message ForwardingHistoryRequest { @@ -3642,6 +4157,12 @@ message BakeMacaroonRequest { // The root key ID used to create the macaroon, must be a positive integer. uint64 root_key_id = 2; + + /* + Informs the RPC on whether to allow external permissions that LND is not + aware of. + */ + bool allow_external_permissions = 3; } message BakeMacaroonResponse { // The hex encoded macaroon, serialized in binary format. @@ -3853,3 +4374,224 @@ message Op { string entity = 1; repeated string actions = 2; } + +message CheckMacPermRequest { + bytes macaroon = 1; + repeated MacaroonPermission permissions = 2; + string fullMethod = 3; +} + +message CheckMacPermResponse { + bool valid = 1; +} + +message RPCMiddlewareRequest { + /* + The unique ID of the intercepted original gRPC request. Useful for mapping + request to response when implementing full duplex message interception. For + streaming requests, this will be the same ID for all incoming and outgoing + middleware intercept messages of the _same_ stream. + */ + uint64 request_id = 1; + + /* + The raw bytes of the complete macaroon as sent by the gRPC client in the + original request. This might be empty for a request that doesn't require + macaroons such as the wallet unlocker RPCs. + */ + bytes raw_macaroon = 2; + + /* + The parsed condition of the macaroon's custom caveat for convenient access. + This field only contains the value of the custom caveat that the handling + middleware has registered itself for. The condition _must_ be validated for + messages of intercept_type stream_auth and request! + */ + string custom_caveat_condition = 3; + + /* + There are three types of messages that will be sent to the middleware for + inspection and approval: Stream authentication, request and response + interception. The first two can only be accepted (=forward to main RPC + server) or denied (=return error to client). Intercepted responses can also + be replaced/overwritten. + */ + oneof intercept_type { + /* + Intercept stream authentication: each new streaming RPC call that is + initiated against lnd and contains the middleware's custom macaroon + caveat can be approved or denied based upon the macaroon in the stream + header. This message will only be sent for streaming RPCs, unary RPCs + must handle the macaroon authentication in the request interception to + avoid an additional message round trip between lnd and the middleware. + */ + StreamAuth stream_auth = 4; + + /* + Intercept incoming gRPC client request message: all incoming messages, + both on streaming and unary RPCs, are forwarded to the middleware for + inspection. For unary RPC messages the middleware is also expected to + validate the custom macaroon caveat of the request. + */ + RPCMessage request = 5; + + /* + Intercept outgoing gRPC response message: all outgoing messages, both on + streaming and unary RPCs, are forwarded to the middleware for inspection + and amendment. The response in this message is the original response as + it was generated by the main RPC server. It can either be accepted + (=forwarded to the client), replaced/overwritten with a new message of + the same type, or replaced by an error message. + */ + RPCMessage response = 6; + + /* + This is used to indicate to the client that the server has successfully + registered the interceptor. This is only used in the very first message + that the server sends to the client after the client sends the server + the middleware registration message. + */ + bool reg_complete = 8; + } + + /* + The unique message ID of this middleware intercept message. There can be + multiple middleware intercept messages per single gRPC request (one for the + incoming request and one for the outgoing response) or gRPC stream (one for + each incoming message and one for each outgoing response). This message ID + must be referenced when responding (accepting/rejecting/modifying) to an + intercept message. + */ + uint64 msg_id = 7; +} + +message StreamAuth { + /* + The full URI (in the format /./MethodName, for + example /lnrpc.Lightning/GetInfo) of the streaming RPC method that was just + established. + */ + string method_full_uri = 1; +} + +message RPCMessage { + /* + The full URI (in the format /./MethodName, for + example /lnrpc.Lightning/GetInfo) of the RPC method the message was sent + to/from. + */ + string method_full_uri = 1; + + /* + Indicates whether the message was sent over a streaming RPC method or not. + */ + bool stream_rpc = 2; + + /* + The full canonical gRPC name of the message type (in the format + .TypeName, for example lnrpc.GetInfoRequest). In case of an + error being returned from lnd, this simply contains the string "error". + */ + string type_name = 3; + + /* + The full content of the gRPC message, serialized in the binary protobuf + format. + */ + bytes serialized = 4; + + /* + Indicates that the response from lnd was an error, not a gRPC response. If + this is set to true then the type_name contains the string "error" and + serialized contains the error string. + */ + bool is_error = 5; +} + +message RPCMiddlewareResponse { + /* + The request message ID this response refers to. Must always be set when + giving feedback to an intercept but is ignored for the initial registration + message. + */ + uint64 ref_msg_id = 1; + + /* + The middleware can only send two types of messages to lnd: The initial + registration message that identifies the middleware and after that only + feedback messages to requests sent to the middleware. + */ + oneof middleware_message { + /* + The registration message identifies the middleware that's being + registered in lnd. The registration message must be sent immediately + after initiating the RegisterRpcMiddleware stream, otherwise lnd will + time out the attempt and terminate the request. NOTE: The middleware + will only receive interception messages for requests that contain a + macaroon with the custom caveat that the middleware declares it is + responsible for handling in the registration message! As a security + measure, _no_ middleware can intercept requests made with _unencumbered_ + macaroons! + */ + MiddlewareRegistration register = 2; + + /* + The middleware received an interception request and gives feedback to + it. The request_id indicates what message the feedback refers to. + */ + InterceptFeedback feedback = 3; + } +} + +message MiddlewareRegistration { + /* + The name of the middleware to register. The name should be as informative + as possible and is logged on registration. + */ + string middleware_name = 1; + + /* + The name of the custom macaroon caveat that this middleware is responsible + for. Only requests/responses that contain a macaroon with the registered + custom caveat are forwarded for interception to the middleware. The + exception being the read-only mode: All requests/responses are forwarded to + a middleware that requests read-only access but such a middleware won't be + allowed to _alter_ responses. As a security measure, _no_ middleware can + change responses to requests made with _unencumbered_ macaroons! + NOTE: Cannot be used at the same time as read_only_mode. + */ + string custom_macaroon_caveat_name = 2; + + /* + Instead of defining a custom macaroon caveat name a middleware can register + itself for read-only access only. In that mode all requests/responses are + forwarded to the middleware but the middleware isn't allowed to alter any of + the responses. + NOTE: Cannot be used at the same time as custom_macaroon_caveat_name. + */ + bool read_only_mode = 3; +} + +message InterceptFeedback { + /* + The error to return to the user. If this is non-empty, the incoming gRPC + stream/request is aborted and the error is returned to the gRPC client. If + this value is empty, it means the middleware accepts the stream/request/ + response and the processing of it can continue. + */ + string error = 1; + + /* + A boolean indicating that the gRPC message should be replaced/overwritten. + This boolean is needed because in protobuf an empty message is serialized as + a 0-length or nil byte slice and we wouldn't be able to distinguish between + an empty replacement message and the "don't replace anything" case. + */ + bool replace_response = 2; + + /* + If the replace_response field is set to true, this field must contain the + binary serialized gRPC message in the protobuf format. + */ + bytes replacement_serialized = 3; +} diff --git a/src/api/pay.ts b/src/api/pay.ts index 4781171..af34458 100644 --- a/src/api/pay.ts +++ b/src/api/pay.ts @@ -1,17 +1,48 @@ -import { FastifyPluginAsync } from "fastify"; +import { FastifyPluginAsync, FastifyReply } from "fastify"; import { Client } from "@grpc/grpc-js"; import crypto from "crypto"; -import { addInvoice } from "../utils/lnd-api"; +import { + addInvoice, + checkPeerConnected, + sendCustomMessage, + SubscribeCustomMessages, +} from "../utils/lnd-api"; import { createPayment } from "../db/payment"; -import { getUserByAlias } from "../db/user"; +import { getUserByAlias, IUserDB } from "../db/user"; import { MSAT } from "../utils/constants"; import getDb from "../db/db"; import config from "../../config/config"; +import { lnrpc } from "../proto"; +import { bytesToString } from "../utils/common"; + +let lnurlPayForwardingRequestCounter = 0; +const lnurlPayForwardingRequests = new Map< + number, + { response: FastifyReply; pubkey: string; alias: string } +>(); + +const LnurlPayRequestLNP2PType = 32768 + 691; + +interface ILnurlPayForwardP2PMessage { + id: number; + request: + | "LNURLPAY_REQUEST1" + | "LNURLPAY_REQUEST1_RESPONSE" + | "LNURLPAY_REQUEST2" + | "LNURLPAY_REQUEST2_RESPONSE"; + data: any; +} const Pay = async function (app, { lightning, router }) { const db = await getDb(); + // This is used for LNURL-pay forwarding to the wallet + const customMessagesSubscription = SubscribeCustomMessages(lightning); + customMessagesSubscription.on("data", async (data) => { + customMessageHandler(data); + }); + app.get<{ Params: { username: string; @@ -27,6 +58,17 @@ const Pay = async function (app, { lightning, router }) { }; } + // If the peer is connected, forward the LNURL-pay request via LN P2P. + if (await checkPeerConnected(lightning, user.pubkey)) { + await handleLnurlPayRequest1Forwarding(lightning, user, response); + return; + } else if (config.disableCustodial) { + return { + status: "ERROR", + reason: `It's not possible pay ${username}@${config.domain} at this time.`, + }; + } + return { tag: "payRequest", callback: `${config.domainUrl}/lightning-address/${username}/send`, @@ -56,6 +98,16 @@ const Pay = async function (app, { lightning, router }) { const { amount, comment } = parseSendTextCallbackQueryParams(request.query); + if (await checkPeerConnected(lightning, user.pubkey)) { + await handleLnurlPayRequest2Forwarding(lightning, user, amount, comment, response); + return; + } else if (config.disableCustodial) { + return { + status: "ERROR", + reason: `Unknown error occured.`, + }; + } + if (comment && comment.length > 144) { console.error("Got invalid comment length"); response.code(400); @@ -128,3 +180,122 @@ function parseSendTextCallbackQueryParams(params: any): ILnUrlPayParams { throw new Error("Could not parse query params"); } } + +function customMessageHandler(data: any) { + console.log("\nINCOMING CUSTOM MESSAGE"); + + try { + const customMessage = lnrpc.CustomMessage.decode(data); + if (customMessage.type !== LnurlPayRequestLNP2PType) { + throw new Error(`Unknown custom message type ${customMessage.type}`); + } + const request = JSON.parse(bytesToString(customMessage.data)) as ILnurlPayForwardP2PMessage; + console.log(request); + + if (request.request === "LNURLPAY_REQUEST1_RESPONSE") { + if (!lnurlPayForwardingRequests.has(request.id)) { + console.error(`Unknown LNURL-pay forwarding request ${request.id}`); + return; + } + const lnurlPayForwardingRequest = lnurlPayForwardingRequests.get(request.id); + lnurlPayForwardingRequests.delete(request.id); + + lnurlPayForwardingRequest?.response.send({ + ...request.data, + callback: `${config.domainUrl}/lightning-address/${lnurlPayForwardingRequest?.alias}/send`, + }); + } else if (request.request === "LNURLPAY_REQUEST2_RESPONSE") { + const customMessage = lnrpc.CustomMessage.decode(data); + const request = JSON.parse(bytesToString(customMessage.data)); + if (!lnurlPayForwardingRequests.has(request.id)) { + console.error(`Unknown LNURL-pay forwarding callback request ${request.id}`); + return; + } + const lnurlPayForwardingRequest = lnurlPayForwardingRequests.get(request.id); + lnurlPayForwardingRequests.delete(request.id); + + lnurlPayForwardingRequest?.response.send(request.data); + } + } catch (error) { + console.error(`Error when handling custom message: ${error.message}`); + } +} + +async function handleLnurlPayRequest1Forwarding( + lightning: Client, + user: IUserDB, + response: FastifyReply, +) { + const currentRequest = lnurlPayForwardingRequestCounter++; + lnurlPayForwardingRequests.set(currentRequest, { + pubkey: user.pubkey, + response, + alias: user.alias, + }); + + const request: ILnurlPayForwardP2PMessage = { + id: currentRequest, + request: "LNURLPAY_REQUEST1", + data: null, + }; + + await sendCustomMessage( + lightning, + user.pubkey, + LnurlPayRequestLNP2PType, + JSON.stringify(request), + ); + + // Timeout after 30 seconds + setTimeout(() => { + if (lnurlPayForwardingRequests.has(currentRequest)) { + lnurlPayForwardingRequests.delete(currentRequest); + } + response.send({ + status: "ERROR", + reason: `It's not possible pay ${user.alias}@${config.domain} at this time.`, + }); + }, 30 * 1000); +} + +async function handleLnurlPayRequest2Forwarding( + lightning: Client, + user: IUserDB, + amount: number, + comment: string | undefined, + response: FastifyReply, +) { + const currentRequest = lnurlPayForwardingRequestCounter++; + lnurlPayForwardingRequests.set(currentRequest, { + pubkey: user.pubkey, + response, + alias: user.alias, + }); + + const request: ILnurlPayForwardP2PMessage = { + id: currentRequest, + request: "LNURLPAY_REQUEST2", + data: { + amount, + comment, + }, + }; + + await sendCustomMessage( + lightning, + user.pubkey, + LnurlPayRequestLNP2PType, + JSON.stringify(request), + ); + + // Timeout after 30 seconds + setTimeout(() => { + if (lnurlPayForwardingRequests.has(currentRequest)) { + lnurlPayForwardingRequests.delete(currentRequest); + } + response.send({ + status: "ERROR", + reason: `It's not possible pay ${user.alias}@${config.domain} at this time.`, + }); + }, 30 * 1000); +} diff --git a/src/utils/common.ts b/src/utils/common.ts index 6c0bc01..b7b13b3 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -10,6 +10,27 @@ export const stringToUint8Array = (str: string) => { return Uint8Array.from(str, (x) => x.charCodeAt(0)); }; +export const bytesToString = (bytes: ArrayLike) => { + return String.fromCharCode.apply(null, bytes as any); +}; + +// TODO function appears to be broken +export function uint8ArrayToUnicodeString(ua: Uint8Array) { + var binstr = Array.prototype.map + .call(ua, function (ch) { + return String.fromCharCode(ch); + }) + .join(""); + var escstr = binstr.replace(/(.)/g, function (m, p) { + var code = p.charCodeAt(p).toString(16).toUpperCase(); + if (code.length < 2) { + code = "0" + code; + } + return "%" + code; + }); + return decodeURIComponent(escstr); +} + export const bytesToHexString = (bytes: Buffer | Uint8Array) => { // console.log("inside bytesToHexString"); // console.log(bytes); @@ -20,7 +41,7 @@ export const bytesToHexString = (bytes: Buffer | Uint8Array) => { export const generateBytes = (n: number): Promise => { return new Promise((resolve, reject) => { - randomBytes(32, function (error, buffer) { + randomBytes(n, function (error, buffer) { if (error) { reject(error); return; diff --git a/src/utils/grpc.ts b/src/utils/grpc.ts index 4f69a82..84e5153 100644 --- a/src/utils/grpc.ts +++ b/src/utils/grpc.ts @@ -30,7 +30,7 @@ export const getGrpcClients = () => { const adminMacaroon = config.backendConfigLnd?.adminMacaroon.replace("~", os.homedir); // console.log(grpcServer, tlsCert, adminMacaroon); const packageDefinition = protoLoader.loadSync( - ["./proto/rpc.proto", "./proto/router.proto"], + ["./proto/lightning.proto", "./proto/router.proto"], loaderOptions, ); const lnrpcProto = loadPackageDefinition(packageDefinition).lnrpc as GrpcObject; diff --git a/src/utils/lnd-api.ts b/src/utils/lnd-api.ts index 5bddb54..766d4aa 100644 --- a/src/utils/lnd-api.ts +++ b/src/utils/lnd-api.ts @@ -219,3 +219,35 @@ export function subscribePeerEvents(lightning: Client) { undefined, ); } + +export async function sendCustomMessage( + lightning: Client, + peerPubkey: string, + type: number, + dataString: string, +) { + const sendCustomMessageRequest = lnrpc.SendCustomMessageRequest.encode({ + peer: hexToUint8Array(peerPubkey), + type, + data: stringToUint8Array(dataString), + }).finish(); + const response = await grpcMakeUnaryRequest( + lightning, + "/lnrpc.Lightning/SendCustomMessage", + sendCustomMessageRequest, + lnrpc.SendCustomMessageResponse.decode, + ); + return response; +} + +export function SubscribeCustomMessages(lightning: Client) { + const request = lnrpc.SubscribeCustomMessagesRequest.encode({}).finish(); + return lightning.makeServerStreamRequest( + "/lnrpc.Lightning/SubscribeCustomMessages", + (arg: any) => arg, + (arg) => arg, + request, + new Metadata(), + undefined, + ); +}