involve lnd in sanity check
This commit is contained in:
parent
2a8ab8cf59
commit
8f66a263c4
7 changed files with 98 additions and 46 deletions
|
|
@ -22,11 +22,12 @@ const start = async () => {
|
||||||
await storageManager.userStorage.UpdateUser(process.argv[3], { balance_sats: +process.argv[4] })
|
await storageManager.userStorage.UpdateUser(process.argv[3], { balance_sats: +process.argv[4] })
|
||||||
log("user balance updated correctly")
|
log("user balance updated correctly")
|
||||||
}
|
}
|
||||||
if (!mainSettings.skipSanityCheck) {
|
|
||||||
await storageManager.VerifyEventsLog()
|
|
||||||
}
|
|
||||||
const mainHandler = new Main(mainSettings, storageManager)
|
const mainHandler = new Main(mainSettings, storageManager)
|
||||||
await mainHandler.lnd.Warmup()
|
await mainHandler.lnd.Warmup()
|
||||||
|
if (!mainSettings.skipSanityCheck) {
|
||||||
|
await mainHandler.VerifyEventsLog()
|
||||||
|
}
|
||||||
const serverMethods = GetServerMethods(mainHandler)
|
const serverMethods = GetServerMethods(mainHandler)
|
||||||
const nostrSettings = LoadNosrtSettingsFromEnv()
|
const nostrSettings = LoadNosrtSettingsFromEnv()
|
||||||
const appsData = await mainHandler.storage.applicationStorage.GetApplications()
|
const appsData = await mainHandler.storage.applicationStorage.GetApplications()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
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 { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean } from '../helpers/envParser.js'
|
||||||
import { AddressPaidCb, BalanceInfo, DecodedInvoice, HtlcCb, Invoice, InvoicePaidCb, LndSettings, NewBlockCb, NodeInfo, PaidInvoice } from './settings.js'
|
import { AddressPaidCb, BalanceInfo, DecodedInvoice, HtlcCb, Invoice, InvoicePaidCb, LndSettings, NewBlockCb, NodeInfo, PaidInvoice } from './settings.js'
|
||||||
import LND from './lnd.js'
|
import LND from './lnd.js'
|
||||||
|
|
@ -36,6 +36,8 @@ export interface LightningHandler {
|
||||||
ListChannels(): Promise<ListChannelsResponse>
|
ListChannels(): Promise<ListChannelsResponse>
|
||||||
ListPendingChannels(): Promise<PendingChannelsResponse>
|
ListPendingChannels(): Promise<PendingChannelsResponse>
|
||||||
GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]>
|
GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]>
|
||||||
|
GetAllPaidInvoices(max: number): Promise<ListInvoiceResponse>
|
||||||
|
GetAllPayments(max: number): Promise<ListPaymentsResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => {
|
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => {
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ export default class {
|
||||||
|
|
||||||
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
|
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
|
||||||
const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
|
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 {
|
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 }))
|
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<string> {
|
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> {
|
||||||
await this.Health()
|
await this.Health()
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||||
import { LightningClient } from '../../../proto/lnd/lightning.client.js'
|
import { LightningClient } from '../../../proto/lnd/lightning.client.js'
|
||||||
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
|
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
|
||||||
import { RouterClient } from '../../../proto/lnd/router.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 { OpenChannelReq } from './openChannelReq.js';
|
||||||
import { AddInvoiceReq } from './addInvoiceReq.js';
|
import { AddInvoiceReq } from './addInvoiceReq.js';
|
||||||
import { PayInvoiceReq } from './payInvoiceReq.js';
|
import { PayInvoiceReq } from './payInvoiceReq.js';
|
||||||
|
|
@ -63,13 +63,13 @@ export default class {
|
||||||
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
|
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
|
||||||
if (paymentRequest.startsWith('lnbcrtmockout')) {
|
if (paymentRequest.startsWith('lnbcrtmockout')) {
|
||||||
const amt = this.decodeOutboundInvoice(paymentRequest)
|
const amt = this.decodeOutboundInvoice(paymentRequest)
|
||||||
return { numSatoshis: amt }
|
return { numSatoshis: amt, paymentHash: paymentRequest }
|
||||||
}
|
}
|
||||||
const i = this.invoicesAwaiting[paymentRequest]
|
const i = this.invoicesAwaiting[paymentRequest]
|
||||||
if (!i) {
|
if (!i) {
|
||||||
throw new Error("invoice not found")
|
throw new Error("invoice not found")
|
||||||
}
|
}
|
||||||
return { numSatoshis: i.value }
|
return { numSatoshis: i.value, paymentHash: paymentRequest }
|
||||||
}
|
}
|
||||||
|
|
||||||
GetFeeLimitAmount(amount: number): number {
|
GetFeeLimitAmount(amount: number): number {
|
||||||
|
|
@ -124,6 +124,13 @@ export default class {
|
||||||
GetBalance(): Promise<BalanceInfo> {
|
GetBalance(): Promise<BalanceInfo> {
|
||||||
throw new Error("GetBalance disabled in mock mode")
|
throw new Error("GetBalance disabled in mock mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async GetAllPaidInvoices(max: number): Promise<ListInvoiceResponse> {
|
||||||
|
throw new Error("not implemented")
|
||||||
|
}
|
||||||
|
async GetAllPayments(max: number): Promise<ListPaymentsResponse> {
|
||||||
|
throw new Error("not implemented")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export type Invoice = {
|
||||||
}
|
}
|
||||||
export type DecodedInvoice = {
|
export type DecodedInvoice = {
|
||||||
numSatoshis: number
|
numSatoshis: number
|
||||||
|
paymentHash: string
|
||||||
}
|
}
|
||||||
export type PaidInvoice = {
|
export type PaidInvoice = {
|
||||||
feeSat: number
|
feeSat: number
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingIn
|
||||||
import { UnsignedEvent } from '../nostr/tools/event.js'
|
import { UnsignedEvent } from '../nostr/tools/event.js'
|
||||||
import { NostrSend } from '../nostr/handler.js'
|
import { NostrSend } from '../nostr/handler.js'
|
||||||
import MetricsManager from '../metrics/index.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 => {
|
export const LoadMainSettingsFromEnv = (test = false): MainSettings => {
|
||||||
return {
|
return {
|
||||||
lndSettings: LoadLndSettingsFromEnv(test),
|
lndSettings: LoadLndSettingsFromEnv(test),
|
||||||
|
|
@ -238,4 +238,71 @@ export default class {
|
||||||
log({ unsigned: event })
|
log({ unsigned: event })
|
||||||
this.nostrSend(invoice.linkedApplication.app_id, { type: 'event', event })
|
this.nostrSend(invoice.linkedApplication.app_id, { type: 'event', event })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<string, { ts: number, updatedBalance: number }> = {}
|
||||||
|
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]+$/;
|
||||||
|
|
@ -6,14 +6,13 @@ import UserStorage from "./userStorage.js";
|
||||||
import PaymentStorage from "./paymentStorage.js";
|
import PaymentStorage from "./paymentStorage.js";
|
||||||
import MetricsStorage from "./metricsStorage.js";
|
import MetricsStorage from "./metricsStorage.js";
|
||||||
import TransactionsQueue, { TX } from "./transactionsQueue.js";
|
import TransactionsQueue, { TX } from "./transactionsQueue.js";
|
||||||
import EventsLogManager, { LoggedEvent } from "./eventsLog.js";
|
import EventsLogManager from "./eventsLog.js";
|
||||||
export type StorageSettings = {
|
export type StorageSettings = {
|
||||||
dbSettings: DbSettings
|
dbSettings: DbSettings
|
||||||
}
|
}
|
||||||
export const LoadStorageSettingsFromEnv = (test = false): StorageSettings => {
|
export const LoadStorageSettingsFromEnv = (test = false): StorageSettings => {
|
||||||
return { dbSettings: LoadDbSettingsFromEnv(test) }
|
return { dbSettings: LoadDbSettingsFromEnv(test) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
DB: DataSource | EntityManager
|
DB: DataSource | EntityManager
|
||||||
settings: StorageSettings
|
settings: StorageSettings
|
||||||
|
|
@ -41,40 +40,6 @@ export default class {
|
||||||
return { executedMigrations, executedMetricsMigrations };
|
return { executedMigrations, executedMetricsMigrations };
|
||||||
}
|
}
|
||||||
|
|
||||||
async VerifyEventsLog() {
|
|
||||||
const events = await this.eventsLog.GetAllLogs()
|
|
||||||
|
|
||||||
const users: Record<string, { ts: number, updatedBalance: number }> = {}
|
|
||||||
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<void>, description?: string) {
|
StartTransaction(exec: TX<void>, description?: string) {
|
||||||
return this.txQueue.PushToQueue({ exec, dbTx: true, description })
|
return this.txQueue.PushToQueue({ exec, dbTx: true, description })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue