From e1561e78fd24b76052cf40f30934f116b02073d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Arturo=20Cabral=20Mej=C3=ADa?= Date: Fri, 7 Apr 2023 12:48:28 -0400 Subject: [PATCH] fix: issues with invoices (#271) * 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 --- resources/default-settings.yaml | 8 +- resources/invoices.html | 26 +++-- src/@types/base.ts | 14 ++- src/@types/services.ts | 1 - src/@types/settings.ts | 4 + src/app/maintenance-worker.ts | 18 ++- src/constants/base.ts | 2 +- .../invoices/post-invoice-controller.ts | 9 +- src/handlers/event-message-handler.ts | 18 ++- .../request-handlers/root-request-handler.ts | 14 ++- src/handlers/subscribe-message-handler.ts | 14 ++- .../lnurl-payments-processor.ts | 3 +- src/schemas/base-schema.ts | 3 +- src/schemas/event-schema.ts | 3 +- src/schemas/filter-schema.ts | 10 +- src/services/payments-service.ts | 107 ++++-------------- src/utils/event.ts | 26 ++++- src/utils/secret.ts | 12 +- src/utils/sliding-window-rate-limiter.ts | 2 - test/integration/features/helpers.ts | 2 +- .../features/nip-01/nip-01.feature | 1 - .../features/nip-16/nip-16.feature | 11 +- test/integration/features/shared.ts | 25 ++-- .../delegated-event-message-handler.spec.ts | 5 +- .../handlers/event-message-handler.spec.ts | 19 +++- .../repositories/event-repository.spec.ts | 10 +- test/unit/schemas/event-schema.spec.ts | 16 +-- test/unit/schemas/filter-schema.spec.ts | 9 +- 28 files changed, 212 insertions(+), 180 deletions(-) diff --git a/resources/default-settings.yaml b/resources/default-settings.yaml index a27ddd32..fac389c8 100755 --- a/resources/default-settings.yaml +++ b/resources/default-settings.yaml @@ -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: @@ -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 diff --git a/resources/invoices.html b/resources/invoices.html index 48ceca3c..7682a0cb 100644 --- a/resources/invoices.html +++ b/resources/invoices.html @@ -107,8 +107,11 @@

Invoice expired!

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) @@ -149,7 +152,8 @@

