Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/@types/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface IEventRepository {
upsert(event: Event): Promise<number>
findByFilters(filters: SubscriptionFilter[]): IQueryResult<DBEvent[]>
deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise<number>
deleteByPubkeyExceptKinds(pubkey: Pubkey, excludedKinds: number[]): Promise<number>
hasActiveRequestToVanish(pubkey: Pubkey): Promise<boolean>
}

export interface IInvoiceRepository {
Expand Down
4 changes: 4 additions & 0 deletions src/constants/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum EventKinds {
DELETE = 5,
REPOST = 6,
REACTION = 7,
REQUEST_TO_VANISH = 62,
// Channels
CHANNEL_CREATION = 40,
CHANNEL_METADATA = 41,
Expand Down Expand Up @@ -36,12 +37,15 @@ export enum EventKinds {
export enum EventTags {
Event = 'e',
Pubkey = 'p',
Relay = 'r',
// Multicast = 'm',
Deduplication = 'd',
Expiration = 'expiration',
Invoice = 'bolt11',
}

export const ALL_RELAYS = 'ALL_RELAYS'

export enum PaymentsProcessors {
LNURL = 'lnurl',
ZEBEDEE = 'zebedee',
Expand Down
7 changes: 5 additions & 2 deletions src/factories/event-strategy-factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent } from '../utils/event'
import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent, isRequestToVanishEvent } from '../utils/event'
import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy'
import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy'
import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy'
Expand All @@ -9,12 +9,15 @@ import { IEventStrategy } from '../@types/message-handlers'
import { IWebSocketAdapter } from '../@types/adapters'
import { ParameterizedReplaceableEventStrategy } from '../handlers/event-strategies/parameterized-replaceable-event-strategy'
import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy'
import { VanishEventStrategy } from '../handlers/event-strategies/vanish-event-strategy'

export const eventStrategyFactory = (
eventRepository: IEventRepository,
): Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]> =>
([event, adapter]: [Event, IWebSocketAdapter]) => {
if (isReplaceableEvent(event)) {
if (isRequestToVanishEvent(event)) {
return new VanishEventStrategy(adapter, eventRepository)
} else if (isReplaceableEvent(event)) {
return new ReplaceableEventStrategy(adapter, eventRepository)
} else if (isEphemeralEvent(event)) {
return new EphemeralEventStrategy(adapter)
Expand Down
1 change: 1 addition & 0 deletions src/factories/message-handler-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const messageHandlerFactory = (
return new EventMessageHandler(
adapter,
eventStrategyFactory(eventRepository),
eventRepository,
userRepository,
createSettings,
slidingWindowRateLimiterFactory,
Expand Down
48 changes: 43 additions & 5 deletions src/handlers/event-message-handler.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { Event, ExpiringEvent } from '../@types/event'
import { ContextMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base'
import { Event, ExpiringEvent } from '../@types/event'
import { EventRateLimit, FeeSchedule, Settings } from '../@types/settings'
import { getEventExpiration, getEventProofOfWork, getPubkeyProofOfWork, getPublicKey, getRelayPrivateKey, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid, isExpiredEvent } from '../utils/event'
import {
getEventExpiration,
getEventProofOfWork,
getPubkeyProofOfWork,
getPublicKey,
getRelayPrivateKey,
isEventIdValid,
isEventKindOrRangeMatch,
isEventSignatureValid,
isExpiredEvent,
isRequestToVanishEvent,
} from '../utils/event'
import { IEventRepository, IUserRepository } from '../@types/repositories'
import { IEventStrategy, IMessageHandler } from '../@types/message-handlers'
import { ContextMetadataKey } from '../constants/base'
import { createCommandResult } from '../utils/messages'
import { createLogger } from '../factories/logger-factory'
import { EventExpirationTimeMetadataKey } from '../constants/base'
import { Factory } from '../@types/base'
import { IncomingEventMessage } from '../@types/messages'
import { IRateLimiter } from '../@types/utils'
import { IUserRepository } from '../@types/repositories'
import { IWebSocketAdapter } from '../@types/adapters'
import { WebSocketAdapterEvent } from '../constants/adapter'

Expand All @@ -19,6 +29,7 @@ export class EventMessageHandler implements IMessageHandler {
public constructor(
protected readonly webSocket: IWebSocketAdapter,
protected readonly strategyFactory: Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]>,
protected readonly eventRepository: IEventRepository,
protected readonly userRepository: IUserRepository,
private readonly settings: () => Settings,
private readonly slidingWindowRateLimiter: Factory<IRateLimiter>,
Expand Down Expand Up @@ -57,6 +68,13 @@ export class EventMessageHandler implements IMessageHandler {
return
}

reason = await this.isBlockedByRequestToVanish(event)
if (reason) {
debug('event %s rejected: %s', event.id, reason)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
return
}

reason = await this.isUserAdmitted(event)
if (reason) {
debug('event %s rejected: %s', event.id, reason)
Expand Down Expand Up @@ -190,6 +208,26 @@ export class EventMessageHandler implements IMessageHandler {
if (!await isEventSignatureValid(event)) {
return 'invalid: event signature verification failed'
}

if (event.kind === EventKinds.REQUEST_TO_VANISH && !isRequestToVanishEvent(event, this.settings().info.relay_url)) {
return 'invalid: request to vanish relay tag invalid'
}
}

protected async isBlockedByRequestToVanish(event: Event): Promise<string | undefined> {
if (isRequestToVanishEvent(event)) {
return
}

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

const existingVanishRequest = await this.eventRepository.hasActiveRequestToVanish(event.pubkey)
if (existingVanishRequest) {
return 'blocked: request to vanish active for pubkey'
}
}

protected async isRateLimited(event: Event): Promise<boolean> {
Expand Down
33 changes: 33 additions & 0 deletions src/handlers/event-strategies/vanish-event-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createCommandResult } from '../../utils/messages'
import { createLogger } from '../../factories/logger-factory'
import { Event } from '../../@types/event'
import { EventKinds } from '../../constants/base'
import { IEventRepository } from '../../@types/repositories'
import { IEventStrategy } from '../../@types/message-handlers'
import { IWebSocketAdapter } from '../../@types/adapters'
import { WebSocketAdapterEvent } from '../../constants/adapter'

const debug = createLogger('vanish-event-strategy')

export class VanishEventStrategy implements IEventStrategy<Event, Promise<void>> {
public constructor(
private readonly webSocket: IWebSocketAdapter,
private readonly eventRepository: IEventRepository,
) {}

public async execute(event: Event): Promise<void> {
debug('received request to vanish event: %o', event)

await this.eventRepository.deleteByPubkeyExceptKinds(
event.pubkey,
[EventKinds.REQUEST_TO_VANISH],
)

const count = await this.eventRepository.create(event)

this.webSocket.emit(
WebSocketAdapterEvent.Message,
createCommandResult(event.id, true, count ? '' : 'duplicate:')
)
}
}
26 changes: 25 additions & 1 deletion src/repositories/event-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
toPairs,
} from 'ramda'

import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base'
import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base'
import { DatabaseClient, EventId } from '../@types/base'
import { DBEvent, Event } from '../@types/event'
import { IEventRepository, IQueryResult } from '../@types/repositories'
Expand Down Expand Up @@ -246,9 +246,33 @@ export class EventRepository implements IEventRepository {
return this.masterDbClient('events')
.where('event_pubkey', toBuffer(pubkey))
.whereIn('event_id', map(toBuffer)(eventIdsToDelete))
.whereNot('event_kind', EventKinds.REQUEST_TO_VANISH)
.whereNull('deleted_at')
.update({
deleted_at: this.masterDbClient.raw('now()'),
})
}

public deleteByPubkeyExceptKinds(pubkey: string, excludedKinds: number[]): Promise<number> {
debug('deleting events from %s except kinds %o', pubkey, excludedKinds)

return this.masterDbClient('events')
.where('event_pubkey', toBuffer(pubkey))
.whereNotIn('event_kind', excludedKinds)
.whereNull('deleted_at')
.update({
deleted_at: this.masterDbClient.raw('now()'),
})
}

public async hasActiveRequestToVanish(pubkey: string): Promise<boolean> {
const result = await this.readReplicaDbClient('events')
.select('event_id')
.where('event_pubkey', toBuffer(pubkey))
.where('event_kind', EventKinds.REQUEST_TO_VANISH)
.whereNull('deleted_at')
.first()

return Boolean(result)
Comment on lines +268 to +276
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are likely better off adding a column to the users table. However, I think I am okay with this approach for now. Optimizing this to use the users table can be a separate issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right , a table flag will would be better for long term optimization. I used this event based check to stay scoped to NIP-62 behavior. We can create a follow up issue for this if you want.

}
}
20 changes: 17 additions & 3 deletions src/utils/event.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import * as secp256k1 from '@noble/secp256k1'

import { ALL_RELAYS, EventKinds, EventTags } from '../constants/base'
import { applySpec, pipe, prop } from 'ramda'
import { CanonicalEvent, DBEvent, Event, UnidentifiedEvent, UnsignedEvent } from '../@types/event'
import { createCipheriv, getRandomValues } from 'crypto'
import { EventId, Pubkey, Tag } from '../@types/base'
import { EventKinds, EventTags } from '../constants/base'

import cluster from 'cluster'
import { deriveFromSecret } from './secret'
import { EventKindsRange } from '../@types/settings'
Expand Down Expand Up @@ -227,6 +225,22 @@ export const isDeleteEvent = (event: Event): boolean => {
return event.kind === EventKinds.DELETE
}

export const isRequestToVanishEvent = (event: Event, relayUrl?: string): boolean => {
if (event.kind !== EventKinds.REQUEST_TO_VANISH) {
return false
}

if (typeof relayUrl === 'undefined') {
return true
}

const relayTags = event.tags
.filter((tag) => tag.length >= 2 && tag[0] === EventTags.Relay)
.map((tag) => tag[1])

return relayTags.length > 0 && relayTags.every((relay) => relay === relayUrl || relay === ALL_RELAYS)
}

export const isExpiredEvent = (event: Event): boolean => {
if (!event.tags.length) {
return false
Expand Down
6 changes: 6 additions & 0 deletions test/unit/factories/event-strategy-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { IEventStrategy } from '../../../src/@types/message-handlers'
import { IWebSocketAdapter } from '../../../src/@types/adapters'
import { ParameterizedReplaceableEventStrategy } from '../../../src/handlers/event-strategies/parameterized-replaceable-event-strategy'
import { ReplaceableEventStrategy } from '../../../src/handlers/event-strategies/replaceable-event-strategy'
import { VanishEventStrategy } from '../../../src/handlers/event-strategies/vanish-event-strategy'

describe('eventStrategyFactory', () => {
let eventRepository: IEventRepository
Expand Down Expand Up @@ -52,6 +53,11 @@ describe('eventStrategyFactory', () => {
expect(factory([event, adapter])).to.be.an.instanceOf(DeleteEventStrategy)
})

it('returns VanishEventStrategy given a request to vanish event', () => {
event.kind = EventKinds.REQUEST_TO_VANISH
expect(factory([event, adapter])).to.be.an.instanceOf(VanishEventStrategy)
})

it('returns ParameterizedReplaceableEventStrategy given a delete event', () => {
event.kind = EventKinds.PARAMETERIZED_REPLACEABLE_FIRST
expect(factory([event, adapter])).to.be.an.instanceOf(ParameterizedReplaceableEventStrategy)
Expand Down
20 changes: 20 additions & 0 deletions test/unit/handlers/event-message-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('EventMessageHandler', () => {
let webSocket: IWebSocketAdapter
let handler: EventMessageHandler
let userRepository: IUserRepository
let eventRepository: any
let event: Event
let message: IncomingEventMessage
let sandbox: Sinon.SinonSandbox
Expand Down Expand Up @@ -69,6 +70,7 @@ describe('EventMessageHandler', () => {
canAcceptEventStub = sandbox.stub(EventMessageHandler.prototype, 'canAcceptEvent' as any)
isEventValidStub = sandbox.stub(EventMessageHandler.prototype, 'isEventValid' as any)
isUserAdmitted = sandbox.stub(EventMessageHandler.prototype, 'isUserAdmitted' as any)
eventRepository = { hasActiveRequestToVanish: sandbox.stub().resolves(false) }
strategyExecuteStub = sandbox.stub()
strategyFactoryStub = sandbox.stub().returns({
execute: strategyExecuteStub,
Expand All @@ -81,6 +83,7 @@ describe('EventMessageHandler', () => {
handler = new EventMessageHandler(
webSocket as any,
strategyFactoryStub,
eventRepository,
userRepository,
() => ({
info: { relay_url: 'relay_url' },
Expand Down Expand Up @@ -119,6 +122,20 @@ describe('EventMessageHandler', () => {
expect(strategyFactoryStub).not.to.have.been.called
})

it('rejects event if request to vanish is active for pubkey', async () => {
canAcceptEventStub.returns(undefined)
isEventValidStub.resolves(undefined)
eventRepository.hasActiveRequestToVanish.resolves(true)

await handler.handleMessage(message)

expect(eventRepository.hasActiveRequestToVanish).to.have.been.calledOnceWithExactly(event.pubkey)
expect(onMessageSpy).to.have.been.calledOnceWithExactly(
[MessageType.OK, event.id, false, 'blocked: request to vanish active for pubkey'],
)
expect(strategyFactoryStub).not.to.have.been.called
})

it('rejects event if invalid', async () => {
isEventValidStub.resolves('reason')

Expand Down Expand Up @@ -242,6 +259,7 @@ describe('EventMessageHandler', () => {
handler = new EventMessageHandler(
{} as any,
() => null,
{ hasActiveRequestToVanish: async () => false } as any,
userRepository,
() => settings,
() => ({ hit: async () => false })
Expand Down Expand Up @@ -717,6 +735,7 @@ describe('EventMessageHandler', () => {
handler = new EventMessageHandler(
webSocket,
() => null,
{ hasActiveRequestToVanish: async () => false } as any,
userRepository,
() => settings,
() => ({ hit: rateLimiterHitStub })
Expand Down Expand Up @@ -984,6 +1003,7 @@ describe('EventMessageHandler', () => {
handler = new EventMessageHandler(
webSocket,
() => null,
{ hasActiveRequestToVanish: async () => false } as any,
userRepository,
() => settings,
() => ({ hit: async () => false })
Expand Down
Loading
Loading