Skip to content
This repository has been archived by the owner on Nov 17, 2023. It is now read-only.

Commit

Permalink
feat(lnd): support route probing for keysend payments
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed Mar 29, 2020
1 parent a34d6b2 commit e4d9438
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 34 deletions.
7 changes: 3 additions & 4 deletions renderer/components/Pay/Pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,15 @@ class Pay extends React.Component {
const isNowSummary =
currentStep === PAY_FORM_STEPS.summary && prevState.currentStep !== PAY_FORM_STEPS.summary
if (isNowSummary) {
let payeeNodeKey
if (invoice) {
;({ payeeNodeKey } = invoice)
const { paymentRequest } = invoice
queryRoutes(paymentRequest, this.amountInSats())
} else if (isPubkey) {
const {
values: { payReq },
} = this.formApi.getState()
payeeNodeKey = payReq
queryRoutes(payReq, this.amountInSats())
}
payeeNodeKey && queryRoutes(payeeNodeKey, this.amountInSats())
}
}

Expand Down
77 changes: 62 additions & 15 deletions renderer/reducers/pay.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { randomBytes, createHash } from 'crypto'
import get from 'lodash/get'
import { createSelector } from 'reselect'
import { send } from 'redux-electron-ipc'
Expand All @@ -8,13 +9,18 @@ import { CoinBig } from '@zap/utils/coin'
import createReducer from '@zap/utils/createReducer'
import { estimateFeeRange } from '@zap/utils/fee'
import { isAutopayEnabled } from '@zap/utils/featureFlag'
import { decodePayReq, getTag } from '@zap/utils/crypto'
import { decodePayReq, isPubkey, getTag } from '@zap/utils/crypto'
import { showError } from './notification'
import { settingsSelectors } from './settings'
import { walletSelectors } from './wallet'
import { infoSelectors } from './info'
import { showAutopayNotification, autopaySelectors } from './autopay'
import { payInvoice } from './payment'
import {
payInvoice,
DEFAULT_CLTV_DELTA,
PREIMAGE_BYTE_LENGTH,
KEYSEND_PREIMAGE_TYPE,
} from './payment'
import { createInvoice } from './invoice'
import messages from './messages'

