From 6760ab0e10608c86766e1ec3ed87a3cccc842c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Arturo=20Cabral=20Mej=C3=ADa?= Date: Fri, 12 Jan 2024 16:12:04 -0500 Subject: [PATCH] feat!: remove nip-26 delegation support (#350) BREAKING CHANGE: NIP-26 support has been removed. Delegated events will not be handled differently. --- README.md | 2 +- ...4900_remove_delegator_from_events_table.js | 18 ++ package.json | 1 - src/@types/event.ts | 7 +- src/@types/subscription.ts | 2 +- src/constants/base.ts | 2 - .../delegated-event-strategy-factory.ts | 21 -- src/factories/message-handler-factory.ts | 15 +- .../delegated-event-message-handler.ts | 75 ------ src/repositories/event-repository.ts | 14 +- src/utils/event.ts | 61 +---- .../delegated-event-strategy-factory.spec.ts | 46 ---- .../factories/message-handler-factory.spec.ts | 14 -- .../delegated-event-message-handler.spec.ts | 229 ------------------ .../repositories/event-repository.spec.ts | 18 +- test/unit/utils/event.spec.ts | 71 ------ 16 files changed, 35 insertions(+), 561 deletions(-) create mode 100644 migrations/20240111204900_remove_delegator_from_events_table.js delete mode 100644 src/factories/delegated-event-strategy-factory.ts delete mode 100644 src/handlers/delegated-event-message-handler.ts delete mode 100644 test/unit/factories/delegated-event-strategy-factory.spec.ts delete mode 100644 test/unit/handlers/delegated-event-message-handler.spec.ts diff --git a/README.md b/README.md index 3ad1a95d..a6545392 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ NIPs with a relay-specific implementation are listed here. - [x] NIP-16: Event Treatment - [x] NIP-20: Command Results - [x] NIP-22: Event `created_at` Limits -- [x] NIP-26: Delegated Event Signing +- [ ] NIP-26: Delegated Event Signing (REMOVED) - [x] NIP-28: Public Chat - [x] NIP-33: Parameterized Replaceable Events - [x] NIP-40: Expiration Timestamp diff --git a/migrations/20240111204900_remove_delegator_from_events_table.js b/migrations/20240111204900_remove_delegator_from_events_table.js new file mode 100644 index 00000000..b5a7310c --- /dev/null +++ b/migrations/20240111204900_remove_delegator_from_events_table.js @@ -0,0 +1,18 @@ +exports.up = async function (knex) { + await knex.schema + .raw('DROP INDEX IF EXISTS pubkey_delegator_kind_idx;') + await knex.schema.alterTable('events', function (table) { + table.dropColumn('event_delegator') + }) +} + +exports.down = async function (knex) { + await knex.schema.alterTable('events', function (table) { + table.binary('event_delegator').nullable().index() + }) + await knex.schema + .raw( + `CREATE UNIQUE INDEX pubkey_delegator_kind_idx + ON events ( event_pubkey, event_delegator, event_kind );`, + ) +} diff --git a/package.json b/package.json index 665a095d..f1100ad7 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ 16, 20, 22, - 26, 28, 33, 40 diff --git a/src/@types/event.ts b/src/@types/event.ts index 52194560..cc9241d4 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -1,5 +1,5 @@ import { ContextMetadata, EventId, Pubkey, Tag } from './base' -import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base' +import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base' export interface BaseEvent { id: EventId @@ -21,10 +21,6 @@ export type UnsignedEvent = Omit export type UnidentifiedEvent = Omit -export interface DelegatedEvent extends Event { - [EventDelegatorMetadataKey]?: Pubkey -} - export interface ExpiringEvent extends Event { [EventExpirationTimeMetadataKey]?: number } @@ -42,7 +38,6 @@ export interface DBEvent { event_content: string event_tags: Tag[] event_signature: Buffer - event_delegator?: Buffer | null event_deduplication?: string | null first_seen: Date deleted_at?: Date diff --git a/src/@types/subscription.ts b/src/@types/subscription.ts index e78efb2b..265506d5 100644 --- a/src/@types/subscription.ts +++ b/src/@types/subscription.ts @@ -5,7 +5,7 @@ export type SubscriptionId = string export interface SubscriptionFilter { ids?: EventId[] - kinds?: EventKinds[] + kinds?: (EventKinds | number)[] since?: number until?: number authors?: Pubkey[] diff --git a/src/constants/base.ts b/src/constants/base.ts index fd6b2a9a..8b9adca8 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -37,7 +37,6 @@ export enum EventTags { Event = 'e', Pubkey = 'p', // Multicast = 'm', - Delegation = 'delegation', Deduplication = 'd', Expiration = 'expiration', Invoice = 'bolt11', @@ -49,7 +48,6 @@ export enum PaymentsProcessors { LNBITS = 'lnbits', } -export const EventDelegatorMetadataKey = Symbol('Delegator') export const EventDeduplicationMetadataKey = Symbol('Deduplication') export const ContextMetadataKey = Symbol('Context') export const EventExpirationTimeMetadataKey = Symbol('Expiration') diff --git a/src/factories/delegated-event-strategy-factory.ts b/src/factories/delegated-event-strategy-factory.ts deleted file mode 100644 index 1f828a9c..00000000 --- a/src/factories/delegated-event-strategy-factory.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { isDeleteEvent, isEphemeralEvent, isReplaceableEvent } from '../utils/event' -import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy' -import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy' -import { Event } from '../@types/event' -import { Factory } from '../@types/base' -import { IEventRepository } from '../@types/repositories' -import { IEventStrategy } from '../@types/message-handlers' -import { IWebSocketAdapter } from '../@types/adapters' - -export const delegatedEventStrategyFactory = ( - eventRepository: IEventRepository, -): Factory>, [Event, IWebSocketAdapter]> => - ([event, adapter]: [Event, IWebSocketAdapter]) => { - if (isEphemeralEvent(event)) { - return new EphemeralEventStrategy(adapter) - } else if (isReplaceableEvent(event) || isDeleteEvent(event)) { - return - } - - return new DefaultEventStrategy(adapter, eventRepository) - } diff --git a/src/factories/message-handler-factory.ts b/src/factories/message-handler-factory.ts index a3123ce2..b1c11e9c 100644 --- a/src/factories/message-handler-factory.ts +++ b/src/factories/message-handler-factory.ts @@ -1,11 +1,8 @@ import { IEventRepository, IUserRepository } from '../@types/repositories' import { IncomingMessage, MessageType } from '../@types/messages' import { createSettings } from './settings-factory' -import { DelegatedEventMessageHandler } from '../handlers/delegated-event-message-handler' -import { delegatedEventStrategyFactory } from './delegated-event-strategy-factory' import { EventMessageHandler } from '../handlers/event-message-handler' import { eventStrategyFactory } from './event-strategy-factory' -import { isDelegatedEvent } from '../utils/event' import { IWebSocketAdapter } from '../@types/adapters' import { slidingWindowRateLimiterFactory } from './rate-limiter-factory' import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler' @@ -18,16 +15,6 @@ export const messageHandlerFactory = ( switch (message[0]) { case MessageType.EVENT: { - if (isDelegatedEvent(message[1])) { - return new DelegatedEventMessageHandler( - adapter, - delegatedEventStrategyFactory(eventRepository), - userRepository, - createSettings, - slidingWindowRateLimiterFactory, - ) - } - return new EventMessageHandler( adapter, eventStrategyFactory(eventRepository), @@ -39,7 +26,7 @@ export const messageHandlerFactory = ( case MessageType.REQ: return new SubscribeMessageHandler(adapter, eventRepository, createSettings) case MessageType.CLOSE: - return new UnsubscribeMessageHandler(adapter,) + return new UnsubscribeMessageHandler(adapter) default: throw new Error(`Unknown message type: ${String(message[0]).substring(0, 64)}`) } diff --git a/src/handlers/delegated-event-message-handler.ts b/src/handlers/delegated-event-message-handler.ts deleted file mode 100644 index 13ede1f2..00000000 --- a/src/handlers/delegated-event-message-handler.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { EventDelegatorMetadataKey, EventTags } from '../constants/base' -import { createCommandResult } from '../utils/messages' -import { createLogger } from '../factories/logger-factory' -import { DelegatedEvent } from '../@types/event' -import { EventMessageHandler } from './event-message-handler' -import { IMessageHandler } from '../@types/message-handlers' -import { IncomingEventMessage } from '../@types/messages' -import { isDelegatedEventValid } from '../utils/event' -import { WebSocketAdapterEvent } from '../constants/adapter' - -const debug = createLogger('delegated-event-message-handler') - -export class DelegatedEventMessageHandler extends EventMessageHandler implements IMessageHandler { - public async handleMessage(message: IncomingEventMessage): Promise { - const [, event] = message - - let reason = await this.isEventValid(event) - if (reason) { - debug('event %s rejected: %s', event.id, reason) - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason)) - return - } - - if (await this.isRateLimited(event)) { - debug('event %s rejected: rate-limited') - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'rate-limited: slow down')) - return - } - - reason = this.canAcceptEvent(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) - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason)) - return - } - - const [, delegator] = event.tags.find((tag) => tag.length === 4 && tag[0] === EventTags.Delegation) - const delegatedEvent: DelegatedEvent = { - ...event, - [EventDelegatorMetadataKey]: delegator, - } - - const strategy = this.strategyFactory([delegatedEvent, this.webSocket]) - - if (typeof strategy?.execute !== 'function') { - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: event not supported')) - return - } - - try { - await strategy.execute(delegatedEvent) - } catch (error) { - console.error('error handling message', message, error) - this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: unable to process event')) - } - } - - protected async isEventValid(event: DelegatedEvent): Promise { - const reason = await super.isEventValid(event) - if (reason) { - return reason - } - - if (!await isDelegatedEventValid(event)) { - return 'invalid: delegation verification failed' - } - } -} diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index fa4f7fa3..9c645b8a 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -28,7 +28,7 @@ import { toPairs, } from 'ramda' -import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base' +import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base' import { DatabaseClient, EventId } from '../@types/base' import { DBEvent, Event } from '../@types/event' import { IEventRepository, IQueryResult } from '../@types/repositories' @@ -105,7 +105,7 @@ export class EventRepository implements IEventRepository { ])(currentFilter[filterName] as string[]) }) })({ - authors: ['event_pubkey', 'event_delegator'], + authors: ['event_pubkey'], ids: ['event_id'], }) @@ -180,11 +180,6 @@ export class EventRepository implements IEventRepository { event_tags: pipe(prop('tags'), toJSON), event_content: prop('content'), event_signature: pipe(prop('sig'), toBuffer), - event_delegator: ifElse( - propSatisfies(is(String), EventDelegatorMetadataKey), - pipe(prop(EventDelegatorMetadataKey as any), toBuffer), - always(null), - ), remote_address: path([ContextMetadataKey as any, 'remoteAddress', 'address']), expires_at: ifElse( propSatisfies(is(Number), EventExpirationTimeMetadataKey), @@ -212,11 +207,6 @@ export class EventRepository implements IEventRepository { event_tags: pipe(prop('tags'), toJSON), event_content: prop('content'), event_signature: pipe(prop('sig'), toBuffer), - event_delegator: ifElse( - propSatisfies(is(String), EventDelegatorMetadataKey), - pipe(prop(EventDelegatorMetadataKey as any), toBuffer), - always(null), - ), event_deduplication: ifElse( propSatisfies(isNil, EventDeduplicationMetadataKey), pipe(paths([['pubkey'], ['kind']]), toJSON), diff --git a/src/utils/event.ts b/src/utils/event.ts index 53ccedc0..9c4bafd5 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -1,6 +1,6 @@ import * as secp256k1 from '@noble/secp256k1' -import { applySpec, converge, curry, mergeLeft, nth, omit, pipe, prop, reduceBy } from 'ramda' +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' @@ -12,7 +12,6 @@ import { EventKindsRange } from '../@types/settings' import { fromBuffer } from './transform' import { getLeadingZeroBits } from './proof-of-work' import { isGenericTagQuery } from './filter' -import { RuneLike } from './runes/rune-like' import { SubscriptionFilter } from '../@types/subscription' import { WebSocketServerAdapterEvent } from '../constants/adapter' @@ -68,18 +67,7 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve if ( !filter.authors.some(startsWith(event.pubkey)) ) { - if (isDelegatedEvent(event)) { - const delegation = event.tags.find((tag) => tag[0] === EventTags.Delegation) - if (typeof delegation === 'undefined') { - return false - } - - if (!filter.authors.some(startsWith(delegation[1]))) { - return false - } - } else { - return false - } + return false } } @@ -116,51 +104,6 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve return true } -export const isDelegatedEvent = (event: Event): boolean => { - return event.tags.some((tag) => tag.length === 4 && tag[0] === EventTags.Delegation) -} - -export const isDelegatedEventValid = async (event: Event): Promise => { - const delegation = event.tags.find((tag) => tag.length === 4 && tag[0] === EventTags.Delegation) - if (!delegation) { - return false - } - - - // Validate rune - const runifiedEvent = (converge( - curry(mergeLeft), - [ - omit(['tags']), - pipe( - prop('tags') as any, - reduceBy( - (acc, tag) => ([...acc, tag[1]]), - [], - nth(0) as any, - ), - ), - ], - ) as any)(event) - - let result: boolean - try { - [result] = RuneLike.from(delegation[2]).test(runifiedEvent) - } catch (error) { - result = false - } - - if (!result) { - return false - } - - const serializedDelegationTag = `nostr:${delegation[0]}:${event.pubkey}:${delegation[2]}` - - const token = await secp256k1.utils.sha256(Buffer.from(serializedDelegationTag)) - - return secp256k1.schnorr.verify(delegation[3], token, delegation[1]) -} - export const getEventHash = async (event: Event | UnidentifiedEvent | UnsignedEvent): Promise => { const id = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializeEvent(event)))) diff --git a/test/unit/factories/delegated-event-strategy-factory.spec.ts b/test/unit/factories/delegated-event-strategy-factory.spec.ts deleted file mode 100644 index 9cc4e0cb..00000000 --- a/test/unit/factories/delegated-event-strategy-factory.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { expect } from 'chai' - -import { DefaultEventStrategy } from '../../../src/handlers/event-strategies/default-event-strategy' -import { delegatedEventStrategyFactory } from '../../../src/factories/delegated-event-strategy-factory' -import { EphemeralEventStrategy } from '../../../src/handlers/event-strategies/ephemeral-event-strategy' -import { Event } from '../../../src/@types/event' -import { EventKinds } from '../../../src/constants/base' -import { Factory } from '../../../src/@types/base' -import { IEventRepository } from '../../../src/@types/repositories' -import { IEventStrategy } from '../../../src/@types/message-handlers' -import { IWebSocketAdapter } from '../../../src/@types/adapters' - -describe('delegatedEventStrategyFactory', () => { - let eventRepository: IEventRepository - let event: Event - let adapter: IWebSocketAdapter - let factory: Factory>, [Event, IWebSocketAdapter]> - - beforeEach(() => { - eventRepository = {} as any - event = {} as any - adapter = {} as any - - factory = delegatedEventStrategyFactory(eventRepository) - }) - - it('returns EphemeralEventStrategy given a set_metadata event', () => { - event.kind = EventKinds.EPHEMERAL_FIRST - expect(factory([event, adapter])).to.be.an.instanceOf(EphemeralEventStrategy) - }) - - it('returns DefaultEventStrategy given a text_note event', () => { - event.kind = EventKinds.TEXT_NOTE - expect(factory([event, adapter])).to.be.an.instanceOf(DefaultEventStrategy) - }) - - it('returns undefined given a replaceable event', () => { - event.kind = EventKinds.REPLACEABLE_FIRST - expect(factory([event, adapter])).to.be.undefined - }) - - it('returns undefined given a delete event', () => { - event.kind = EventKinds.DELETE - expect(factory([event, adapter])).to.be.undefined - }) -}) diff --git a/test/unit/factories/message-handler-factory.spec.ts b/test/unit/factories/message-handler-factory.spec.ts index b44c1ce7..4c5299ff 100644 --- a/test/unit/factories/message-handler-factory.spec.ts +++ b/test/unit/factories/message-handler-factory.spec.ts @@ -2,10 +2,8 @@ import { expect } from 'chai' import { IEventRepository, IUserRepository } from '../../../src/@types/repositories' import { IncomingMessage, 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 { IWebSocketAdapter } from '../../../src/@types/adapters' import { messageHandlerFactory } from '../../../src/factories/message-handler-factory' import { SubscribeMessageHandler } from '../../../src/handlers/subscribe-message-handler' @@ -38,18 +36,6 @@ describe('messageHandlerFactory', () => { expect(factory([message, adapter])).to.be.an.instanceOf(EventMessageHandler) }) - it('returns DelegatedEventMessageHandler when given an EVENT message with delegated event', () => { - event.tags = [ - [EventTags.Delegation, '', '', ''], - ] - message = [ - MessageType.EVENT, - event, - ] - - expect(factory([message, adapter])).to.be.an.instanceOf(DelegatedEventMessageHandler) - }) - it('returns SubscribeMessageHandler when given a REQ message', () => { message = [ MessageType.REQ, diff --git a/test/unit/handlers/delegated-event-message-handler.spec.ts b/test/unit/handlers/delegated-event-message-handler.spec.ts deleted file mode 100644 index a965bbe8..00000000 --- a/test/unit/handlers/delegated-event-message-handler.spec.ts +++ /dev/null @@ -1,229 +0,0 @@ -import chai from 'chai' -import chaiAsPromised from 'chai-as-promised' -import EventEmitter from 'events' -import Sinon from 'sinon' -import sinonChai from 'sinon-chai' - -chai.use(sinonChai) -chai.use(chaiAsPromised) - -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' - -const { expect } = chai - -describe('DelegatedEventMessageHandler', () => { - let webSocket: EventEmitter - let handler: DelegatedEventMessageHandler - let userRepository: IUserRepository - let event: Event - let message: IncomingEventMessage - let sandbox: Sinon.SinonSandbox - - let originalConsoleWarn: any = undefined - - beforeEach(() => { - sandbox = Sinon.createSandbox() - originalConsoleWarn = console.warn - console.warn = () => undefined - event = { - content: 'hello', - created_at: 1665546189, - id: 'f'.repeat(64), - kind: 1, - pubkey: 'f'.repeat(64), - sig: 'f'.repeat(128), - tags: [ - [EventTags.Delegation, 'delegator', 'rune', 'signature'], - ], - } - }) - - afterEach(() => { - console.warn = originalConsoleWarn - sandbox.restore() - }) - - describe('handleMessage', () => { - let canAcceptEventStub: Sinon.SinonStub - let isEventValidStub: Sinon.SinonStub - let strategyFactoryStub: Sinon.SinonStub - let onMessageSpy: Sinon.SinonSpy - let strategyExecuteStub: Sinon.SinonStub - let isRateLimitedStub: Sinon.SinonStub - let isUserAdmitted: Sinon.SinonStub - - beforeEach(() => { - canAcceptEventStub = sandbox.stub(DelegatedEventMessageHandler.prototype, 'canAcceptEvent' as any) - isEventValidStub = sandbox.stub(DelegatedEventMessageHandler.prototype, 'isEventValid' as any) - isUserAdmitted = sandbox.stub(EventMessageHandler.prototype, 'isUserAdmitted' as any) - strategyExecuteStub = sandbox.stub() - strategyFactoryStub = sandbox.stub().returns({ - execute: strategyExecuteStub, - }) - onMessageSpy = sandbox.fake.returns(undefined) - webSocket = new EventEmitter() - webSocket.on(WebSocketAdapterEvent.Message, onMessageSpy) - message = [MessageType.EVENT, event] - isRateLimitedStub = sandbox.stub(EventMessageHandler.prototype, 'isRateLimited' as any) - handler = new DelegatedEventMessageHandler( - webSocket as any, - strategyFactoryStub, - userRepository, - () => ({}) as any, - () => ({ hit: async () => false }), - ) - }) - - afterEach(() => { - isEventValidStub.restore() - canAcceptEventStub.restore() - webSocket.removeAllListeners() - }) - - it('rejects event if it can\'t be accepted', async () => { - canAcceptEventStub.returns('reason') - - await handler.handleMessage(message) - - expect(canAcceptEventStub).to.have.been.calledOnceWithExactly(event) - expect(onMessageSpy).to.have.been.calledOnceWithExactly( - [ - MessageType.OK, - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', - false, - 'reason', - ], - ) - expect(strategyFactoryStub).not.to.have.been.called - }) - - it('rejects event if invalid', async () => { - isEventValidStub.returns('reason') - - await handler.handleMessage(message) - - expect(isEventValidStub).to.have.been.calledOnceWithExactly(event) - expect(onMessageSpy).not.to.have.been.calledOnceWithExactly() - expect(strategyFactoryStub).not.to.have.been.called - }) - - it('rejects event if rate-limited', async () => { - isRateLimitedStub.resolves(true) - - await handler.handleMessage(message) - - expect(isRateLimitedStub).to.have.been.calledOnceWithExactly(event) - expect(onMessageSpy).to.have.been.calledOnceWithExactly( - [MessageType.OK, event.id, false, 'rate-limited: slow down'], - ) - expect(strategyFactoryStub).not.to.have.been.called - }) - - it('rejects event is user is not admitted', async () => { - isUserAdmitted.resolves('not admitted') - - await handler.handleMessage(message) - - expect(isRateLimitedStub).to.have.been.calledOnceWithExactly(event) - expect(isUserAdmitted).to.have.been.calledOnceWithExactly(event) - expect(onMessageSpy).to.have.been.calledOnceWithExactly([ - MessageType.OK, - event.id, - false, - 'not admitted', - ]) - }) - - it('does not call strategy if none given', async () => { - isEventValidStub.returns(undefined) - canAcceptEventStub.returns(undefined) - strategyFactoryStub.returns(undefined) - - await handler.handleMessage(message) - - expect(isEventValidStub).to.have.been.calledOnceWithExactly(event) - expect(onMessageSpy).not.to.have.been.calledOnceWithExactly() - expect(strategyFactoryStub).to.have.been.calledOnceWithExactly([ - event, - webSocket, - ]) - expect(strategyExecuteStub).not.to.have.been.called - }) - - it('calls strategy with event', async () => { - isEventValidStub.returns(undefined) - canAcceptEventStub.returns(undefined) - - await handler.handleMessage(message) - - expect(isEventValidStub).to.have.been.calledOnceWithExactly(event) - expect(onMessageSpy).not.to.have.been.calledOnceWithExactly() - expect(strategyFactoryStub).to.have.been.calledOnceWithExactly([ - event, - webSocket, - ]) - expect(strategyExecuteStub).to.have.been.calledOnceWithExactly(event) - }) - - it('does not reject if strategy rejects', async () => { - const error = new Error('mistakes were made') - isEventValidStub.returns(undefined) - canAcceptEventStub.returns(undefined) - strategyExecuteStub.rejects(error) - - return expect(handler.handleMessage(message)).to.eventually.be.fulfilled - }) - }) - - describe('isEventValid', () => { - let parentIsEventValidStub: Sinon.SinonStub - - beforeEach(() => { - parentIsEventValidStub = Sinon.stub(EventMessageHandler.prototype, 'isEventValid' as any) - event = { - 'id': 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc', - 'pubkey': '62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49', - 'created_at': 1660896109, - 'kind': 1, - 'tags': [ - [ - EventTags.Delegation, - '86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e', - 'kind=1&created_at>1640995200', - 'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1', - ], - ], - 'content': 'Hello world', - 'sig': 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6', - } - }) - - afterEach(() => { - parentIsEventValidStub.restore() - }) - - it('returns undefined if event and delegate tag is valid', async () => { - parentIsEventValidStub.resolves(undefined) - - expect(await (handler as any).isEventValid(event)).to.be.undefined - }) - - it('returns reason if event is not valid', () => { - parentIsEventValidStub.resolves('reason') - return expect((handler as any).isEventValid(event)).to.eventually.equal('reason') - }) - - it('returns reason if delegate signature is not valid', () => { - parentIsEventValidStub.resolves(undefined) - - event.tags[0][3] = 'wrong sig' - return expect((handler as any).isEventValid(event)).to.eventually.equal('invalid: delegation verification failed') - }) - }) -}) diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index fd1b02df..abe026f9 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -74,7 +74,7 @@ describe('EventRepository', () => { const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\') or "event_delegator" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\')) order by "event_created_at" asc limit 500') + expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\')) order by "event_created_at" asc limit 500') }) it('selects events by two authors', () => { @@ -89,7 +89,7 @@ describe('EventRepository', () => { const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\') or "event_delegator" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\')) order by "event_created_at" asc limit 500') + expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\')) order by "event_created_at" asc limit 500') }) it('selects events by one author prefix (even length)', () => { @@ -103,7 +103,7 @@ describe('EventRepository', () => { const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\' or substring("event_delegator" from 1 for 3) = X\'22e804\') order by "event_created_at" asc limit 500') + expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\') order by "event_created_at" asc limit 500') }) it('selects events by one author prefix (odd length)', () => { @@ -117,7 +117,7 @@ describe('EventRepository', () => { const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\' or substring("event_delegator" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\') order by "event_created_at" asc limit 500') + expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\') order by "event_created_at" asc limit 500') }) it('selects events by two author prefix (first even, second odd)', () => { @@ -132,7 +132,7 @@ describe('EventRepository', () => { const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\' or substring("event_delegator" from 1 for 3) = X\'22e804\' or substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x32e18270\' AND E\'\\\\x32e1827f\' or substring("event_delegator" from 1 for 4) BETWEEN E\'\\\\x32e18270\' AND E\'\\\\x32e1827f\') order by "event_created_at" asc limit 500') + expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\' or substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x32e18270\' AND E\'\\\\x32e1827f\') order by "event_created_at" asc limit 500') }) }) @@ -363,7 +363,7 @@ describe('EventRepository', () => { const query = repository.findByFilters(filters).toString() - expect(query).to.equal('(select * from "events" where "event_kind" in (1)) union (select * from "events" where (substring("event_id" from 1 for 3) BETWEEN E\'\\\\xaaaaa0\' AND E\'\\\\xaaaaaf\') order by "event_created_at" asc limit 500) union (select * from "events" where (substring("event_pubkey" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\' or substring("event_delegator" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\') order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc limit 500) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc limit 500') + expect(query).to.equal('(select * from "events" where "event_kind" in (1)) union (select * from "events" where (substring("event_id" from 1 for 3) BETWEEN E\'\\\\xaaaaa0\' AND E\'\\\\xaaaaaf\') order by "event_created_at" asc limit 500) union (select * from "events" where (substring("event_pubkey" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\') order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc limit 500) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc limit 500') }) }) }) @@ -435,7 +435,7 @@ describe('EventRepository', () => { const query = (repository as any).insert(event).toString() - expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (\'I\'\'ve set up mirroring between relays: https://i.imgur.com/HxCDipB.png\', 1648351380, NULL, X\'6b3cdd0302ded8068ad3f0269c74423ca4fee460f800f3d90103b63f14400407\', 1, X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'b37adfed0e6398546d623536f9ddc92b95b7dc71927e1123266332659253ecd0ffa91ddf2c0a82a8426c5b363139d28534d6cac893b8a810149557a3f6d36768\', \'[["p","8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9","wss://nostr-pub.wellorder.net"],["e","7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96","wss://nostr-relay.untethr.me"]]\', NULL, \'::1\') on conflict do nothing') + expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (\'I\'\'ve set up mirroring between relays: https://i.imgur.com/HxCDipB.png\', 1648351380, X\'6b3cdd0302ded8068ad3f0269c74423ca4fee460f800f3d90103b63f14400407\', 1, X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'b37adfed0e6398546d623536f9ddc92b95b7dc71927e1123266332659253ecd0ffa91ddf2c0a82a8426c5b363139d28534d6cac893b8a810149557a3f6d36768\', \'[["p","8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9","wss://nostr-pub.wellorder.net"],["e","7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96","wss://nostr-relay.untethr.me"]]\', NULL, \'::1\') on conflict do nothing') }) }) @@ -462,7 +462,7 @@ describe('EventRepository', () => { const query = repository.upsert(event).toString() - expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626') + expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626') }) it('replaces event based on event_pubkey, event_kind and event_deduplication', () => { @@ -480,7 +480,7 @@ describe('EventRepository', () => { const query = repository.upsert(event).toString() - expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626') + expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626') }) }) }) diff --git a/test/unit/utils/event.spec.ts b/test/unit/utils/event.spec.ts index 0009decb..f845b5a5 100644 --- a/test/unit/utils/event.spec.ts +++ b/test/unit/utils/event.spec.ts @@ -3,8 +3,6 @@ import { expect } from 'chai' import { CanonicalEvent, Event } from '../../../src/@types/event' import { getEventExpiration, - isDelegatedEvent, - isDelegatedEventValid, isDeleteEvent, isEphemeralEvent, isEventIdValid, @@ -400,75 +398,6 @@ describe('NIP-16', () => { // }) // }) -describe('NIP-26', () => { - let event: Event - beforeEach(() => { - event = { - 'id': 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc', - 'pubkey': '62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49', - 'created_at': 1660896109, - 'kind': 1, - 'tags': [ - [ - 'delegation', - '86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e', - 'kind=1&created_at>1640995200', - 'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1', - ], - ], - 'content': 'Hello world', - 'sig': 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6', - } - }) - - describe('isDelegatedEvent', () => { - it('returns true if event contains delegation tag', () => { - expect(isDelegatedEvent(event)).to.be.true - }) - }) - - describe('isDelegatedEventValid', () => { - it('resolves with true if delegated event is valid', async () => { - expect(await isDelegatedEventValid(event)).to.be.true - }) - - it('resolves with false if no delegation tag is found', async () => { - event.tags = [] - expect(await isDelegatedEventValid(event)).to.be.false - }) - - it('resolves with false if delegation signature is invalid', async () => { - event.tags[0][3] = 'f' - expect(await isDelegatedEventValid(event)).to.be.false - }) - - it('resolves with false if delegation rule is not a valid rune', async () => { - event.tags[0][2] = '@' - expect(await isDelegatedEventValid(event)).to.be.false - }) - - - it('resolves with false if no delegation rule does not match', async () => { - event.tags[0][2] = 'a=1' - expect(await isDelegatedEventValid(event)).to.be.false - }) - }) - - describe('isEventMatchingFilter', () => { - it('returns true if author is delegator', () => { - expect( - isEventMatchingFilter({ authors: ['86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e'] })(event) - ).to.be.true - }) - - it('returns false if author is not delegator', () => { - expect( - isEventMatchingFilter({ authors: ['e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7'] })(event) - ).to.be.false - }) - }) -}) - describe('NIP-09', () => { describe('isDeleteEvent', () => { it('returns true if event is kind 5', () => {