From 2c5ee679e24862fc93e132fd0b628c6d0d2a39d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Arturo=20Cabral=20Mej=C3=ADa?= Date: Fri, 12 May 2023 19:18:48 -0400 Subject: [PATCH 1/5] chore: add event_kinds whitelist for fee schedules --- resources/default-settings.yaml | 17 ++++++++--------- src/@types/settings.ts | 1 + 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/resources/default-settings.yaml b/resources/default-settings.yaml index 1dab3296..7db0cc0e 100755 --- a/resources/default-settings.yaml +++ b/resources/default-settings.yaml @@ -9,15 +9,14 @@ payments: processor: zebedee feeSchedules: admission: - - enabled: false - description: Admission fee charged per public key in msats (1000 msats = 1 satoshi) - amount: 1000000 - whitelists: - pubkeys: - - replace-with-your-pubkey-in-hex - # Allow the following Zap providers: - # LightningTipBot by Calle - - "fcd720c38d9ee337188f47aac845dcd8f590ccdb4a928b76dde18187b4c9d37d" + - enabled: false + description: Admission fee charged per public key in msats (1000 msats = 1 satoshi) + amount: 1000000 + whitelists: + pubkeys: + - replace-with-your-pubkey-in-hex + event_kinds: + - 9735 # Nip-57 Lightning Zap Receipts paymentsProcessors: zebedee: baseURL: https://api.zebedee.io/ diff --git a/src/@types/settings.ts b/src/@types/settings.ts index c7920d93..3e3bd688 100644 --- a/src/@types/settings.ts +++ b/src/@types/settings.ts @@ -126,6 +126,7 @@ export interface Worker { export interface FeeScheduleWhitelists { pubkeys?: Pubkey[] + event_kinds?: (EventKinds | [EventKinds, EventKinds])[] } export interface FeeSchedule { From a94f419f8e8bd24f11b7c32fe2220392aa94e974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Arturo=20Cabral=20Mej=C3=ADa?= Date: Fri, 12 May 2023 19:21:29 -0400 Subject: [PATCH 2/5] chore: fix identation in default-settings.yml --- resources/default-settings.yaml | 158 ++++++++++++++++---------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/resources/default-settings.yaml b/resources/default-settings.yaml index 7db0cc0e..c673c5f8 100755 --- a/resources/default-settings.yaml +++ b/resources/default-settings.yaml @@ -42,24 +42,24 @@ mirroring: limits: invoice: rateLimits: - - period: 60000 - rate: 12 - - period: 3600000 - rate: 30 + - period: 60000 + rate: 12 + - period: 3600000 + rate: 30 ipWhitelist: - - "::1" - - "10.10.10.1" - - "::ffff:10.10.10.1" + - "::1" + - "10.10.10.1" + - "::ffff:10.10.10.1" connection: rateLimits: - - period: 1000 - rate: 12 - - period: 60000 - rate: 48 + - period: 1000 + rate: 12 + - period: 60000 + rate: 48 ipWhitelist: - - "::1" - - "10.10.10.1" - - "::ffff:10.10.10.1" + - "::1" + - "10.10.10.1" + - "::ffff:10.10.10.1" event: eventId: minLeadingZeroBits: 0 @@ -75,69 +75,69 @@ limits: maxPositiveDelta: 900 maxNegativeDelta: 0 content: - - description: 64 KB for event kind ranges 0-10 and 40-49 - kinds: - - - 0 - - 10 - - - 40 - - 49 - maxLength: 102400 - - description: 96 KB for event kind ranges 11-39 and 50-max - kinds: - - - 11 - - 39 - - - 50 - - 9007199254740991 - maxLength: 102400 + - description: 64 KB for event kind ranges 0-10 and 40-49 + maxLength: 102400 + kinds: + - - 0 + - 10 + - - 40 + - 49 + - description: 96 KB for event kind ranges 11-39 and 50-max + maxLength: 102400 + kinds: + - - 11 + - 39 + - - 50 + - 9007199254740991 rateLimits: - - description: 6 events/min for event kinds 0, 3, 40 and 41 - kinds: - - 0 - - 3 - - 40 - - 41 - period: 60000 - rate: 6 - - description: 12 events/min for event kinds 1, 2, 4 and 42 - kinds: - - 1 - - 2 - - 4 - - 42 - period: 60000 - rate: 12 - - description: 30 events/min for event kind ranges 5-7 and 43-49 - kinds: - - - 5 - - 7 - - - 43 - - 49 - period: 60000 - rate: 30 - - description: 24 events/min for replaceable events and parameterized replaceable - events - kinds: - - - 10000 - - 19999 - - - 30000 - - 39999 - period: 60000 - rate: 24 - - description: 60 events/min for ephemeral events - kinds: - - - 20000 - - 29999 - period: 60000 - rate: 60 - - description: 720 events/hour for all events - period: 3600000 - rate: 720 + - description: 6 events/min for event kinds 0, 3, 40 and 41 + kinds: + - 0 + - 3 + - 40 + - 41 + period: 60000 + rate: 6 + - description: 12 events/min for event kinds 1, 2, 4 and 42 + kinds: + - 1 + - 2 + - 4 + - 42 + period: 60000 + rate: 12 + - description: 30 events/min for event kind ranges 5-7 and 43-49 + kinds: + - - 5 + - 7 + - - 43 + - 49 + period: 60000 + rate: 30 + - description: 24 events/min for replaceable events and parameterized replaceable + events + kinds: + - - 10000 + - 19999 + - - 30000 + - 39999 + period: 60000 + rate: 24 + - description: 60 events/min for ephemeral events + kinds: + - - 20000 + - 29999 + period: 60000 + rate: 60 + - description: 720 events/hour for all events + period: 3600000 + rate: 720 whitelists: pubkeys: [] ipAddresses: - - "::1" - - "10.10.10.1" - - "::ffff:10.10.10.1" + - "::1" + - "10.10.10.1" + - "::ffff:10.10.10.1" client: subscription: maxSubscriptions: 10 @@ -148,10 +148,10 @@ limits: minPrefixLength: 4 message: rateLimits: - - description: 240 raw messages/min - period: 60000 - rate: 240 + - description: 240 raw messages/min + period: 60000 + rate: 240 ipWhitelist: - - "::1" - - "10.10.10.1" - - "::ffff:10.10.10.1" + - "::1" + - "10.10.10.1" + - "::ffff:10.10.10.1" From 50e36b7417352613f87370cfa9b9502cb15b58a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Arturo=20Cabral=20Mej=C3=ADa?= Date: Fri, 12 May 2023 19:22:42 -0400 Subject: [PATCH 3/5] chore: waive admission fee for specific event kinds --- src/constants/base.ts | 4 + src/handlers/event-message-handler.ts | 1 + .../handlers/event-message-handler.spec.ts | 174 +++++++++++++++++- 3 files changed, 171 insertions(+), 8 deletions(-) diff --git a/src/constants/base.ts b/src/constants/base.ts index 37bce50e..fd6b2a9a 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -5,6 +5,7 @@ export enum EventKinds { CONTACT_LIST = 3, ENCRYPTED_DIRECT_MESSAGE = 4, DELETE = 5, + REPOST = 6, REACTION = 7, // Channels CHANNEL_CREATION = 40, @@ -17,6 +18,9 @@ export enum EventKinds { // Relay-only RELAY_INVITE = 50, INVOICE_UPDATE = 402, + // Lightning zaps + ZAP_REQUEST = 9734, + ZAP_RECEIPT = 9735, // Replaceable events REPLACEABLE_FIRST = 10000, REPLACEABLE_LAST = 19999, diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index 07264656..f5bac291 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -268,6 +268,7 @@ export class EventMessageHandler implements IMessageHandler { const isApplicableFee = (feeSchedule: FeeSchedule) => feeSchedule.enabled && !feeSchedule.whitelists?.pubkeys?.some((prefix) => event.pubkey.startsWith(prefix)) + && !feeSchedule.whitelists?.event_kinds?.some(isEventKindOrRangeMatch(event)) const feeSchedules = currentSettings.payments?.feeSchedules?.admission?.filter(isApplicableFee) if (!Array.isArray(feeSchedules) || !feeSchedules.length) { diff --git a/test/unit/handlers/event-message-handler.spec.ts b/test/unit/handlers/event-message-handler.spec.ts index 9cd73788..c28d5e0a 100644 --- a/test/unit/handlers/event-message-handler.spec.ts +++ b/test/unit/handlers/event-message-handler.spec.ts @@ -585,7 +585,7 @@ describe('EventMessageHandler', () => { it('returns undefined if kind is not blacklisted in range', () => { eventLimits.kind.blacklist = [[1, 5]] - event.kind = 6 + event.kind = EventKinds.REACTION expect( (handler as any).canAcceptEvent(event) ).to.be.undefined @@ -652,10 +652,10 @@ describe('EventMessageHandler', () => { it('returns reason if kind is not whitelisted in range', () => { eventLimits.kind.whitelist = [[1, 5]] - event.kind = 6 + event.kind = EventKinds.REACTION expect( (handler as any).canAcceptEvent(event) - ).to.equal('blocked: event kind 6 not allowed') + ).to.equal('blocked: event kind 7 not allowed') }) }) }) @@ -867,11 +867,6 @@ describe('EventMessageHandler', () => { period: 60000, rate: 2, }, - { - kinds: [[10, 20]], - period: 86400000, - rate: 3, - }, ] rateLimiterHitStub.resolves(false) @@ -931,4 +926,167 @@ describe('EventMessageHandler', () => { expect(actualResult).to.be.true }) }) + + describe('isUserAdmitted', () => { + let settings: Settings + let userRepository: IUserRepository + let getClientAddressStub: SinonStub + let webSocket: IWebSocketAdapter + let getRelayPublicKeyStub: SinonStub + let userRepositoryFindByPubkeyStub: SinonStub + + beforeEach(() => { + settings = { + info: { + relay_url: 'relay_url', + }, + payments: { + enabled: true, + feeSchedules: { + admission: [ + { + enabled: true, + amount: 1000n, + whitelists: { + pubkeys: [], + event_kinds: [], + }, + }, + ], + }, + }, + limits: { + event: { + pubkey: { + minBalance: 0n, + }, + }, + }, + } as any + event = { + content: 'hello', + created_at: 1665546189, + id: 'f'.repeat(64), + kind: 1, + pubkey: 'f'.repeat(64), + sig: 'f'.repeat(128), + tags: [], + } + getRelayPublicKeyStub = sandbox.stub(EventMessageHandler.prototype, 'getRelayPublicKey' as any) + getClientAddressStub = sandbox.stub() + userRepositoryFindByPubkeyStub = sandbox.stub() + webSocket = { + getClientAddress: getClientAddressStub, + } as any + userRepository = { + findByPubkey: userRepositoryFindByPubkeyStub, + } as any + handler = new EventMessageHandler( + webSocket, + () => null, + userRepository, + () => settings, + () => ({ hit: async () => false }) + ) + }) + + it ('fulfills with undefined if payments are disabled', async () => { + settings.payments.enabled = false + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + + it('fulfills with undefined if event pubkey equals relay\'s own public key', async () => { + getRelayPublicKeyStub.returns(event.pubkey) + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + + it('fulfills with undefined if fee schedules are not set', async () => { + settings.payments.feeSchedules = undefined + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + + it('fulfills with undefined if admission fee schedules are not set', async () => { + settings.payments.feeSchedules.admission = undefined + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + + it('fulfills with undefined if there are no admission fee schedules', async () => { + settings.payments.feeSchedules.admission = [] + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + + it('fulfills with undefined if there are no enabled admission fee schedules', async () => { + settings.payments.feeSchedules.admission[0].enabled = false + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + + it('fulfills with undefined if admission fee schedule is waived for pubkey', async () => { + settings.payments.feeSchedules.admission[0].whitelists.pubkeys.push(event.pubkey) + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + + it('fulfills with undefined if admission fee schedule is waived for event kind', async () => { + event.kind = EventKinds.ZAP_RECEIPT + settings.payments.feeSchedules.admission[0].whitelists.event_kinds.push(EventKinds.ZAP_RECEIPT) + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + + it('fulfills with undefined if admission fee schedule is waived for event kind range', async () => { + event.kind = EventKinds.TEXT_NOTE + settings.payments.feeSchedules.admission[0].whitelists.event_kinds.push([ + EventKinds.SET_METADATA, + EventKinds.RECOMMEND_SERVER, + ]) + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + + it('fulfills with reason if admission fee schedule is not waived for event kind range', async () => { + event.kind = EventKinds.CONTACT_LIST + settings.payments.feeSchedules.admission[0].whitelists.event_kinds.push([ + EventKinds.SET_METADATA, + EventKinds.RECOMMEND_SERVER, + ]) + + return expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: pubkey not admitted') + }) + + it('fulfills with reason if user is not found', async () => { + return expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: pubkey not admitted') + }) + + it('fulfills with reason if user is not admitted', async () => { + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: false }) + + return expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: pubkey not admitted') + }) + + it('fulfills with reason if user is not admitted', async () => { + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: false }) + + return expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: pubkey not admitted') + }) + + it('fulfills with reason if user does not meet minimum balance', async () => { + settings.limits.event.pubkey.minBalance = 1000n + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: true, balance: 999n }) + + return expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: insufficient balance') + }) + + it('fulfills with undefined if user is admitted', async () => { + settings.limits.event.pubkey.minBalance = 0n + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: true }) + + return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined + }) + }) }) From 7b22cdf988e08cf2bba21adb1596b3c61f6bfe53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Arturo=20Cabral=20Mej=C3=ADa?= Date: Fri, 12 May 2023 19:23:18 -0400 Subject: [PATCH 4/5] docs: add payment settings to CONFIGURATION.md --- CONFIGURATION.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 3e58a6b4..7365333f 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -54,6 +54,18 @@ Running `nostream` for the first time creates the settings file in ` Date: Fri, 12 May 2023 19:41:16 -0400 Subject: [PATCH 5/5] docs: improve read replica docs --- CONFIGURATION.md | 35 +++++++++++++++++++++++++++-------- docker-compose.yml | 3 ++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 7365333f..4edec215 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -20,14 +20,31 @@ The following environment variables can be set: | DB_MAX_POOL_SIZE | Max. connections per worker | 32 | | DB_ACQUIRE_CONNECTION_TIMEOUT | New connection timeout (ms) | 60000 | | READ_REPLICA_ENABLED | Read Replica (RR) Toggle | 'false' | -| RR_DB_HOST | PostgresSQL Hostname (RR) | | -| RR_DB_PORT | PostgreSQL Port (RR) | 5432 | -| RR_DB_USER | PostgreSQL Username (RR) | nostr_ts_relay | -| RR_DB_PASSWORD | PostgreSQL Password (RR) | nostr_ts_relay | -| RR_DB_NAME | PostgreSQL Database name (RR) | nostr_ts_relay | -| RR_DB_MIN_POOL_SIZE | Min. connections per worker (RR) | 16 | -| RR_DB_MAX_POOL_SIZE | Max. connections per worker (RR) | 32 | -| RR_DB_ACQUIRE_CONNECTION_TIMEOUT | New connection timeout (ms) (RR) | 60000 | +| READ_REPLICAS | Number of read replicas (RR0, RR1, ..., RRn) | 2 | +| RR0_DB_HOST | PostgresSQL Hostname (RR) | | +| RR0_DB_PORT | PostgreSQL Port (RR) | 5432 | +| RR0_DB_USER | PostgreSQL Username (RR) | nostr_ts_relay | +| RR0_DB_PASSWORD | PostgreSQL Password (RR) | nostr_ts_relay | +| RR0_DB_NAME | PostgreSQL Database name (RR) | nostr_ts_relay | +| RR0_DB_MIN_POOL_SIZE | Min. connections per worker (RR) | 16 | +| RR0_DB_MAX_POOL_SIZE | Max. connections per worker (RR) | 32 | +| RR0_DB_ACQUIRE_CONNECTION_TIMEOUT| New connection timeout (ms) (RR) | 60000 | +| RR1_DB_HOST | PostgresSQL Hostname (RR) | | +| RR1_DB_PORT | PostgreSQL Port (RR) | 5432 | +| RR1_DB_USER | PostgreSQL Username (RR) | nostr_ts_relay | +| RR1_DB_PASSWORD | PostgreSQL Password (RR) | nostr_ts_relay | +| RR1_DB_NAME | PostgreSQL Database name (RR) | nostr_ts_relay | +| RR1_DB_MIN_POOL_SIZE | Min. connections per worker (RR) | 16 | +| RR1_DB_MAX_POOL_SIZE | Max. connections per worker (RR) | 32 | +| RR1_DB_ACQUIRE_CONNECTION_TIMEOUT| New connection timeout (ms) (RR) | 60000 | +| RRn_DB_HOST | PostgresSQL Hostname (RR) | | +| RRn_DB_PORT | PostgreSQL Port (RR) | 5432 | +| RRn_DB_USER | PostgreSQL Username (RR) | nostr_ts_relay | +| RRn_DB_PASSWORD | PostgreSQL Password (RR) | nostr_ts_relay | +| RRn_DB_NAME | PostgreSQL Database name (RR) | nostr_ts_relay | +| RRn_DB_MIN_POOL_SIZE | Min. connections per worker (RR) | 16 | +| RRn_DB_MAX_POOL_SIZE | Max. connections per worker (RR) | 32 | +| RRn_DB_ACQUIRE_CONNECTION_TIMEOUT| New connection timeout (ms) (RR) | 60000 | | TOR_HOST | Tor Hostname | | | TOR_CONTROL_PORT | Tor control Port | 9051 | | TOR_PASSWORD | Tor control password | nostr_ts_relay | @@ -41,6 +58,8 @@ The following environment variables can be set: | DEBUG | Debugging filter | | | ZEBEDEE_API_KEY | Zebedee Project API Key | | +If you've set READ_REPLICAS to 4, you should configure RR0_ through RR3_. + # Settings Running `nostream` for the first time creates the settings file in `/.nostr/settings.yaml`. If the file is not created and an error is thrown ensure that the `/.nostr` folder exists. The configuration directory can be changed by setting the `NOSTR_CONFIG_DIR` environment variable. diff --git a/docker-compose.yml b/docker-compose.yml index 9a29891c..b425a288 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: DB_MAX_POOL_SIZE: 64 DB_ACQUIRE_CONNECTION_TIMEOUT: 60000 # Read Replica - READ_REPLICAS: 1 + READ_REPLICAS: 2 READ_REPLICA_ENABLED: 'false' # Read Replica No. 1 RR0_DB_HOST: db @@ -36,6 +36,7 @@ services: RR1_DB_MIN_POOL_SIZE: 16 RR1_DB_MAX_POOL_SIZE: 64 RR1_DB_ACQUIRE_CONNECTION_TIMEOUT: 10000 + # Add RR2, RR3, etc. to configure more read replicas # Redis REDIS_HOST: nostream-cache REDIS_PORT: 6379