Expand Down Expand Up @@ -214,36 +220,77 @@ export const queryFees = (address, amountInSats) => async (dispatch, getState) =
/**
* queryRoutes - Find valid routes to make a payment to a node.
*
* @param {object} invoice Decoded bolt11 invoice
* @param {object} payReq Payment request or node pubkey
* @param {number} amt Payment amount (in sats)
* @param {number} finalCltvDelta The number of blocks the last hop has to reveal the preimage
* @returns {Function} Thunk
*/
export const queryRoutes = invoice => async (dispatch, getState) => {
const { payeeNodeKey, millisatoshis } = invoice
const amountInSats = millisatoshis / 1000
export const queryRoutes = (payReq, amt, finalCltvDelta = DEFAULT_CLTV_DELTA) => async (
dispatch,
getState
) => {
const isKeysend = isPubkey(payReq)
let pubkey
let paymentHash

let payload = {
useMissionControl: true,
finalCltvDelta,
}

// Keysend payment.
if (isKeysend) {
pubkey = payReq
const preimage = randomBytes(PREIMAGE_BYTE_LENGTH)
paymentHash = createHash('sha256')
.update(preimage)
.digest()

payload = {
...payload,
amt,
dest: Buffer.from(payReq, 'hex'),
dest_custom_records: {
[KEYSEND_PREIMAGE_TYPE]: preimage,
},
}
}

// Bolt11 invoice payment.
else {
const invoice = decodePayReq(payReq)
const { millisatoshis } = invoice
const amountInSats = millisatoshis / 1000
paymentHash = getTag(invoice, 'payment_hash')
pubkey = invoice.payeeNodeKey

payload = {
...payload,
amt: amountInSats,
dest: Buffer.from(pubkey, 'hex'),
}
}

const callQueryRoutes = async () => {
const { routes } = await grpc.services.Lightning.queryRoutes({
pubKey: payeeNodeKey,
amt: amountInSats,
useMissionControl: true,
...payload,
pubKey: pubkey,
})
return routes
}

const callProbePayment = async () => {
const routes = []
const route = await grpc.services.Router.probePayment({
dest: Buffer.from(payeeNodeKey, 'hex'),
amt: amountInSats,
finalCltvDelta: getTag(invoice, 'min_final_cltv_expiry'),
})
const route = await grpc.services.Router.probePayment(payload)
// Flag this as an exact route. This can be used as a hint for whether to use sendToRoute to fulfil the payment.
route.isExact = true
// Store the payment hash for use with keysnd.
route.paymentHash = paymentHash
routes.push(route)
return routes
}

dispatch({ type: QUERY_ROUTES, pubKey: payeeNodeKey })
dispatch({ type: QUERY_ROUTES, pubKey: pubkey })

try {
let routes = []
Expand Down
26 changes: 14 additions & 12 deletions renderer/reducers/payment.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { networkSelectors } from './network'
import { showError } from './notification'
import messages from './messages'

export const DEFAULT_CLTV_DELTA = 43
export const KEYSEND_PREIMAGE_TYPE = '5482373484'
export const PREIMAGE_BYTE_LENGTH = 32

// ------------------------------------
// Initial State
// ------------------------------------
Expand Down Expand Up @@ -186,7 +190,7 @@ const decPaymentRetry = paymentId => ({
* Controller code that wraps the send action and schedules automatic retries in the case of a failure.
*
* @param {object} options Options
* @param {string} options.payReq Payment request
* @param {string} options.payReq Payment request or node pubkey
* @param {number} options.amt Payment amount (in sats)
* @param {number} options.feeLimit The max fee to apply
* @param {number} options.retries Number of remaining retries
Expand All @@ -213,26 +217,23 @@ export const payInvoice = ({
feeLimit: feeLimit ? { fixed: feeLimit } : null,
allowSelfPayment: true,
}

// Keysend payment.
if (isKeysend) {
const defaultCltvDelta = 43
const keySendPreimageType = '5482373484'
const preimageByteLength = 32

const preimage = randomBytes(preimageByteLength)
pubkey = payReq
const preimage = randomBytes(PREIMAGE_BYTE_LENGTH)
paymentHash = createHash('sha256')
.update(preimage)
.digest()
pubkey = payReq

payload = {
...payload,
paymentHash,
amt,
finalCltvDelta: defaultCltvDelta,
dest: Buffer.from(payReq, 'hex'),
finalCltvDelta: DEFAULT_CLTV_DELTA,
dest: Buffer.from(pubkey, 'hex'),
destCustomRecords: {
[keySendPreimageType]: preimage,
[KEYSEND_PREIMAGE_TYPE]: preimage,
},
}
}
Expand All @@ -246,7 +247,7 @@ export const payInvoice = ({
paymentRequest = invoice.paymentRequest // eslint-disable-line prefer-destructuring
payload = {
...payload,
amt: !millisatoshis && amt,
amt: millisatoshis ? null : amt,
paymentRequest,
}
}
Expand Down Expand Up @@ -289,8 +290,9 @@ export const payInvoice = ({
try {
const routeToUse = { ...route }
delete routeToUse.isExact
delete routeToUse.paymentHash
result = await grpc.services.Router.sendToRoute({
paymentHash: Buffer.from(paymentHash, 'hex'),
paymentHash: route.paymentHash ? Buffer.from(route.paymentHash, 'hex') : null,
route: routeToUse,
})
} catch (error) {
Expand Down
19 changes: 16 additions & 3 deletions services/grpc/router.methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const PAYMENT_FEE_LIMIT = config.payments.feeLimit
const PAYMENT_PROBE_TIMEOUT = config.payments.probeTimeout
const PAYMENT_PROBE_FEE_LIMIT = config.payments.probeFeeLimit

export const KEYSEND_PREIMAGE_TYPE = '5482373484'

// ------------------------------------
// Wrappers / Overrides
// ------------------------------------
Expand All @@ -23,14 +25,16 @@ const PAYMENT_PROBE_FEE_LIMIT = config.payments.probeFeeLimit
* @returns {Promise} The route route when state is SUCCEEDED
*/
async function probePayment(options) {
// Use a payload that has the payment hash set to some random bytes.
// This will cause the payment to fail at the final destination.
const payload = defaults(omitBy(options, isNil), {
payment_hash: new Uint8Array(randomBytes(32)),
timeout_seconds: PAYMENT_PROBE_TIMEOUT,
fee_limit_sat: PAYMENT_PROBE_FEE_LIMIT,
allow_self_payment: true,
})

// Use a payload that has the payment hash set to some random bytes.
// This will cause the payment to fail at the final destination.
payload.payment_hash = new Uint8Array(randomBytes(32))

logGrpcCmd('Router.probePayment', payload)

let result
Expand All @@ -55,6 +59,15 @@ async function probePayment(options) {

case 'FAILED_INCORRECT_PAYMENT_DETAILS':
grpcLog.info('PROBE SUCCESS: %o', data)
// FIXME: For some reason the custom_records key is corrupt in the grpc response object.
// For now, assume that if a custom_record key is set that it is a keysend record and fix it accordingly.
data.route.hops = data.route.hops.map(hop => {
Object.keys(hop.custom_records).forEach(key => {
hop.custom_records[KEYSEND_PREIMAGE_TYPE] = hop.custom_records[key]
delete hop.custom_records[key]
})
return hop
})
result = data.route
break

Expand Down

0 comments on commit e4d9438

Please sign in to comment.