Invoice expired!

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) => { @@ -162,16 +166,22 @@

Invoice expired!

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; diff --git a/src/@types/base.ts b/src/@types/base.ts index 7065832b..78808cf5 100644 --- a/src/@types/base.ts +++ b/src/@types/base.ts @@ -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< diff --git a/src/@types/services.ts b/src/@types/services.ts index 8058f5ae..9a151375 100644 --- a/src/@types/services.ts +++ b/src/@types/services.ts @@ -13,7 +13,6 @@ export interface IPaymentsService { confirmInvoice( invoice: Pick, ): Promise - sendNewInvoiceNotification(invoice: Invoice): Promise sendInvoiceUpdateNotification(invoice: Invoice): Promise getPendingInvoices(): Promise } diff --git a/src/@types/settings.ts b/src/@types/settings.ts index a0ddec7c..c7920d93 100644 --- a/src/@types/settings.ts +++ b/src/@types/settings.ts @@ -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 { diff --git a/src/app/maintenance-worker.ts b/src/app/maintenance-worker.ts index 4570a072..4c929c8f 100644 --- a/src/app/maintenance-worker.ts +++ b/src/app/maintenance-worker.ts @@ -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' @@ -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() diff --git a/src/constants/base.ts b/src/constants/base.ts index e23f4bb6..37bce50e 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -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 { @@ -37,6 +36,7 @@ export enum EventTags { Delegation = 'delegation', Deduplication = 'd', Expiration = 'expiration', + Invoice = 'bolt11', } export enum PaymentsProcessors { diff --git a/src/controllers/invoices/post-invoice-controller.ts b/src/controllers/invoices/post-invoice-controller.ts index 646ba85e..7fe3a027 100644 --- a/src/controllers/invoices/post-invoice-controller.ts +++ b/src/controllers/invoices/post-invoice-controller.ts @@ -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)}` @@ -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 diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index bbea8775..07264656 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -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' @@ -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 ?? {} @@ -185,6 +193,10 @@ export class EventMessageHandler implements IMessageHandler { } protected async isRateLimited(event: Event): Promise { + if (this.getRelayPublicKey() === event.pubkey) { + return false + } + const { whitelists, rateLimits } = this.settings().limits?.event ?? {} if (!rateLimits || !rateLimits.length) { return false @@ -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)) diff --git a/src/handlers/request-handlers/root-request-handler.ts b/src/handlers/request-handlers/root-request-handler.ts index dc0073b1..d9450334 100644 --- a/src/handlers/request-handlers/root-request-handler.ts +++ b/src/handlers/request-handlers/root-request-handler.ts @@ -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, @@ -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, diff --git a/src/handlers/subscribe-message-handler.ts b/src/handlers/subscribe-message-handler.ts index abb47ec3..f85c2a5f 100644 --- a/src/handlers/subscribe-message-handler.ts +++ b/src/handlers/subscribe-message-handler.ts @@ -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}` + } + + } } diff --git a/src/payments-processors/lnurl-payments-processor.ts b/src/payments-processors/lnurl-payments-processor.ts index 7c0b275c..30b412e3 100644 --- a/src/payments-processors/lnurl-payments-processor.ts +++ b/src/payments-processors/lnurl-payments-processor.ts @@ -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) diff --git a/src/schemas/base-schema.ts b/src/schemas/base-schema.ts index 334ebdd8..fe24136f 100644 --- a/src/schemas/base-schema.ts +++ b/src/schemas/base-schema.ts @@ -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') @@ -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') diff --git a/src/schemas/event-schema.ts b/src/schemas/event-schema.ts index 10141020..fab6520b 100644 --- a/src/schemas/event-schema.ts +++ b/src/schemas/event-schema.ts @@ -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) diff --git a/src/schemas/filter-schema.ts b/src/schemas/filter-schema.ts index 1fd2fed9..f67453f2 100644 --- a/src/schemas/filter-schema.ts +++ b/src/schemas/filter-schema.ts @@ -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))) diff --git a/src/services/payments-service.ts b/src/services/payments-service.ts index 7e03e067..312b5b0d 100644 --- a/src/services/payments-service.ts +++ b/src/services/payments-service.ts @@ -1,17 +1,16 @@ -import { andThen, pipe } from 'ramda' -import { broadcastEvent, encryptKind4Event, getPublicKey, getRelayPrivateKey, identifyEvent, signEvent } from '../utils/event' +import { andThen, otherwise, pipe } from 'ramda' +import { broadcastEvent, getPublicKey, getRelayPrivateKey, identifyEvent, signEvent } from '../utils/event' import { DatabaseClient, Pubkey } from '../@types/base' import { FeeSchedule, Settings } from '../@types/settings' import { IEventRepository, IInvoiceRepository, IUserRepository } from '../@types/repositories' import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice' +import { Event, ExpiringEvent, UnidentifiedEvent } from '../@types/event' +import { EventExpirationTimeMetadataKey, EventKinds, EventTags } from '../constants/base' import { createLogger } from '../factories/logger-factory' -import { EventKinds } from '../constants/base' import { IPaymentsProcessor } from '../@types/clients' import { IPaymentsService } from '../@types/services' -import { toBech32 } from '../utils/transform' import { Transaction } from '../database/transaction' -import { UnidentifiedEvent } from '../@types/event' const debug = createLogger('payments-service') @@ -54,7 +53,7 @@ export class PaymentsService implements IPaymentsService { amount: bigint, description: string, ): Promise { - debug('create invoice for %s for %s: %d', pubkey, amount.toString(), description) + debug('create invoice for %s for %s: %s', pubkey, amount.toString(), description) const transaction = new Transaction(this.dbClient) try { @@ -220,68 +219,6 @@ export class PaymentsService implements IPaymentsService { } } - public async sendNewInvoiceNotification(invoice: Invoice): Promise { - debug('invoice created notification %s: %o', invoice.id, invoice) - const currentSettings = this.settings() - - const { - info: { - relay_url: relayUrl, - name: relayName, - }, - } = currentSettings - - const relayPrivkey = getRelayPrivateKey(relayUrl) - const relayPubkey = getPublicKey(relayPrivkey) - - let unit: string = invoice.unit - let amount: bigint = invoice.amountRequested - if (invoice.unit === InvoiceUnit.MSATS) { - amount /= 1000n - unit = 'sats' - } - - const url = new URL(relayUrl) - - const terms = new URL(relayUrl) - terms.protocol = ['https', 'wss'].includes(url.protocol) - ? 'https' - : 'http' - terms.pathname += 'terms' - - const unsignedInvoiceEvent: UnidentifiedEvent = { - pubkey: relayPubkey, - kind: EventKinds.ENCRYPTED_DIRECT_MESSAGE, - created_at: Math.floor(invoice.createdAt.getTime() / 1000), - content: `From: ${toBech32('npub')(relayPubkey)}@${url.hostname} (${relayName}) -To: ${toBech32('npub')(invoice.pubkey)}@${url.hostname} -🧾 Admission Fee Invoice - -Amount: ${amount.toString()} ${unit} - -⚠️ By paying this invoice, you confirm that you have read and agree to the Terms of Service: -${terms.toString()} -${invoice.expiresAt ? ` -⏳ Expires at ${invoice.expiresAt.toISOString()}` : ''} - -${invoice.bolt11}`, - tags: [ - ['p', invoice.pubkey], - ['bolt11', invoice.bolt11], - ], - } - - const persistEvent = this.eventRepository.create.bind(this.eventRepository) - - await pipe( - identifyEvent, - andThen(encryptKind4Event(relayPrivkey, invoice.pubkey)), - andThen(signEvent(relayPrivkey)), - andThen(broadcastEvent), - andThen(persistEvent), - )(unsignedInvoiceEvent) - } - public async sendInvoiceUpdateNotification(invoice: Invoice): Promise { debug('invoice updated notification %s: %o', invoice.id, invoice) const currentSettings = this.settings() @@ -289,7 +226,6 @@ ${invoice.bolt11}`, const { info: { relay_url: relayUrl, - name: relayName, }, } = currentSettings @@ -309,31 +245,36 @@ ${invoice.bolt11}`, unit = InvoiceUnit.SATS } - const url = new URL(relayUrl) + const now = new Date() + const expiration = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate()) - const unsignedInvoiceEvent: UnidentifiedEvent = { + const unsignedInvoiceEvent: UnidentifiedEvent & Pick = { pubkey: relayPubkey, - kind: EventKinds.ENCRYPTED_DIRECT_MESSAGE, - created_at: Math.floor(invoice.createdAt.getTime() / 1000), - content: `🧾 Admission Fee Invoice Paid for ${relayPubkey}@${url.hostname} (${relayName}) - -Amount received: ${amount.toString()} ${unit} - -Thanks!`, + kind: EventKinds.INVOICE_UPDATE, + created_at: Math.floor(now.getTime() / 1000), + content: `Invoice paid: ${amount.toString()} ${unit}`, tags: [ - ['p', invoice.pubkey], - ['c', invoice.id], + [EventTags.Pubkey, invoice.pubkey], + [EventTags.Invoice, invoice.bolt11], + [EventTags.Expiration, Math.floor(expiration.getTime() / 1000).toString()], ], + [EventExpirationTimeMetadataKey]: expiration.getTime() / 1000, } - const persistEvent = this.eventRepository.create.bind(this.eventRepository) + const persistEvent = async (event: Event) => { + await this.eventRepository.create(event) + + return event + } + + const logError = (error: Error) => console.error('Unable to send notification', error) await pipe( identifyEvent, - andThen(encryptKind4Event(relayPrivkey, invoice.pubkey)), andThen(signEvent(relayPrivkey)), - andThen(broadcastEvent), andThen(persistEvent), + andThen(broadcastEvent), + otherwise(logError), )(unsignedInvoiceEvent) } } diff --git a/src/utils/event.ts b/src/utils/event.ts index 577bb478..cf069d6d 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -181,15 +181,33 @@ export const identifyEvent = async (event: UnidentifiedEvent): Promise Buffer.from(secp256k1.getPublicKey(privkey, true)).subarray(1).toString('hex') +const publicKeyCache: Record = {} +export const getPublicKey = (privkey: string) => { + if (privkey in publicKeyCache) { + return publicKeyCache[privkey] + } + + publicKeyCache[privkey] = secp256k1.utils.bytesToHex(secp256k1.getPublicKey(privkey, true).subarray(1)) + + return publicKeyCache[privkey] +} export const signEvent = (privkey: string | Buffer | undefined) => async (event: UnsignedEvent): Promise => { const sig = await secp256k1.schnorr.sign(event.id, privkey as any) diff --git a/src/utils/secret.ts b/src/utils/secret.ts index 4260ec99..16b7fd28 100644 --- a/src/utils/secret.ts +++ b/src/utils/secret.ts @@ -1,11 +1,15 @@ import { createHmac } from 'crypto' export function deriveFromSecret(purpose: string | Buffer): Buffer { - return hmacSha256(process.env.SECRET as string, purpose) + if (!process.env.SECRET) { + throw new Error('SECRET environment variable not set') + } + + return hmacSha256(process.env.SECRET, purpose) } export function hmacSha256(secret: string | Buffer, data: string | Buffer): Buffer { - return createHmac('sha256', secret) - .update(data) - .digest() + return createHmac('sha256', secret) + .update(data) + .digest() } diff --git a/src/utils/sliding-window-rate-limiter.ts b/src/utils/sliding-window-rate-limiter.ts index fdc3fdf6..7dcb62a9 100644 --- a/src/utils/sliding-window-rate-limiter.ts +++ b/src/utils/sliding-window-rate-limiter.ts @@ -17,8 +17,6 @@ export class SlidingWindowRateLimiter implements IRateLimiter { const timestamp = Date.now() const { period } = options - debug('add %d hits on %s bucket', step, key) - const [,, entries] = await Promise.all([ this.cache.removeRangeByScoreFromSortedSet(key, 0, timestamp - period), this.cache.addToSortedSet(key, { [`${timestamp}:${step}`]: timestamp.toString() }), diff --git a/test/integration/features/helpers.ts b/test/integration/features/helpers.ts index 26a2e379..34dc1653 100644 --- a/test/integration/features/helpers.ts +++ b/test/integration/features/helpers.ts @@ -54,7 +54,7 @@ export async function createEvent(input: Partial, privkey: any): Promise< } export function createIdentity(name: string) { - const hmac = createHmac('sha256', process.env.SECRET ?? Math.random().toString()) + const hmac = createHmac('sha256', Math.random().toString()) hmac.update(name) const privkey = hmac.digest().toString('hex') const pubkey = Buffer.from(secp256k1.getPublicKey(privkey, true)).toString('hex').substring(2) diff --git a/test/integration/features/nip-01/nip-01.feature b/test/integration/features/nip-01/nip-01.feature index e8d7264a..be0c2ca9 100644 --- a/test/integration/features/nip-01/nip-01.feature +++ b/test/integration/features/nip-01/nip-01.feature @@ -72,7 +72,6 @@ Feature: NIP-01 And Alice subscribes to text_note events from Bob and set_metadata events from Charlie Then Alice receives 2 events from Bob and Charlie - @test Scenario: Alice is interested in Bob's events from back in November Given someone called Alice And someone called Bob diff --git a/test/integration/features/nip-16/nip-16.feature b/test/integration/features/nip-16/nip-16.feature index 53225b41..780e7ee7 100644 --- a/test/integration/features/nip-16/nip-16.feature +++ b/test/integration/features/nip-16/nip-16.feature @@ -12,9 +12,10 @@ Feature: NIP-16 Event treatment Scenario: Charlie sends an ephemeral event Given someone called Charlie - And Charlie subscribes to author Charlie + Given someone called Alice + And Alice subscribes to author Charlie When Charlie sends a ephemeral_event_0 event with content "now you see me" - Then Charlie receives a ephemeral_event_0 event from Charlie with content "now you see me" - Then Charlie unsubscribes from author Charlie - When Charlie subscribes to author Charlie - Then Charlie receives 0 ephemeral_event_0 events and EOSE + Then Alice receives a ephemeral_event_0 event from Charlie with content "now you see me" + Then Alice unsubscribes from author Charlie + When Alice subscribes to author Charlie + Then Alice receives 0 ephemeral_event_0 events and EOSE diff --git a/test/integration/features/shared.ts b/test/integration/features/shared.ts index 38aaa854..83281d05 100644 --- a/test/integration/features/shared.ts +++ b/test/integration/features/shared.ts @@ -35,6 +35,7 @@ export const streams = new WeakMap>() BeforeAll({ timeout: 1000 }, async function () { process.env.RELAY_PORT = '18808' + process.env.SECRET = Math.random().toString().repeat(6) cacheClient = getCacheClient() dbClient = getMasterDbClient() rrDbClient = getReadReplicaDbClient() @@ -43,11 +44,12 @@ BeforeAll({ timeout: 1000 }, async function () { const settings = SettingsStatic.createSettings() SettingsStatic._settings = pipe( - assocPath( ['limits', 'event', 'createdAt', 'maxPositiveDelta'], 0), - assocPath( ['limits', 'message', 'rateLimits'], []), - assocPath( ['limits', 'event', 'rateLimits'], []), - assocPath( ['limits', 'invoice', 'rateLimits'], []), - assocPath( ['limits', 'connection', 'rateLimits'], []), + assocPath(['payments', 'enabled'], false), + assocPath(['limits', 'event', 'createdAt', 'maxPositiveDelta'], 0), + assocPath(['limits', 'message', 'rateLimits'], []), + assocPath(['limits', 'event', 'rateLimits'], []), + assocPath(['limits', 'invoice', 'rateLimits'], []), + assocPath(['limits', 'connection', 'rateLimits'], []), )(settings) as any worker = workerFactory() @@ -80,11 +82,10 @@ After(async function () { const dbClient = getMasterDbClient() await dbClient('events') - .where({ - event_pubkey: Object + .whereIn('event_pubkey', Object .values(this.parameters.identities as Record) .map(({ pubkey }) => Buffer.from(pubkey, 'hex')), - }).del() + ).delete() this.parameters.identities = {} }) @@ -94,14 +95,14 @@ Given(/someone called (\w+)/, async function(name: string) { this.parameters.clients[name] = connection this.parameters.subscriptions[name] = [] this.parameters.events[name] = [] - const subject = new Subject() - connection.once('close', subject.next.bind(subject)) + const close = new Subject() + connection.once('close', close.next.bind(close)) - const project = (raw: MessageEvent) => JSON.parse(raw.data.toString('utf8')) + const projection = (raw: MessageEvent) => JSON.parse(raw.data.toString('utf8')) const replaySubject = new ReplaySubject(2, 1000) - fromEvent(connection, 'message').pipe(map(project) as any,takeUntil(subject)).subscribe(replaySubject) + fromEvent(connection, 'message').pipe(map(projection) as any,takeUntil(close)).subscribe(replaySubject) streams.set( connection, diff --git a/test/unit/handlers/delegated-event-message-handler.spec.ts b/test/unit/handlers/delegated-event-message-handler.spec.ts index d32e9ac6..a965bbe8 100644 --- a/test/unit/handlers/delegated-event-message-handler.spec.ts +++ b/test/unit/handlers/delegated-event-message-handler.spec.ts @@ -11,6 +11,7 @@ import { IncomingEventMessage, MessageType } from '../../../src/@types/messages' import { DelegatedEventMessageHandler } from '../../../src/handlers/delegated-event-message-handler' import { Event } from '../../../src/@types/event' import { EventMessageHandler } from '../../../src/handlers/event-message-handler' +import { EventTags } from '../../../src/constants/base' import { IUserRepository } from '../../../src/@types/repositories' import { WebSocketAdapterEvent } from '../../../src/constants/adapter' @@ -38,7 +39,7 @@ describe('DelegatedEventMessageHandler', () => { pubkey: 'f'.repeat(64), sig: 'f'.repeat(128), tags: [ - ['delegation', 'delegator', 'rune', 'signature'], + [EventTags.Delegation, 'delegator', 'rune', 'signature'], ], } }) @@ -192,7 +193,7 @@ describe('DelegatedEventMessageHandler', () => { 'kind': 1, 'tags': [ [ - 'delegation', + EventTags.Delegation, '86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e', 'kind=1&created_at>1640995200', 'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1', diff --git a/test/unit/handlers/event-message-handler.spec.ts b/test/unit/handlers/event-message-handler.spec.ts index a1e9004e..9cd73788 100644 --- a/test/unit/handlers/event-message-handler.spec.ts +++ b/test/unit/handlers/event-message-handler.spec.ts @@ -26,11 +26,17 @@ describe('EventMessageHandler', () => { let event: Event let message: IncomingEventMessage let sandbox: Sinon.SinonSandbox + let origEnv: NodeJS.ProcessEnv let originalConsoleWarn: (message?: any, ...optionalParams: any[]) => void | undefined = undefined beforeEach(() => { sandbox = Sinon.createSandbox() + origEnv = { ...process.env } + process.env = { + // deepcode ignore HardcodedNonCryptoSecret/test: + SECRET: 'changeme', + } originalConsoleWarn = console.warn console.warn = () => undefined event = { @@ -45,6 +51,7 @@ describe('EventMessageHandler', () => { }) afterEach(() => { + process.env = origEnv console.warn = originalConsoleWarn sandbox.restore() }) @@ -75,7 +82,9 @@ describe('EventMessageHandler', () => { webSocket as any, strategyFactoryStub, userRepository, - () => ({}) as any, + () => ({ + info: { relay_url: 'relay_url' }, + }) as any, () => ({ hit: async () => false }) ) }) @@ -128,7 +137,7 @@ describe('EventMessageHandler', () => { expect(isUserAdmitted).to.have.been.calledWithExactly(event) expect(strategyFactoryStub).not.to.have.been.called }) - + it('rejects event if it is expired', async () => { isEventValidStub.resolves(undefined) @@ -223,6 +232,9 @@ describe('EventMessageHandler', () => { }, } settings = { + info: { + relay_url: 'relay_url', + }, limits: { event: eventLimits, }, @@ -690,6 +702,9 @@ describe('EventMessageHandler', () => { rateLimits: [], } settings = { + info: { + relay_url: 'relay_url', + }, limits: { event: eventLimits, }, diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index d61006de..ea925066 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -11,7 +11,7 @@ chai.use(sinonChai) const { expect } = chai -import { ContextMetadataKey, EventDeduplicationMetadataKey } from '../../../src/constants/base' +import { ContextMetadataKey, EventDeduplicationMetadataKey, EventTags } from '../../../src/constants/base' import { DatabaseClient } from '../../../src/@types/base' import { EventRepository } from '../../../src/repositories/event-repository' @@ -383,12 +383,12 @@ describe('EventRepository', () => { kind: 1, tags: [ [ - 'p', + EventTags.Pubkey, '8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9', 'wss://nostr-pub.wellorder.net', ], [ - 'e', + EventTags.Event, '7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96', 'wss://nostr-relay.untethr.me', ], @@ -417,12 +417,12 @@ describe('EventRepository', () => { kind: 1, tags: [ [ - 'p', + EventTags.Pubkey, '8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9', 'wss://nostr-pub.wellorder.net', ], [ - 'e', + EventTags.Event, '7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96', 'wss://nostr-relay.untethr.me', ], diff --git a/test/unit/schemas/event-schema.spec.ts b/test/unit/schemas/event-schema.spec.ts index 19426e9b..deae9629 100644 --- a/test/unit/schemas/event-schema.spec.ts +++ b/test/unit/schemas/event-schema.spec.ts @@ -1,8 +1,9 @@ -import { assocPath, omit, range } from 'ramda' +import { assocPath, omit } from 'ramda' import { expect } from 'chai' import { Event } from '../../../src/@types/event' import { eventSchema } from '../../../src/schemas/event-schema' +import { EventTags } from '../../../src/constants/base' import { validateSchema } from '../../../src/utils/validation' describe('NIP-01', () => { @@ -16,31 +17,31 @@ describe('NIP-01', () => { 'kind': 7, 'tags': [ [ - 'e', + EventTags.Event, 'c58e83bb744e4c29642db7a5c3bd1519516ad5c51f6ba5f90c451d03c1961210', '', 'root', ], [ - 'e', + EventTags.Event, 'd0d78967b734628cec7bdfa2321c71c1f1c48e211b4b54333c3b0e94e7e99166', '', 'reply', ], [ - 'p', + EventTags.Pubkey, 'edfa27d49d2af37ee331e1225bb6ed1912c6d999281b36d8018ad99bc3573c29', ], [ - 'p', + EventTags.Pubkey, '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', ], [ - 'e', + EventTags.Event, '6fed2aae1e4f7d8b535774e4f7061c10e2ff20df1ef047da09462c7937925cd5', ], [ - 'p', + EventTags.Pubkey, '2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5', ], ], @@ -115,7 +116,6 @@ describe('NIP-01', () => { ], tag: [ { message: 'must be an array', transform: assocPath(['tags', 0], null) }, - { message: 'must contain less than or equal to 10 items', transform: assocPath(['tags', 0], range(0, 11).map(() => 'x')) }, ], identifier: [ { message: 'must be a string', transform: assocPath(['tags', 0, 0], null) }, diff --git a/test/unit/schemas/filter-schema.spec.ts b/test/unit/schemas/filter-schema.spec.ts index 09f10812..fab8498f 100644 --- a/test/unit/schemas/filter-schema.spec.ts +++ b/test/unit/schemas/filter-schema.spec.ts @@ -1,4 +1,4 @@ -import { assocPath, range } from 'ramda' +import { assocPath } from 'ramda' import { expect } from 'chai' import { filterSchema } from '../../../src/schemas/filter-schema' @@ -32,7 +32,6 @@ describe('NIP-01', () => { const cases = { ids: [ { message: 'must be an array', transform: assocPath(['ids'], null) }, - { message: 'must contain less than or equal to 1000 items', transform: assocPath(['ids'], range(0, 1001).map(() => 'ffff')) }, ], prefixOrId: [ { message: 'length must be less than or equal to 64 characters long', transform: assocPath(['ids', 0], 'f'.repeat(65)) }, @@ -41,7 +40,6 @@ describe('NIP-01', () => { ], authors: [ { message: 'must be an array', transform: assocPath(['authors'], null) }, - { message: 'must contain less than or equal to 1000 items', transform: assocPath(['authors'], range(0, 1001).map(() => 'ffff')) }, ], prefixOrAuthor: [ { message: 'length must be less than or equal to 64 characters long', transform: assocPath(['authors', 0], 'f'.repeat(65)) }, @@ -50,7 +48,6 @@ describe('NIP-01', () => { ], kinds: [ { message: 'must be an array', transform: assocPath(['kinds'], null) }, - { message: 'must contain less than or equal to 20 items', transform: assocPath(['kinds'], range(0, 21).map(() => 1)) }, ], kind: [ { message: 'must be greater than or equal to 0', transform: assocPath(['kinds', 0], -1) }, @@ -73,11 +70,9 @@ describe('NIP-01', () => { { message: 'must be a number', transform: assocPath(['limit'], null) }, { message: 'must be greater than or equal to 0', transform: assocPath(['limit'], -1) }, { message: 'must be a multiple of 1', transform: assocPath(['limit'], Math.PI) }, - { message: 'must be less than or equal to 5000', transform: assocPath(['limit'], 5001) }, ], '#e': [ { message: 'must be an array', transform: assocPath(['#e'], null) }, - { message: 'must contain less than or equal to 256 items', transform: assocPath(['#e'], range(0, 1024 + 1).map(() => 'f')) }, ], '#e[0]': [ { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#e', 0], 'f'.repeat(1024 + 1)) }, @@ -85,7 +80,6 @@ describe('NIP-01', () => { ], '#p': [ { message: 'must be an array', transform: assocPath(['#p'], null) }, - { message: 'must contain less than or equal to 256 items', transform: assocPath(['#p'], range(0, 1024 + 1).map(() => 'f')) }, ], '#p[0]': [ { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#p', 0], 'f'.repeat(1024 + 1)) }, @@ -93,7 +87,6 @@ describe('NIP-01', () => { ], '#r': [ { message: 'must be an array', transform: assocPath(['#r'], null) }, - { message: 'must contain less than or equal to 256 items', transform: assocPath(['#r'], range(0, 1024 + 1).map(() => 'f')) }, ], '#r[0]': [ { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#r', 0], 'f'.repeat(1024 + 1)) },