From 8f66a263c4fadba4136c8b6ca3ec11fe46341e73 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Thu, 14 Mar 2024 21:38:28 +0100 Subject: [PATCH] involve lnd in sanity check --- src/index.ts | 7 ++-- src/services/lnd/index.ts | 4 +- src/services/lnd/lnd.ts | 11 +++++- src/services/lnd/mock.ts | 13 +++++-- src/services/lnd/settings.ts | 1 + src/services/main/index.ts | 71 ++++++++++++++++++++++++++++++++++- src/services/storage/index.ts | 37 +----------------- 7 files changed, 98 insertions(+), 46 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3f8f435e..a9651b57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,11 +22,12 @@ const start = async () => { await storageManager.userStorage.UpdateUser(process.argv[3], { balance_sats: +process.argv[4] }) log("user balance updated correctly") } - if (!mainSettings.skipSanityCheck) { - await storageManager.VerifyEventsLog() - } + const mainHandler = new Main(mainSettings, storageManager) await mainHandler.lnd.Warmup() + if (!mainSettings.skipSanityCheck) { + await mainHandler.VerifyEventsLog() + } const serverMethods = GetServerMethods(mainHandler) const nostrSettings = LoadNosrtSettingsFromEnv() const appsData = await mainHandler.storage.applicationStorage.GetApplications() diff --git a/src/services/lnd/index.ts b/src/services/lnd/index.ts index 7443a7d8..bc1309cb 100644 --- a/src/services/lnd/index.ts +++ b/src/services/lnd/index.ts @@ -1,5 +1,5 @@ import * as Types from '../../../proto/autogenerated/ts/types.js' -import { GetInfoResponse, NewAddressResponse, AddInvoiceResponse, PayReq, Payment, SendCoinsResponse, EstimateFeeResponse, TransactionDetails, ClosedChannelsResponse, ListChannelsResponse, PendingChannelsResponse } from '../../../proto/lnd/lightning.js' +import { GetInfoResponse, NewAddressResponse, AddInvoiceResponse, PayReq, Payment, SendCoinsResponse, EstimateFeeResponse, TransactionDetails, ClosedChannelsResponse, ListChannelsResponse, PendingChannelsResponse, ListInvoiceResponse, ListPaymentsResponse } from '../../../proto/lnd/lightning.js' import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean } from '../helpers/envParser.js' import { AddressPaidCb, BalanceInfo, DecodedInvoice, HtlcCb, Invoice, InvoicePaidCb, LndSettings, NewBlockCb, NodeInfo, PaidInvoice } from './settings.js' import LND from './lnd.js' @@ -36,6 +36,8 @@ export interface LightningHandler { ListChannels(): Promise ListPendingChannels(): Promise GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]> + GetAllPaidInvoices(max: number): Promise + GetAllPayments(max: number): Promise } export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => { diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index f8d5c616..b49e5513 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -233,7 +233,7 @@ export default class { async DecodeInvoice(paymentRequest: string): Promise { const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) - return { numSatoshis: Number(res.response.numSatoshis) } + return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } } GetFeeLimitAmount(amount: number): number { @@ -315,6 +315,15 @@ export default class { return response.forwardingEvents.map(e => ({ fee: Number(e.fee), chanIdIn: e.chanIdIn, chanIdOut: e.chanIdOut, timestampNs: Number(e.timestampNs), offset: response.lastOffsetIndex })) } + async GetAllPaidInvoices(max: number) { + const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true }, DeadLineMetadata()) + return res.response + } + async GetAllPayments(max: number) { + const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true }) + return res.response + } + async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise { await this.Health() const abortController = new AbortController() diff --git a/src/services/lnd/mock.ts b/src/services/lnd/mock.ts index b550f695..44b2e6cb 100644 --- a/src/services/lnd/mock.ts +++ b/src/services/lnd/mock.ts @@ -7,7 +7,7 @@ import * as Types from '../../../proto/autogenerated/ts/types.js' import { LightningClient } from '../../../proto/lnd/lightning.client.js' import { InvoicesClient } from '../../../proto/lnd/invoices.client.js' import { RouterClient } from '../../../proto/lnd/router.client.js' -import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, TransactionDetails, ClosedChannelsResponse, ListChannelsResponse, PendingChannelsResponse } from '../../../proto/lnd/lightning.js' +import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, TransactionDetails, ClosedChannelsResponse, ListChannelsResponse, PendingChannelsResponse, ListInvoiceResponse, ListPaymentsResponse } from '../../../proto/lnd/lightning.js' import { OpenChannelReq } from './openChannelReq.js'; import { AddInvoiceReq } from './addInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js'; @@ -63,13 +63,13 @@ export default class { async DecodeInvoice(paymentRequest: string): Promise { if (paymentRequest.startsWith('lnbcrtmockout')) { const amt = this.decodeOutboundInvoice(paymentRequest) - return { numSatoshis: amt } + return { numSatoshis: amt, paymentHash: paymentRequest } } const i = this.invoicesAwaiting[paymentRequest] if (!i) { throw new Error("invoice not found") } - return { numSatoshis: i.value } + return { numSatoshis: i.value, paymentHash: paymentRequest } } GetFeeLimitAmount(amount: number): number { @@ -124,6 +124,13 @@ export default class { GetBalance(): Promise { throw new Error("GetBalance disabled in mock mode") } + + async GetAllPaidInvoices(max: number): Promise { + throw new Error("not implemented") + } + async GetAllPayments(max: number): Promise { + throw new Error("not implemented") + } } diff --git a/src/services/lnd/settings.ts b/src/services/lnd/settings.ts index 1939af06..43f0dba3 100644 --- a/src/services/lnd/settings.ts +++ b/src/services/lnd/settings.ts @@ -40,6 +40,7 @@ export type Invoice = { } export type DecodedInvoice = { numSatoshis: number + paymentHash: string } export type PaidInvoice = { feeSat: number diff --git a/src/services/main/index.ts b/src/services/main/index.ts index a4761d49..897b5dd8 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -16,7 +16,7 @@ import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingIn import { UnsignedEvent } from '../nostr/tools/event.js' import { NostrSend } from '../nostr/handler.js' import MetricsManager from '../metrics/index.js' -import EventsLogManager from '../storage/eventsLog.js' +import EventsLogManager, { LoggedEvent } from '../storage/eventsLog.js' export const LoadMainSettingsFromEnv = (test = false): MainSettings => { return { lndSettings: LoadLndSettingsFromEnv(test), @@ -238,4 +238,71 @@ export default class { log({ unsigned: event }) this.nostrSend(invoice.linkedApplication.app_id, { type: 'event', event }) } -} \ No newline at end of file + + async VerifyEventsLog() { + const events = await this.storage.eventsLog.GetAllLogs() + const invoices = await this.lnd.GetAllPaidInvoices(300) + const payments = await this.lnd.GetAllPayments(300) + const verifyWithLnd = (type: "balance_decrement" | "balance_increment", invoice: string) => { + if (type === 'balance_decrement') { + const entry = payments.payments.find(p => p.paymentRequest === invoice) + if (!entry) { + throw new Error("payment not found in lnd " + invoice) + } + return Number(entry.valueSat) + } + const entry = invoices.invoices.find(i => i.paymentRequest === invoice) + if (!entry) { + throw new Error("invoice not found in lnd " + invoice) + } + return Number(entry.amtPaidSat) + } + + const users: Record = {} + for (let i = 0; i < events.length; i++) { + const e = events[i] + if (e.type === 'balance_decrement' || e.type === 'balance_increment') { + users[e.userId] = this.checkUserEntry(e, users[e.userId]) + if (LN_INVOICE_REGEX.test(e.data)) { + const invoiceEntry = await this.storage.paymentStorage.GetInvoiceOwner(e.data) + if (!invoiceEntry) { + throw new Error("invoice entry not found for " + e.data) + } + if (invoiceEntry.paid_at_unix === 0) { + throw new Error("invoice was never paid " + e.data) + } + if (!invoiceEntry.internal) { + const amt = verifyWithLnd(e.type, e.data) + if (amt !== e.amount) { + throw new Error(`invalid amounts got: ${amt} expected: ${e.amount}`) + } + } + } + } else { + await this.storage.paymentStorage.VerifyDbEvent(e) + } + } + await Promise.all(Object.entries(users).map(async ([userId, u]) => { + const user = await this.storage.userStorage.GetUser(userId) + if (user.balance_sats !== u.updatedBalance) { + throw new Error("sanity check on balance failed, expected: " + u.updatedBalance + " found: " + user.balance_sats) + } + })) + } + + checkUserEntry(e: LoggedEvent, u: { ts: number, updatedBalance: number } | undefined) { + const newEntry = { ts: e.timestampMs, updatedBalance: e.balance + e.amount * (e.type === 'balance_decrement' ? -1 : 1) } + if (!u) { + return newEntry + } + if (e.timestampMs < u.ts) { + throw new Error("entry out of order " + e.timestampMs + " " + u.ts) + } + if (e.balance !== u.updatedBalance) { + throw new Error("inconsistent balance update got: " + e.balance + " expected " + u.updatedBalance) + } + return newEntry + } +} + +const LN_INVOICE_REGEX = /^(lightning:)?(lnbc|lntb)[0-9a-zA-Z]+$/; \ No newline at end of file diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 512889f9..9029cf04 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -6,14 +6,13 @@ import UserStorage from "./userStorage.js"; import PaymentStorage from "./paymentStorage.js"; import MetricsStorage from "./metricsStorage.js"; import TransactionsQueue, { TX } from "./transactionsQueue.js"; -import EventsLogManager, { LoggedEvent } from "./eventsLog.js"; +import EventsLogManager from "./eventsLog.js"; export type StorageSettings = { dbSettings: DbSettings } export const LoadStorageSettingsFromEnv = (test = false): StorageSettings => { return { dbSettings: LoadDbSettingsFromEnv(test) } } - export default class { DB: DataSource | EntityManager settings: StorageSettings @@ -41,40 +40,6 @@ export default class { return { executedMigrations, executedMetricsMigrations }; } - async VerifyEventsLog() { - const events = await this.eventsLog.GetAllLogs() - - const users: Record = {} - for (let i = 0; i < events.length; i++) { - const e = events[i] - if (e.type === 'balance_decrement' || e.type === 'balance_increment') { - users[e.userId] = this.checkUserEntry(e, users[e.userId]) - } else { - await this.paymentStorage.VerifyDbEvent(e) - } - } - await Promise.all(Object.entries(users).map(async ([userId, u]) => { - const user = await this.userStorage.GetUser(userId) - if (user.balance_sats !== u.updatedBalance) { - throw new Error("sanity check on balance failed, expected: " + u.updatedBalance + " found: " + user.balance_sats) - } - })) - } - - checkUserEntry(e: LoggedEvent, u: { ts: number, updatedBalance: number } | undefined) { - const newEntry = { ts: e.timestampMs, updatedBalance: e.balance + e.amount * (e.type === 'balance_decrement' ? -1 : 1) } - if (!u) { - return newEntry - } - if (e.timestampMs < u.ts) { - throw new Error("entry out of order " + e.timestampMs + " " + u.ts) - } - if (e.balance !== u.updatedBalance) { - throw new Error("inconsistent balance update got: " + e.balance + " expected " + u.updatedBalance) - } - return newEntry - } - StartTransaction(exec: TX, description?: string) { return this.txQueue.PushToQueue({ exec, dbTx: true, description }) }