feat: allow lightning zap receipts on paid relays (#303)

* chore: add event_kinds whitelist for fee schedules

* chore: fix identation in default-settings.yml

* chore: waive admission fee for specific event kinds

* docs: add payment settings to CONFIGURATION.md

* docs: improve read replica docs
This commit is contained in:
Ricardo Arturo Cabral Mejía 2023-05-12 19:49:51 -04:00
parent 565f67c5f6
commit 14bc96f516
No known key found for this signature in database
GPG Key ID: 5931EBF43A650245
7 changed files with 300 additions and 105 deletions

View File

@ -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 `<project_root>/.nostr/settings.yaml`. If the file is not created and an error is thrown ensure that the `<project_root>/.nostr` folder exists. The configuration directory can be changed by setting the `NOSTR_CONFIG_DIR` environment variable.
@ -54,6 +73,18 @@ Running `nostream` for the first time creates the settings file in `<project_roo
| info.contact | Relay operator's contact. (e.g. mailto:operator@relay-your-domain.com) |
| network.maxPayloadSize | Maximum number of bytes accepted per WebSocket frame |
| network.remoteIpHeader | HTTP header from proxy containing IP address from client. |
| payments.enabled | Enabled payments. Defaults to false. |
| payments.processor | Either `zebedee`, `lnbits`, `lnurl`. |
| payments.feeSchedules.admission[].enabled | Enables admission fee. Defaults to false. |
| payments.feeSchedules.admission[].amount | Admission fee amount in msats. |
| payments.feeSchedules.admission[].whitelists.pubkeys | List of pubkeys to waive admission fee. |
| payments.feeSchedules.admission[].whitelists.event_kinds | List of event kinds to waive admission fee. Use `[min, max]` for ranges. |
| paymentProcessors.zebedee.baseURL | Zebedee's API base URL. |
| paymentProcessors.zebedee.callbackBaseURL | Public-facing Nostream's Zebedee Callback URL (e.g. https://relay.your-domain.com/callbacks/zebedee) |
| paymentProcessors.zebedee.ipWhitelist | List with Zebedee's API Production IPs. See [ZBD API Documentation](https://api-reference.zebedee.io/#c7e18276-6935-4cca-89ae-ad949efe9a6a) for more info. |
| paymentProcessors.lnbits.baseURL | Base URL of your Lnbits instance. |
| paymentProcessors.lnbits.callbackBaseURL | Public-facing Nostream's Lnbits Callback URL. (e.g. https://relay.your-domain.com/callbacks/lnbits) |
| paymentProcessors.lnurl.invoiceURL | [LUD-06 Pay Request](https://github.com/lnurl/luds/blob/luds/06.md) provider URL. (e.g. https://getalby.com/lnurlp/your-username) |
| mirroring.static[].address | Address of mirrored relay. (e.g. ws://100.100.100.100:8008) |
| mirroring.static[].filters | Subscription filters used to mirror. |
| mirroring.static[].secret | Secret to pass to relays. Nostream relays only. Optional. |

View File

@ -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

View File

@ -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/
@ -43,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
@ -76,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
@ -149,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"

View File

@ -126,6 +126,7 @@ export interface Worker {
export interface FeeScheduleWhitelists {
pubkeys?: Pubkey[]
event_kinds?: (EventKinds | [EventKinds, EventKinds])[]
}
export interface FeeSchedule {

View File

@ -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,

View File

@ -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) {

View File

@ -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
})
})
})