aboutsummaryrefslogtreecommitdiffstats
path: root/names
diff options
context:
space:
mode:
Diffstat (limited to 'names')
-rw-r--r--names/server.js44
1 files changed, 33 insertions, 11 deletions
diff --git a/names/server.js b/names/server.js
index f2a9f41..a9ade90 100644
--- a/names/server.js
+++ b/names/server.js
@@ -388,20 +388,42 @@ const HOLD_KEYS = new Set(['a7fc7e2cbc16949c411a982fe6cecec0c1674fb4dabbdce57027
// delivered once is never delivered again — the record is saved before
// anything else happens.
const delivering = new Set();
+// Past this many sats delivered to one key within an hour, the next ones
+// wait in the queue for a person (the monitor pages on a queue older than
+// 30 min): a bug we haven't found yet costs at most this much per key.
+const VELOCITY = { windowMs: 3600_000, maxSat: 1_500_000 };
async function forward(name, sat, hash, pubkey) {
- if (hash && state.delivered?.[hash]) { log(`forward: ${hash.slice(0, 12)} was already delivered — not again`); return; }
- if (hash && delivering.has(hash)) throw new Error('already being delivered');
- if (hash) delivering.add(hash);
+ if (!hash) throw Object.assign(new Error('no payment hash — refusing to deliver'), { permanent: true });
+ if (state.delivered?.[hash]) { log(`forward: ${hash.slice(0, 12)} was already delivered — not again`); return; }
+ if (delivering.has(hash)) throw new Error('already being delivered');
+ delivering.add(hash);
try {
- await forwardOnce(name, sat, hash, pubkey);
- if (hash) {
- const all = state.delivered ||= {};
- all[hash] = Date.now();
- const cutoff = Date.now() - 60 * 86400_000;
- for (const [h, t] of Object.entries(all)) if (t < cutoff) delete all[h];
- persist();
+ // Every delivery is backed by a payment that really arrived: our node's
+ // own invoice for this hash, paid, for at least this much. Whatever
+ // path got here (settle loop, retry, operator), nothing goes out that
+ // didn't come in.
+ const inv = ln && (await ln.call('listinvoices', { payment_hash: hash })).invoices?.[0];
+ const got = Math.floor(Number(inv?.amount_received_msat?.msat ?? inv?.amount_received_msat ?? 0) / 1000);
+ if (!inv || inv.status !== 'paid' || got < sat) {
+ throw Object.assign(new Error(`no paid invoice of ${sat} sat behind ${hash.slice(0, 12)} (${inv ? inv.status + ' ' + got : 'none'}) — refusing`), { permanent: true });
+ }
+ const key = pubkey || state.names[name]?.pubkey || name;
+ const recent = Object.values(state.deliveredTo?.[key] || {}).filter((x) => x.at > Date.now() - VELOCITY.windowMs);
+ const lastHour = recent.reduce((n, x) => n + x.sat, 0);
+ if (lastHour + sat > VELOCITY.maxSat) {
+ log(`ALERT forward velocity: ${key.slice(0, 12)} (${name}) got ${lastHour} sat in the last hour; ${sat} more waits for a person`);
+ throw new Error(`velocity hold: ${lastHour} sat delivered to this key in the last hour`);
}
- } finally { if (hash) delivering.delete(hash); }
+ await forwardOnce(name, sat, hash, pubkey);
+ const all = state.delivered ||= {};
+ all[hash] = Date.now();
+ const cutoff = Date.now() - 60 * 86400_000;
+ for (const [h, t] of Object.entries(all)) if (t < cutoff) delete all[h];
+ const mine = (state.deliveredTo ||= {})[key] ||= {};
+ mine[hash] = { sat, at: Date.now() };
+ for (const [h, x] of Object.entries(mine)) if (x.at < Date.now() - VELOCITY.windowMs) delete mine[h];
+ persist();
+ } finally { delivering.delete(hash); }
}
async function forwardOnce(name, sat, hash, pubkey) {