@atomiqlabs/lp-lib 17.5.3 → 17.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/swaps/assertions/FromBtcAmountAssertions.js +2 -2
- package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +16 -6
- package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.d.ts +2 -2
- package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +34 -24
- package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.d.ts +2 -2
- package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +30 -19
- package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +1 -1
- package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +12 -5
- package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +2 -2
- package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +1 -1
- package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +38 -4
- package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +35 -15
- package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +2 -2
- package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +7 -2
- package/dist/utils/Utils.d.ts +33 -0
- package/dist/utils/Utils.js +51 -1
- package/package.json +1 -1
- package/src/swaps/assertions/FromBtcAmountAssertions.ts +2 -2
- package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +14 -5
- package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +46 -29
- package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +42 -24
- package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +13 -7
- package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +4 -4
- package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +37 -7
- package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +36 -18
- package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +25 -2
- package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +5 -1
- package/src/utils/Utils.ts +48 -0
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
ChainSwapType,
|
|
8
8
|
ClaimEvent,
|
|
9
9
|
InitializeEvent,
|
|
10
|
-
RefundEvent,
|
|
10
|
+
RefundEvent,
|
|
11
|
+
SwapCommitStateType,
|
|
11
12
|
SwapData
|
|
12
13
|
} from "@atomiqlabs/base";
|
|
13
14
|
import {expressHandlerWrapper, getAbortController, HEX_REGEX, isDefinedRuntimeError} from "../../../utils/Utils";
|
|
@@ -27,9 +28,8 @@ import {
|
|
|
27
28
|
ProbeAndRouteResponse,
|
|
28
29
|
routesMatch
|
|
29
30
|
} from "../../../wallets/ILightningWallet";
|
|
30
|
-
import {
|
|
31
|
+
import {LightningAssertions} from "../../assertions/LightningAssertions";
|
|
31
32
|
import {isQuoteThrow} from "../../../plugins/IPlugin";
|
|
32
|
-
import {ToBtcSwapState} from "../tobtc_abstract/ToBtcSwapAbs";
|
|
33
33
|
|
|
34
34
|
export type ToBtcLnConfig = ToBtcBaseConfig & {
|
|
35
35
|
routingFeeMultiplier: bigint | number,
|
|
@@ -197,7 +197,8 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
|
|
|
197
197
|
]);
|
|
198
198
|
|
|
199
199
|
for(let {obj: swap} of queriedData) {
|
|
200
|
-
await this.processPastSwap(swap)
|
|
200
|
+
await this.processPastSwap(swap)
|
|
201
|
+
.catch(e => this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e));
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
|
|
@@ -300,6 +301,10 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
|
|
|
300
301
|
this.swapLogger.info(invoiceData, "subscribeToPayment(): result callback, outcome: "+result.status+" invoice: "+invoiceData.pr);
|
|
301
302
|
this.processPaymentResult(invoiceData, result).catch(e => this.swapLogger.error(invoiceData, "subscribeToPayment(): process payment result", e));
|
|
302
303
|
this.activeSubscriptions.delete(paymentHash);
|
|
304
|
+
}).catch(e => {
|
|
305
|
+
this.swapLogger.error(invoiceData, "subscribeToPayment(): error while waiting for invoice payment: ", e);
|
|
306
|
+
//Delete from active subscription, the watchdog will re-subscribe later
|
|
307
|
+
this.activeSubscriptions.delete(paymentHash);
|
|
303
308
|
});
|
|
304
309
|
this.swapLogger.info(invoiceData, "subscribeToPayment(): subscribe to payment outcome, invoice: "+invoiceData.pr);
|
|
305
310
|
|
|
@@ -418,6 +423,9 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
|
|
|
418
423
|
} else throw e;
|
|
419
424
|
}
|
|
420
425
|
|
|
426
|
+
const unlock = swap.lock(120);
|
|
427
|
+
if(unlock==null) return;
|
|
428
|
+
|
|
421
429
|
await swap.setState(ToBtcLnSwapState.COMMITED);
|
|
422
430
|
await this.saveSwapData(swap);
|
|
423
431
|
try {
|
|
@@ -426,10 +434,22 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
|
|
|
426
434
|
this.swapLogger.error(swap, "processInitialized(): lightning payment error", e);
|
|
427
435
|
if(isDefinedRuntimeError(e)) {
|
|
428
436
|
if(swap.metadata!=null) swap.metadata.payError = e;
|
|
429
|
-
await
|
|
430
|
-
|
|
431
|
-
|
|
437
|
+
const payment = await this.lightning.getPayment(swap.lnPaymentHash);
|
|
438
|
+
if(!unlock()) return;
|
|
439
|
+
if(payment==null || payment.status==="failed") {
|
|
440
|
+
if(swap.state as ToBtcLnSwapState === ToBtcLnSwapState.COMMITED)
|
|
441
|
+
await swap.setState(ToBtcLnSwapState.NON_PAYABLE);
|
|
442
|
+
await this.saveSwapData(swap);
|
|
443
|
+
return;
|
|
444
|
+
} else if(payment.status==="confirmed") {
|
|
445
|
+
await this.processPaymentResult(swap, payment);
|
|
446
|
+
return;
|
|
447
|
+
} else {
|
|
448
|
+
//Fall-through to subscribe
|
|
449
|
+
}
|
|
432
450
|
} else throw e;
|
|
451
|
+
} finally {
|
|
452
|
+
unlock();
|
|
433
453
|
}
|
|
434
454
|
this.subscribeToPayment(swap);
|
|
435
455
|
return;
|
|
@@ -1078,6 +1098,16 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
|
|
|
1078
1098
|
};
|
|
1079
1099
|
|
|
1080
1100
|
if(data.state===ToBtcLnSwapState.NON_PAYABLE) {
|
|
1101
|
+
if(data.payInitiated) {
|
|
1102
|
+
//Check that the ln payment is indeed non-existent or failed
|
|
1103
|
+
const lnPayment = await this.lightning.getPayment(data.lnPaymentHash);
|
|
1104
|
+
if(lnPayment!=null && lnPayment.status!=="failed") throw {
|
|
1105
|
+
_httpStatus: 200,
|
|
1106
|
+
code: 20008,
|
|
1107
|
+
msg: "Payment in-flight"
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1081
1111
|
const refundSigData = await swapContract.getRefundSignature(signer, data.data, this.config.refundAuthorizationTimeout);
|
|
1082
1112
|
|
|
1083
1113
|
//Double check the state after promise result
|
|
@@ -311,7 +311,8 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
311
311
|
]);
|
|
312
312
|
|
|
313
313
|
for(let {obj: swap} of swaps) {
|
|
314
|
-
await this.processPastSwap(swap)
|
|
314
|
+
await this.processPastSwap(swap)
|
|
315
|
+
.catch(e => this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e));
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
318
|
|
|
@@ -794,13 +795,12 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
794
795
|
swap
|
|
795
796
|
);
|
|
796
797
|
if(isQuoteThrow(pluginCheckResult)) {
|
|
797
|
-
|
|
798
|
+
if(swap.state===SpvVaultSwapState.CREATED)
|
|
799
|
+
await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
|
|
800
|
+
throw {
|
|
798
801
|
code: 29999,
|
|
799
802
|
msg: pluginCheckResult.message
|
|
800
|
-
}
|
|
801
|
-
if(swap.metadata!=null) swap.metadata.postQuoteError = error;
|
|
802
|
-
await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
|
|
803
|
-
throw error;
|
|
803
|
+
};
|
|
804
804
|
}
|
|
805
805
|
|
|
806
806
|
await this.Vaults.checkVaultReplacedTransactions(vault, true);
|
|
@@ -811,16 +811,22 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
811
811
|
};
|
|
812
812
|
}
|
|
813
813
|
|
|
814
|
+
const unlock = swap.lock(120);
|
|
815
|
+
if(!unlock) throw {
|
|
816
|
+
code: 20517,
|
|
817
|
+
msg: "Bitcoin transaction submission already in progress, please retry later!"
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
let swapSendingSet = false;
|
|
821
|
+
let dataSendingSet = false;
|
|
814
822
|
try {
|
|
815
823
|
const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
|
|
816
824
|
|
|
817
825
|
//Double-check the state to prevent race condition
|
|
818
|
-
if(swap.state!==SpvVaultSwapState.CREATED) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
};
|
|
823
|
-
}
|
|
826
|
+
if(swap.state!==SpvVaultSwapState.CREATED) throw {
|
|
827
|
+
code: 20505,
|
|
828
|
+
msg: "Invalid quote ID, not found or expired!"
|
|
829
|
+
};
|
|
824
830
|
|
|
825
831
|
//Double check in-flight swap count
|
|
826
832
|
this.checkTooManyInflightSwaps();
|
|
@@ -828,11 +834,13 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
828
834
|
swap.btcTxId = signedTx.id;
|
|
829
835
|
swap.state = SpvVaultSwapState.SIGNED;
|
|
830
836
|
swap.sending = true;
|
|
837
|
+
swapSendingSet = true;
|
|
831
838
|
await this.saveSwapData(swap);
|
|
832
839
|
|
|
833
840
|
data.btcTx.raw = btcRawTx;
|
|
834
841
|
(data as any).sending = true;
|
|
835
842
|
vault.addWithdrawal(data);
|
|
843
|
+
dataSendingSet = true;
|
|
836
844
|
await this.Vaults.saveVault(vault);
|
|
837
845
|
|
|
838
846
|
this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+swap.btcTxId);
|
|
@@ -850,15 +858,25 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
850
858
|
};
|
|
851
859
|
}
|
|
852
860
|
} catch (e) {
|
|
853
|
-
(
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
861
|
+
if(swapSendingSet) swap.sending = false;
|
|
862
|
+
if(dataSendingSet) {
|
|
863
|
+
(data as any).sending = false;
|
|
864
|
+
vault.removeWithdrawal(data);
|
|
865
|
+
await this.Vaults.saveVault(vault);
|
|
866
|
+
}
|
|
857
867
|
|
|
858
|
-
if
|
|
859
|
-
|
|
868
|
+
//Check if the error is only because the state has already changed
|
|
869
|
+
if(!isDefinedRuntimeError(e) || e.code!==20505) {
|
|
870
|
+
//We only make the swap failed if the error happened in CREATED or SIGNED states
|
|
871
|
+
if(swap.state===SpvVaultSwapState.CREATED || swap.state===SpvVaultSwapState.SIGNED) {
|
|
872
|
+
if(isDefinedRuntimeError(e) && swap.metadata!=null) swap.metadata.postQuoteError = e;
|
|
873
|
+
await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
860
876
|
|
|
861
877
|
throw e;
|
|
878
|
+
} finally {
|
|
879
|
+
unlock();
|
|
862
880
|
}
|
|
863
881
|
|
|
864
882
|
await responseStream.writeParamsAndEnd({
|
|
@@ -13,6 +13,29 @@ import {IBitcoinWallet} from "../../../wallets/IBitcoinWallet";
|
|
|
13
13
|
import {FromBtcAmountAssertions} from "../../assertions/FromBtcAmountAssertions";
|
|
14
14
|
import {isQuoteThrow} from "../../../plugins/IPlugin";
|
|
15
15
|
|
|
16
|
+
/*
|
|
17
|
+
TODO: IN CASE THIS EVER GETS USED, FIX THE FOLLOWING:
|
|
18
|
+
|
|
19
|
+
HIGH 5 — FromBtcTrusted (0-conf): no RBF check, fee gate is min not max, and the burn defense only fires when it shouldn't
|
|
20
|
+
|
|
21
|
+
Handler is opt-in (ONCHAIN_TRUSTED, not in shipped configs), but if enabled:
|
|
22
|
+
|
|
23
|
+
• Acceptance (FromBtcTrusted.js:186-194) checks only: direct parents confirmed + fee threshold. Nothing inspects tx.ins[].sequence for BIP-125 (grep: only storage-sequence hits). A double-spendable opt-in-RBF tx is
|
|
24
|
+
accepted and tokens are sent within seconds.
|
|
25
|
+
• The fee gate is fee >= recommendedFee || fee >= currentRate (line 187) — the weaker of the two wins; the documented intent is max(...). The runner even overrides the library's 1.25 multiplier to 1
|
|
26
|
+
(IntermediaryRunner.ts FromBtcTrusted section). A stale low fee maximizes the replacement window and minimizes the attacker's BIP-125 cost.
|
|
27
|
+
• The burn defense (checkDoubleSpends → burn() → sendRawPackage([tx1, burnChild])) is rejected by any node whose mempool already holds the replacement — so it structurally loses against a broadcast double-spend, and
|
|
28
|
+
with mempoolfullrbf (default since Core 28) even non-signaling txs are replaceable.
|
|
29
|
+
• Worse, checkDoubleSpends (lines 636-644) treats getTransaction == null as "double-spent" — but null also means mempool eviction (fee spike, node restart). With no conflict anywhere, the burn package is valid and
|
|
30
|
+
burns the entire deposit to miners — after tokens were already sent, that's a full LP loss with no attacker at all.
|
|
31
|
+
|
|
32
|
+
MEDIUM 6 — FromBtcTrusted /setRefundAddress is dead code; refundable user funds strand permanently
|
|
33
|
+
|
|
34
|
+
FromBtcTrusted.js:606 looks up getData(paymentHash, null); IntermediaryStorageManager.getData (line 64-66) converts null → sequence 0x0, but every swap is stored with a random 64-bit sequence — so the lookup always
|
|
35
|
+
misses and the endpoint always throws 10001. (The status endpoint at line 515 does it correctly.) Any swap that becomes REFUNDABLE (over-max payment, plugin veto, chain revert, balance failure) without a refund
|
|
36
|
+
address set at creation can never be refunded (refundSwap early-returns without one). User funds sit in the LP wallet forever. One-line fix: pass parsedBody.sequence.
|
|
37
|
+
*/
|
|
38
|
+
|
|
16
39
|
export type FromBtcTrustedConfig = SwapBaseConfig & {
|
|
17
40
|
doubleSpendCheckInterval: number,
|
|
18
41
|
swapAddressExpiry: number,
|
|
@@ -298,7 +321,7 @@ export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrust
|
|
|
298
321
|
|
|
299
322
|
const txns = await chainInterface.txsTransfer(signer.getAddress(), swap.token, swap.adjustedOutput, swap.dstAddress);
|
|
300
323
|
|
|
301
|
-
let unlock = swap.lock(30
|
|
324
|
+
let unlock = swap.lock(30);
|
|
302
325
|
if(unlock==null) return;
|
|
303
326
|
|
|
304
327
|
const pluginCheckResult = await PluginManager.onHandlePreFromBtcExecute(
|
|
@@ -399,7 +422,7 @@ export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrust
|
|
|
399
422
|
try {
|
|
400
423
|
await this.processPastSwap(swap, txs[0]?.tx, txs[0]?.vout);
|
|
401
424
|
} catch (e) {
|
|
402
|
-
this.swapLogger.error(swap, "
|
|
425
|
+
this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e);
|
|
403
426
|
}
|
|
404
427
|
}
|
|
405
428
|
}
|
|
@@ -171,7 +171,11 @@ export class FromBtcLnTrusted extends SwapHandler<FromBtcLnTrustedSwap, FromBtcL
|
|
|
171
171
|
]);
|
|
172
172
|
|
|
173
173
|
for(let {obj: swap} of queriedData) {
|
|
174
|
-
|
|
174
|
+
try {
|
|
175
|
+
if(await this.processPastSwap(swap)) cancelInvoices.push(swap);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e);
|
|
178
|
+
}
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
await this.cancelInvoices(cancelInvoices);
|
package/src/utils/Utils.ts
CHANGED
|
@@ -150,3 +150,51 @@ export function parsePsbt(btcTx: Transaction): BtcTx {
|
|
|
150
150
|
})
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns the minimum Bitcoin block window (in blocks) for which a "fast blocks" safety
|
|
156
|
+
* factor is genuinely usable: the probability that N consecutive blocks are produced
|
|
157
|
+
* with an average interval below (blocktime / safetyFactor) is less than `probability`.
|
|
158
|
+
*
|
|
159
|
+
* Model: block arrivals are a Poisson process (i.i.d. exponential intervals), so the
|
|
160
|
+
* production time of N blocks is Erlang/Gamma(shape=N). A Chernoff (Cramer) bound on
|
|
161
|
+
* the lower tail gives the per-block large-deviation rate I(S) = ln(S) + 1/S - 1, i.e.
|
|
162
|
+
* P(N blocks faster than blocktime/S on average) <= exp(-N * I(S))
|
|
163
|
+
* which this function inverts for N. Conservative: the exact Erlang CDF already
|
|
164
|
+
* satisfies the bound ~25% earlier. Use for the fast-block tail (e.g. incoming LN
|
|
165
|
+
* HTLC expiry racing an escrow claim window).
|
|
166
|
+
*
|
|
167
|
+
* @param {number|bigint} safetyFactor Assumed max block-speed multiplier (must be > 1)
|
|
168
|
+
* @param {number} [probability=0.0001] Tolerated failure probability (default 0.01%)
|
|
169
|
+
* @returns {number} Minimum safe block window in blocks
|
|
170
|
+
*/
|
|
171
|
+
export function getMinSafeBlockWindowFast(safetyFactor: number | bigint, probability: number = 0.0001): bigint {
|
|
172
|
+
const S = Number(safetyFactor);
|
|
173
|
+
const rate = Math.log(S) + 1/S - 1;
|
|
174
|
+
if (!(rate > 0)) throw new RangeError("safetyFactor must be > 1");
|
|
175
|
+
return BigInt(Math.ceil(Math.log(1/probability) / rate));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Slow-tail mirror of getMinSafeBlockWindowFast(): returns the minimum block window
|
|
180
|
+
* (in blocks) for which a "slow blocks" safety factor is genuinely usable: the
|
|
181
|
+
* probability that N consecutive blocks take LONGER than N * blocktime * safetyFactor
|
|
182
|
+
* is less than `probability`. Chernoff bound on the upper Erlang tail gives the rate
|
|
183
|
+
* I(S) = S - 1 - ln(S). Use when converting a wall-clock deadline into a block count
|
|
184
|
+
* (e.g. payout/claim timeouts that must expire before some real-world time even if
|
|
185
|
+
* blocks come slowly).
|
|
186
|
+
*
|
|
187
|
+
* @param {number|bigint} safetyFactor Assumed max block-slowness multiplier (must be > 1)
|
|
188
|
+
* @param {number} [probability=0.0001] Tolerated failure probability (default 0.01%)
|
|
189
|
+
* @returns {number} Minimum safe block window in blocks
|
|
190
|
+
*/
|
|
191
|
+
export function getMinSafeBlockWindowSlow(safetyFactor: bigint | number, probability: number = 0.0001): bigint {
|
|
192
|
+
const S = Number(safetyFactor);
|
|
193
|
+
const rate = S - 1 - Math.log(S);
|
|
194
|
+
if (!(rate > 0)) throw new RangeError("safetyFactor must be > 1");
|
|
195
|
+
return BigInt(Math.ceil(Math.log(1/probability) / rate));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function bigIntMax(a: bigint, b: bigint): bigint {
|
|
199
|
+
return a > b ? a : b;
|
|
200
|
+
}
|