Skip to content

Commit

Permalink
fix: issues with invoices (#271)
Browse files Browse the repository at this point in the history
* fix: issues with invoices

* chore: add invoice event tag

* chore: add sub limits

* chore: cleanup invoices page

* chore: use mergeDeepLeft when updating invoice

* chore: ignore whitelisted pubkey for adminssion fee

* chore: use secp256k1 bytesToHex

* fix: insecure derivation from secret

* fix: tests

* chore: consistent returns

* test: fix intg tests

* fix: intg tests

* chore: set SECRET for intg tests
  • Loading branch information
cameri authored Apr 7, 2023
1 parent f237400 commit e1561e7
Show file tree
Hide file tree
Showing 28 changed files with 212 additions and 180 deletions.
8 changes: 6 additions & 2 deletions resources/default-settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ limits:
- 10
- - 40
- 49
maxLength: 65536
maxLength: 102400
- description: 96 KB for event kind ranges 11-39 and 50-max
kinds:
- - 11
- 39
- - 50
- 9007199254740991
maxLength: 98304
maxLength: 102400
rateLimits:
- description: 6 events/min for event kinds 0, 3, 40 and 41
kinds:
Expand Down Expand Up @@ -143,6 +143,10 @@ limits:
subscription:
maxSubscriptions: 10
maxFilters: 10
maxFilterValues: 2500
maxSubscriptionIdLength: 256
maxLimit: 5000
minPrefixLength: 4
message:
rateLimits:
- description: 240 raw messages/min
Expand Down
26 changes: 18 additions & 8 deletions resources/invoices.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,11 @@ <h2 class="text-danger">Invoice expired!</h2>
var timeout
var paid = false
var fallbackTimeout
var now = Math.floor(Date.now()/1000)

console.log('invoice id', reference)
console.log('pubkey', pubkey)
console.log('bolt11', invoice)

function getBackoffTime() {
return 5000 + Math.floor(Math.random() * 5000)
Expand Down Expand Up @@ -149,7 +152,8 @@ <h2 class="text-danger">Invoice expired!</h2>
var socket = new WebSocket(relayUrl)
socket.onopen = () => {
console.log('connected')
socket.send(JSON.stringify(['REQ', 'payment', { kinds: [4], authors: [relayPubkey], '#c': [reference], limit: 1 }]))
var subscription = ['REQ', 'payment', { kinds: [402], '#p': [pubkey], since: now - 60 }]
socket.send(JSON.stringify(subscription))
}

socket.onmessage = (raw) => {
Expand All @@ -162,16 +166,22 @@ <h2 class="text-danger">Invoice expired!</h2>

switch (message[0]) {
case 'EVENT': {
// TODO: validate event
const event = message[2]
// TODO: validate signature
if (event.pubkey === relayPubkey) {
paid = true
if (
event.pubkey === relayPubkey
&& event.kind === 402
) {
const pubkeyTag = event.tags.find((t) => t[0] === 'p' && t[1] === pubkey)
const invoiceTag = event.tags.find((t) => t[0] === 'bolt11' && t[1] === invoice)

if (expiresAt) clearTimeout(timeout)
if (pubkeyTag && invoiceTag) {
paid = true

hide('pending')
show('paid')
if (expiresAt) clearTimeout(timeout)

hide('pending')
show('paid')
}
}
}
break;
Expand Down
14 changes: 10 additions & 4 deletions src/@types/base.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { Knex } from 'knex'
import { SocketAddress } from 'net'

import { EventTags } from '../constants/base'

export type EventId = string
export type Pubkey = string
export type TagName = string
export type TagName = EventTags | string
export type Signature = string
export type Tag = TagBase & string[]

export type Secret = string

export interface TagBase {
0: TagName
[index: number]: string
type ExtraTagValues = {
[index in Range<2, 100>]?: string
}

export interface TagBase extends ExtraTagValues {
0: TagName;
1: string
}

type Enumerate<
Expand Down
1 change: 0 additions & 1 deletion src/@types/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export interface IPaymentsService {
confirmInvoice(
invoice: Pick<Invoice, 'id' | 'amountPaid' | 'confirmedAt'>,
): Promise<void>
sendNewInvoiceNotification(invoice: Invoice): Promise<void>
sendInvoiceUpdateNotification(invoice: Invoice): Promise<void>
getPendingInvoices(): Promise<Invoice[]>
}
4 changes: 4 additions & 0 deletions src/@types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export interface EventLimits {
export interface ClientSubscriptionLimits {
maxSubscriptions?: number
maxFilters?: number
maxFilterValues?: number
maxLimit?: number
minPrefixLength?: number
maxSubscriptionIdLength?: number
}

export interface ClientLimits {
Expand Down
18 changes: 12 additions & 6 deletions src/app/maintenance-worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mergeDeepLeft, path, pipe } from 'ramda'
import { IRunnable } from '../@types/base'
import { path } from 'ramda'

import { createLogger } from '../factories/logger-factory'
import { delayMs } from '../utils/misc'
Expand Down Expand Up @@ -47,21 +47,27 @@ export class MaintenanceWorker implements IRunnable {
for (const invoice of invoices) {
debug('invoice %s: %o', invoice.id, invoice)
try {
debug('getting invoice %s from payment processor', invoice.id)
debug('getting invoice %s from payment processor: %o', invoice.id, invoice)
const updatedInvoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(invoice)
await delay()
debug('updating invoice status %s: %o', invoice.id, invoice)
debug('updating invoice status %s: %o', updatedInvoice.id, updatedInvoice)
await this.paymentsService.updateInvoiceStatus(updatedInvoice)

if (
invoice.status !== updatedInvoice.status
&& updatedInvoice.status == InvoiceStatus.COMPLETED
&& invoice.confirmedAt
&& updatedInvoice.confirmedAt
) {
debug('confirming invoice %s & notifying %s', invoice.id, invoice.pubkey)

const update = pipe(
mergeDeepLeft(updatedInvoice),
mergeDeepLeft({ amountPaid: invoice.amountRequested }),
)(invoice)

await Promise.all([
this.paymentsService.confirmInvoice(invoice),
this.paymentsService.sendInvoiceUpdateNotification(invoice),
this.paymentsService.confirmInvoice(update),
this.paymentsService.sendInvoiceUpdateNotification(update),
])

await delay()
Expand Down
2 changes: 1 addition & 1 deletion src/constants/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export enum EventKinds {
PARAMETERIZED_REPLACEABLE_FIRST = 30000,
PARAMETERIZED_REPLACEABLE_LAST = 39999,
USER_APPLICATION_FIRST = 40000,
USER_APPLICATION_LAST = Number.MAX_SAFE_INTEGER,
}

export enum EventTags {
Expand All @@ -37,6 +36,7 @@ export enum EventTags {
Delegation = 'delegation',
Deduplication = 'd',
Expiration = 'expiration',
Invoice = 'bolt11',
}

export enum PaymentsProcessors {
Expand Down
9 changes: 6 additions & 3 deletions src/controllers/invoices/post-invoice-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ export class PostInvoiceController implements IController {
}

let invoice: Invoice
const amount = admissionFee.reduce((sum, fee) => sum + BigInt(fee.amount), 0n)
const amount = admissionFee.reduce((sum, fee) => {
return fee.enabled && !fee.whitelists?.pubkeys?.includes(pubkey)
? BigInt(fee.amount) + sum
: sum
}, 0n)

try {
const description = `${relayName} Admission Fee for ${toBech32('npub')(pubkey)}`

Expand All @@ -145,8 +150,6 @@ export class PostInvoiceController implements IController {
amount,
description,
)

await this.paymentsService.sendNewInvoiceNotification(invoice)
} catch (error) {
console.error('Unable to create invoice. Reason:', error)
response
Expand Down
18 changes: 17 additions & 1 deletion src/handlers/event-message-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Event, ExpiringEvent } from '../@types/event'
import { EventRateLimit, FeeSchedule, Settings } from '../@types/settings'
import { getEventExpiration, getEventProofOfWork, getPubkeyProofOfWork, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid, isExpiredEvent } from '../utils/event'
import { getEventExpiration, getEventProofOfWork, getPubkeyProofOfWork, getPublicKey, getRelayPrivateKey, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid, isExpiredEvent } from '../utils/event'
import { IEventStrategy, IMessageHandler } from '../@types/message-handlers'
import { ContextMetadataKey } from '../constants/base'
import { createCommandResult } from '../utils/messages'
Expand Down Expand Up @@ -79,7 +79,15 @@ export class EventMessageHandler implements IMessageHandler {
}
}

protected getRelayPublicKey(): string {
const relayPrivkey = getRelayPrivateKey(this.settings().info.relay_url)
return getPublicKey(relayPrivkey)
}

protected canAcceptEvent(event: Event): string | undefined {
if (this.getRelayPublicKey() === event.pubkey) {
return
}
const now = Math.floor(Date.now()/1000)

const limits = this.settings().limits?.event ?? {}
Expand Down Expand Up @@ -185,6 +193,10 @@ export class EventMessageHandler implements IMessageHandler {
}

protected async isRateLimited(event: Event): Promise<boolean> {
if (this.getRelayPublicKey() === event.pubkey) {
return false
}

const { whitelists, rateLimits } = this.settings().limits?.event ?? {}
if (!rateLimits || !rateLimits.length) {
return false
Expand Down Expand Up @@ -249,6 +261,10 @@ export class EventMessageHandler implements IMessageHandler {
return
}

if (this.getRelayPublicKey() === event.pubkey) {
return
}

const isApplicableFee = (feeSchedule: FeeSchedule) =>
feeSchedule.enabled
&& !feeSchedule.whitelists?.pubkeys?.some((prefix) => event.pubkey.startsWith(prefix))
Expand Down
14 changes: 9 additions & 5 deletions src/handlers/request-handlers/root-request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const rootRequestHandler = (request: Request, response: Response, next: N
paymentsUrl.protocol = paymentsUrl.protocol === 'wss:' ? 'https:' : 'http:'
paymentsUrl.pathname = '/invoices'

const content = settings.limits?.event?.content

const relayInformationDocument = {
name,
description,
Expand All @@ -29,12 +31,14 @@ export const rootRequestHandler = (request: Request, response: Response, next: N
limitation: {
max_message_length: settings.network.maxPayloadSize,
max_subscriptions: settings.limits?.client?.subscription?.maxSubscriptions,
max_filters: settings.limits?.client?.subscription?.maxFilters,
max_limit: 5000,
max_subid_length: 256,
min_prefix: 4,
max_filters: settings.limits?.client?.subscription?.maxFilterValues,
max_limit: settings.limits?.client?.subscription?.maxLimit,
max_subid_length: settings.limits?.client?.subscription?.maxSubscriptionIdLength,
min_prefix: settings.limits?.client?.subscription?.minPrefixLength,
max_event_tags: 2500,
max_content_length: 102400,
max_content_length: Array.isArray(content)
? content[0].maxLength // best guess since we have per-kind limits
: content?.maxLength,
min_pow_difficulty: settings.limits?.event?.eventId?.minLeadingZeroBits,
auth_required: false,
payment_required: settings.payments?.enabled,
Expand Down
14 changes: 12 additions & 2 deletions src/handlers/subscribe-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,23 +87,33 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
private canSubscribe(subscriptionId: SubscriptionId, filters: SubscriptionFilter[]): string | undefined {
const subscriptions = this.webSocket.getSubscriptions()
const existingSubscription = subscriptions.get(subscriptionId)
const subscriptionLimits = this.settings().limits?.client?.subscription

if (existingSubscription?.length && equals(filters, existingSubscription)) {
return `Duplicate subscription ${subscriptionId}: Ignorning`
}

const maxSubscriptions = this.settings().limits?.client?.subscription?.maxSubscriptions ?? 0
const maxSubscriptions = subscriptionLimits?.maxSubscriptions ?? 0
if (maxSubscriptions > 0
&& !existingSubscription?.length && subscriptions.size + 1 > maxSubscriptions
) {
return `Too many subscriptions: Number of subscriptions must be less than or equal to ${maxSubscriptions}`
}

const maxFilters = this.settings().limits?.client?.subscription?.maxFilters ?? 0
const maxFilters = subscriptionLimits?.maxFilters ?? 0
if (maxFilters > 0) {
if (filters.length > maxFilters) {
return `Too many filters: Number of filters per susbscription must be less then or equal to ${maxFilters}`
}
}

if (
typeof subscriptionLimits.maxSubscriptionIdLength === 'number'
&& subscriptionId.length > subscriptionLimits.maxSubscriptionIdLength
) {
return `Subscription ID too long: Subscription ID must be less or equal to ${subscriptionLimits.maxSubscriptionIdLength}`
}


}
}
3 changes: 2 additions & 1 deletion src/payments-processors/lnurl-payments-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export class LnurlPaymentsProcesor implements IPaymentsProcessor {

return {
id: invoice.id,
status: response.data.settled ? InvoiceStatus['COMPLETED'] : InvoiceStatus['PENDING'],
confirmedAt: response.data.settled ? new Date() : undefined,
status: response.data.settled ? InvoiceStatus.COMPLETED : InvoiceStatus.PENDING,
}
} catch (error) {
console.error(`Unable to get invoice ${invoice.id}. Reason:`, error)
Expand Down
3 changes: 1 addition & 2 deletions src/schemas/base-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const kindSchema = Schema.number().min(0).multiple(1).label('kind')

export const signatureSchema = Schema.string().case('lower').hex().length(128).label('sig')

export const subscriptionSchema = Schema.string().min(1).max(255).label('subscriptionId')
export const subscriptionSchema = Schema.string().min(1).label('subscriptionId')

const seconds = (value: any, helpers: any) => (Number.isSafeInteger(value) && Math.log10(value) < 10) ? value : helpers.error('any.invalid')

Expand All @@ -20,5 +20,4 @@ export const createdAtSchema = Schema.number().min(0).multiple(1).custom(seconds
export const tagSchema = Schema.array()
.ordered(Schema.string().max(255).required().label('identifier'))
.items(Schema.string().allow('').max(1024).label('value'))
.max(10)
.label('tag')
3 changes: 1 addition & 2 deletions src/schemas/event-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ export const eventSchema = Schema.object({
pubkey: pubkeySchema.required(),
created_at: createdAtSchema.required(),
kind: kindSchema.required(),
tags: Schema.array().items(tagSchema).max(2500).required(),
tags: Schema.array().items(tagSchema).required(),
content: Schema.string()
.allow('')
.max(100 * 1024) // 100 kB
.required(),
sig: signatureSchema.required(),
}).unknown(false)
10 changes: 5 additions & 5 deletions src/schemas/filter-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import Schema from 'joi'
import { createdAtSchema, kindSchema, prefixSchema } from './base-schema'

export const filterSchema = Schema.object({
ids: Schema.array().items(prefixSchema.label('prefixOrId')).max(1000),
authors: Schema.array().items(prefixSchema.label('prefixOrAuthor')).max(1000),
kinds: Schema.array().items(kindSchema).max(20),
ids: Schema.array().items(prefixSchema.label('prefixOrId')),
authors: Schema.array().items(prefixSchema.label('prefixOrAuthor')),
kinds: Schema.array().items(kindSchema),
since: createdAtSchema,
until: createdAtSchema,
limit: Schema.number().min(0).multiple(1).max(5000),
}).pattern(/^#[a-z]$/, Schema.array().items(Schema.string().max(1024)).max(256))
limit: Schema.number().min(0).multiple(1),
}).pattern(/^#[a-z]$/, Schema.array().items(Schema.string().max(1024)))
Loading

0 comments on commit e1561e7

Please sign in to comment.