aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-18 01:11:17 -0700
committerAdam Soltys <[email protected]>2026-08-18 01:11:17 -0700
commit1a51f71a84ff661dd0ec271ef3bc0194545b27c4 (patch)
tree5ee70b9648e96a5ea1e0b3c29426de117ad144fa
parent4069f813820f1814bef5b93c679619bc7ed8ddba (diff)
downloadcoinosv3-1a51f71a84ff661dd0ec271ef3bc0194545b27c4.tar.gz
coinosv3-1a51f71a84ff661dd0ec271ef3bc0194545b27c4.zip
The HTLC expiry stops trusting a cached clock: tip fetches bypass the CDN, and when the server names its own tip the payment adopts it and retries instead of dying
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
-rw-r--r--src/ark/manager.js16
1 files changed, 15 insertions, 1 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js
index 3a73e0b..eb23b51 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -190,7 +190,9 @@ export class ArkManager {
get chain() {
const base = this.esploraUrl;
return {
- tipHeight: async () => Number(await fetch(`${base}/blocks/tip/height`).then((r) => r.text())),
+ // no-store: a CDN-cached tip lags a 30s-block network by many blocks,
+ // and a stale tip under-shoots HTLC expiries the server then rejects
+ tipHeight: async () => Number(await fetch(`${base}/blocks/tip/height`, { cache: 'no-store' }).then((r) => r.text())),
getTxStatus: async (txid) => {
const r = await fetch(`${base}/tx/${txid}/status`);
if (r.ok) return r.json();
@@ -835,6 +837,18 @@ export class ArkManager {
resps = await requestLightningPayHtlcCosign(this.arkUrl,
builds.map((b, i) => cosignPartBytes({ build: b, input: decoded[i], vtxoKeys: keysList[i], nonces: noncesList[i] })));
} catch (e) {
+ // A stale local tip under-shoots the HTLC expiry, and the server's
+ // error helpfully names ITS tip — adopt it and let the next drive
+ // retry with a valid expiry instead of failing the whole payment.
+ const tipM = e instanceof GrpcError && e.message.match(/expiry is too low.*?our tip is (\d+)/i);
+ if (tipM) {
+ const serverTip = parseInt(tipM[1], 10);
+ action.htlcExpiry = serverTip + (this.info.htlcSendExpiryDelta || 258);
+ this._tipH = Math.max(this._tipH || 0, serverTip);
+ this._tipAt = Date.now();
+ this._save();
+ throw new Error('chain tip was stale — retrying with the corrected expiry');
+ }
// INVALID_ARGUMENT (status 3): the server rejected the package outright
// — nothing was spent, everything returns to spendable. "already
// spent" names ONE stale input but not which: free them all and let