TL;DR
arkd's tapscript closure surface partitions the 5 closure types into two buckets in pkg/ark-lib/script/vtxo_script.go:
ForfeitClosures() ⊇ { MultisigClosure, CLTVMultisigClosure, ConditionMultisigClosure } // execution / collaborative
ExitClosures() ⊇ { CSVMultisigClosure, ConditionCSVMultisigClosure } // unilateral exit to L1
There is no condition-bearing closure on the execution side. Any execution path that needs a condition + absolute timelock has no protocol-supported leaf — the closest substitute (ConditionCSVMultisigClosure) is an exit-side leaf, meaning the user must unroll to L1 and wait the relative timelock from L1 confirmation rather than claim cooperatively offchain.
The convention, made explicit by arkd's own taxonomy
| Path type |
Without condition |
With condition |
Execution / collaborative (off-chain, arkd co-signs via /v1/tx/submit; absolute deadlines via CLTV) |
CLTVMultisigClosure (e.g. refund: funder + arkd, absolute finalExpiration) |
— GAP — |
| Unilateral exit (push VTXO chain to L1, then spend on-chain after the relative CSV timelock matures) |
CSVMultisigClosure (e.g. server-unroll, exit closures) |
ConditionCSVMultisigClosure |
The collaborative-spend validator at internal/infrastructure/tx-builder/covenantless/builder.go:96-138 confirms this by construction: its switch over input closure types accepts Multisig / CSV / CLTV / ConditionMultisig but has no case for ConditionCSVMultisig — exit closures aren't validated on the collaborative offchain path; they're spent on L1 after unroll.
The gap and its consequence
The matrix above leaves one cell empty: a condition-bearing execution leaf. Any protocol that wants "cooperatively spend offchain, gated on a hash-preimage reveal and an absolute deadline" currently has no first-class closure to express that — only the exit-side substitute, which silently changes the user-facing semantics from "wait T seconds then call /penalty to claim instantly" into "publish your VTXO chain to L1, wait for confirmations + the CSV relative timelock, then claim on-chain with full L1 fees."
For protocols where the leaf is meant as a deterrent (forfeit / penalty leaves in fair-exchange protocols), this is functionally important — the deterrent strength depends on the claim being cheap and fast. Forcing a unilateral exit weakens the deterrent for small stakes.
Use case — forfeit / penalty leaves in fair-exchange protocols
Concrete pattern surfaced while implementing an R1-style forfeit leaf on a trustless coin-flip escrow:
- Two parties co-stake into per-party escrows.
- One reveals its secret to the other (via the server).
- If the other party withholds its secret, the revealer should be able to sweep the whole pot after an absolute deadline — cooperatively, via
/tx/submit, with arkd co-signing — using only its own revealed secret.
That decomposes naturally to:
- hash-preimage check ("I revealed") → condition
- absolute deadline → CLTV (execution path: arkd co-signs, parties are presumed online)
- 2-of-2 multisig (revealer + arkd) → outer multisig
The protocol cleanly supports each individual component but not the combination. ConditionMultisigClosure won't admit OP_CHECKLOCKTIMEVERIFY inside its conditionScript (correctly — forbiddenOpcodes enforces stateless conditions). ConditionCSVMultisigClosure is exit-bucket. CLTVMultisigClosure lacks a condition slot. No combination works for cooperative-claim-after-absolute-deadline.
Proposed
Add ConditionCLTVMultisigClosure to the closure set, mirroring ConditionCSVMultisigClosure structurally, and add it to ForfeitClosures() (execution-bucket) rather than ExitClosures():
type ConditionCLTVMultisigClosure struct {
CLTVMultisigClosure
Condition []byte
}
Encoded script form:
<conditionScript> VERIFY <locktime> CHECKLOCKTIMEVERIFY DROP <pubkey> CHECKSIGVERIFY ... CHECKSIG
Implementation analogous to existing ConditionCSVMultisigClosure: same condition-tokenization split (break at first OP_VERIFY), same forbiddenOpcodes enforcement on the condition, but the outer-closure timelock uses absolute CLTV. The collaborative-spend validator at builder.go would gain a case for this closure mirroring ConditionMultisigClosure's logic (evaluate condition + register pubkeys), with the CLTV propagated to the spending tx's nLockTime (as CLTVMultisigClosure already does at offchain/tx.go:121-150).
SDK-side counterpart in @arkade-os/sdk's script/tapscript.ts would add ConditionCLTVMultisigTapscript to the decodeTapscript enumeration so client builders recognize it.
This completes the 2×2 grid (exec/exit × bare/conditioned) and makes the CSV-for-exit / CLTV-for-execution distinction expressible for all leaf shapes — including condition-bearing execution leaves which are currently unrepresentable.
Alternative considered
Relaxing forbiddenOpcodes to allow OP_CHECKLOCKTIMEVERIFY inside the conditionScript (and threading a real tx context into EvaluateScriptToBool instead of the fake stub) would technically work, but mixes two distinct concerns — stateless boolean predicate vs. tx-introspecting timelock — at the condition layer, which I read the current ban as deliberately codifying. A dedicated closure type preserves the factoring and matches the existing ConditionCSVMultisig precedent.
Happy to PR
If the maintainers think this fits, I'm happy to send a PR with the closure addition + round-trip tests matching the existing ConditionCSVMultisig patterns + the SDK counterpart.
TL;DR
arkd's tapscript closure surface partitions the 5 closure types into two buckets in
pkg/ark-lib/script/vtxo_script.go:There is no condition-bearing closure on the execution side. Any execution path that needs a condition + absolute timelock has no protocol-supported leaf — the closest substitute (
ConditionCSVMultisigClosure) is an exit-side leaf, meaning the user must unroll to L1 and wait the relative timelock from L1 confirmation rather than claim cooperatively offchain.The convention, made explicit by arkd's own taxonomy
/v1/tx/submit; absolute deadlines via CLTV)CLTVMultisigClosure(e.g. refund: funder + arkd, absolutefinalExpiration)CSVMultisigClosure(e.g. server-unroll, exit closures)ConditionCSVMultisigClosureThe collaborative-spend validator at
internal/infrastructure/tx-builder/covenantless/builder.go:96-138confirms this by construction: its switch over input closure types acceptsMultisig/CSV/CLTV/ConditionMultisigbut has no case forConditionCSVMultisig— exit closures aren't validated on the collaborative offchain path; they're spent on L1 after unroll.The gap and its consequence
The matrix above leaves one cell empty: a condition-bearing execution leaf. Any protocol that wants "cooperatively spend offchain, gated on a hash-preimage reveal and an absolute deadline" currently has no first-class closure to express that — only the exit-side substitute, which silently changes the user-facing semantics from "wait
Tseconds then call/penaltyto claim instantly" into "publish your VTXO chain to L1, wait for confirmations + the CSV relative timelock, then claim on-chain with full L1 fees."For protocols where the leaf is meant as a deterrent (forfeit / penalty leaves in fair-exchange protocols), this is functionally important — the deterrent strength depends on the claim being cheap and fast. Forcing a unilateral exit weakens the deterrent for small stakes.
Use case — forfeit / penalty leaves in fair-exchange protocols
Concrete pattern surfaced while implementing an R1-style forfeit leaf on a trustless coin-flip escrow:
/tx/submit, with arkd co-signing — using only its own revealed secret.That decomposes naturally to:
The protocol cleanly supports each individual component but not the combination.
ConditionMultisigClosurewon't admitOP_CHECKLOCKTIMEVERIFYinside its conditionScript (correctly —forbiddenOpcodesenforces stateless conditions).ConditionCSVMultisigClosureis exit-bucket.CLTVMultisigClosurelacks a condition slot. No combination works for cooperative-claim-after-absolute-deadline.Proposed
Add
ConditionCLTVMultisigClosureto the closure set, mirroringConditionCSVMultisigClosurestructurally, and add it toForfeitClosures()(execution-bucket) rather thanExitClosures():Encoded script form:
Implementation analogous to existing
ConditionCSVMultisigClosure: same condition-tokenization split (break at firstOP_VERIFY), sameforbiddenOpcodesenforcement on the condition, but the outer-closure timelock uses absolute CLTV. The collaborative-spend validator atbuilder.gowould gain a case for this closure mirroringConditionMultisigClosure's logic (evaluate condition + register pubkeys), with the CLTV propagated to the spending tx'snLockTime(asCLTVMultisigClosurealready does atoffchain/tx.go:121-150).SDK-side counterpart in
@arkade-os/sdk'sscript/tapscript.tswould addConditionCLTVMultisigTapscriptto thedecodeTapscriptenumeration so client builders recognize it.This completes the 2×2 grid (exec/exit × bare/conditioned) and makes the CSV-for-exit / CLTV-for-execution distinction expressible for all leaf shapes — including condition-bearing execution leaves which are currently unrepresentable.
Alternative considered
Relaxing
forbiddenOpcodesto allowOP_CHECKLOCKTIMEVERIFYinside the conditionScript (and threading a real tx context intoEvaluateScriptToBoolinstead of the fake stub) would technically work, but mixes two distinct concerns — stateless boolean predicate vs. tx-introspecting timelock — at the condition layer, which I read the current ban as deliberately codifying. A dedicated closure type preserves the factoring and matches the existingConditionCSVMultisigprecedent.Happy to PR
If the maintainers think this fits, I'm happy to send a PR with the closure addition + round-trip tests matching the existing
ConditionCSVMultisigpatterns + the SDK counterpart.