Target-rotated round, the consensus-enforced covenant scripts. MEDIUM by CVSS, but it changes the deadline math every other timelock issue depends on. On main. Verified against the covenant source and the Radiant Core interpreter.
The property
GravityHtlcCovenantFt.rxd:54-57 (and the NFT/RXD siblings):
refund() {
require(tx.age >= refundCsv);
require(hash256(tx.outputs[0].lockingBytecode) == expectedMakerFtHash);
}
No signature check. After maturity the refund branch is spendable by anyone who supplies a fee input — the covenant only constrains where the value goes, not who moves it.
That is a deliberate and reasonable design (it makes the refund recoverable without the maker's key). But it has a consequence the surrounding code does not account for.
The attack, and it is simply the rational play
The maker already holds p and has already taken the counter leg. At the instant CSV matures, the maker broadcasts a minimum-fee refund. Every node that sees it first rejects the taker's later claim — even a much higher-fee one — as txn-mempool-conflict.
Radiant has no RBF. There is nothing to outbid with. The taker cannot pay its way past a conflicting transaction that arrived first.
So the taker's real deadline is not CSV maturity. It is:
maturity − propagation − one confirmation
and nothing in the code enforces that margin. blocks_to_deadline plumbing already exists in htlc_spend.py:211-237 and is not used for this.
Why this matters beyond itself
Three other issues filed this round concern t_rxd being unvalidated — #507 (nothing couples it to the value-scaled burial), #508 (the sizer has no caller and the script gate is a tautology), #484 (the coordinator gate measures punctuality, not slow-chain risk). All of them reason about the deadline as maturity. If the effective deadline is earlier by propagation plus a confirmation — and on a chain whose measured inter-block time ranges 9s to 330s, one confirmation is a wide and highly variable quantity — then every one of those margins is computed against the wrong target.
This cannot be fixed in script: there is no opcode that bounds age from above.
Fix
Enforce a hard claim-by height in the leg, at maturity − propagation_margin − confirmation_margin, and refuse to build a claim past it (the same shape as the ETH leg's CLAIM_INCLUSION_BUDGET_S, which exists for the identical reason on the other chain). Size the confirmation margin from a slow-tail inter-block measurement, not the mean — and pair the refusal with an honest-path test, because a margin sized carelessly here refuses valid claims, which on this chain forfeits the asset.
Target-rotated round, the consensus-enforced covenant scripts. MEDIUM by CVSS, but it changes the deadline math every other timelock issue depends on. On
main. Verified against the covenant source and the Radiant Core interpreter.The property
GravityHtlcCovenantFt.rxd:54-57(and the NFT/RXD siblings):No signature check. After maturity the refund branch is spendable by anyone who supplies a fee input — the covenant only constrains where the value goes, not who moves it.
That is a deliberate and reasonable design (it makes the refund recoverable without the maker's key). But it has a consequence the surrounding code does not account for.
The attack, and it is simply the rational play
The maker already holds
pand has already taken the counter leg. At the instant CSV matures, the maker broadcasts a minimum-fee refund. Every node that sees it first rejects the taker's later claim — even a much higher-fee one — astxn-mempool-conflict.Radiant has no RBF. There is nothing to outbid with. The taker cannot pay its way past a conflicting transaction that arrived first.
So the taker's real deadline is not CSV maturity. It is:
and nothing in the code enforces that margin.
blocks_to_deadlineplumbing already exists inhtlc_spend.py:211-237and is not used for this.Why this matters beyond itself
Three other issues filed this round concern
t_rxdbeing unvalidated — #507 (nothing couples it to the value-scaled burial), #508 (the sizer has no caller and the script gate is a tautology), #484 (the coordinator gate measures punctuality, not slow-chain risk). All of them reason about the deadline as maturity. If the effective deadline is earlier by propagation plus a confirmation — and on a chain whose measured inter-block time ranges 9s to 330s, one confirmation is a wide and highly variable quantity — then every one of those margins is computed against the wrong target.This cannot be fixed in script: there is no opcode that bounds age from above.
Fix
Enforce a hard claim-by height in the leg, at
maturity − propagation_margin − confirmation_margin, and refuse to build a claim past it (the same shape as the ETH leg'sCLAIM_INCLUSION_BUDGET_S, which exists for the identical reason on the other chain). Size the confirmation margin from a slow-tail inter-block measurement, not the mean — and pair the refusal with an honest-path test, because a margin sized carelessly here refuses valid claims, which on this chain forfeits the asset.