TL;DR: LNHANCE is a soft fork proposal for bitcoin which combines 4 total
opcodes: OP_CHECKTEMPLATEVERIFY, OP_CHECKSIGFROMSTACK (CSFS),
OP_CHECKSIGFROMSTACKVERIFY, and OP_INTERNALKEY. It repurposes 2 NOPs in
all script types and 2 SUCCESSes in Tapscript only. This combination of
opcodes enables many enhancements for the Lightning Network, along with
enabling other off chain UTXO sharing protocols and more. Concretely, it
enables LN-Symmetry, Timeout Trees, simplified PTLC scripts, unidirectional
non-interactive channels, (better than now) vaults, trustless coin pools, and
more.
As bitcoin-inquisition already contains an implementation of BIP-119
(OP_CHECKTEMPLATEVERIFY), implementing LNHANCE on Inquisition only requires
adding implementations of BIN-2024-0003 and -0004.
This Pull Request adds a total of 3 opcodes:
OP_INTERNALKEY in Tapscript only, copies the Taproot Internal Key to the
stack.
OP_CHECKSIGFROMSTACK in Tapscript only, validates a BIP340 Schnorr
signature using an item from the stack as the message.
OP_CHECKSIGFROMSTACKVERIFY in all script types, validates a BIP340 Schnorr
signature using an item from the stack as the message, and leaves the stack
unchanged.
The semantics of the signature-checking opcodes are similar to Tapscript
opcodes, including the empty signature->false behavior for
OP_CHECKSIGFROMSTACK.
Because this PR is for bitcoin-inquisition, the
implementation
of OP_INTERNALKEY is remarkably simple. The taproot internal key is already
extracted for use with verifying tapscript OP_CHECKSIG(VERIFY|ADD) with the
key 0x01, so it simply needs to be added to the stack.
OP_CHECKSIGFROMSTACK(VERIFY)
The implementation of OP_CHECKSIGFROMSTACK(VERIFY) is split between the main opcode
processing, and a function analogous to EvalChecksigPreTapscript and EvalChecksigTapscript named EvalChecksigFromStack.
An earlier approach was to add a SignatureChecker that could carry the
message rather than generating the message from the transaction, and give CSFS
the exact semantics of existing sigops in their respective script types, but
that approach had two problems: getting BIP340 signing into legacy/segwitv0
scripts became intrusive, and the semantics of legacy/segwitv0 signature
checking are not particularly desirable for a new opcode. As a result the
current implementation uses a separate function specific to CSFS and gives
modern Tapscript-style semantics. Only after changing code approaches did it
become clear that ECDSA checking might not be desirable in these new sigops.
The commit history still reflects the removal of ECDSA in a separate commit.
Outside of these changes in interpreter.cpp, the only non-flag change in
production code is to allow variable length input to BIP340
signature-checking.
Testing
Tests for both CHECKSIGFROMSTACK and INTERNALKEY have been added to
tx_valid.json
and
tx_invalid.json
for the trasnsaction test framework.
Here
are some scripts using bitcoinjs-lib and noble-curves that were used to
generate the vectors.
You can run a regtest node from this branch to use them yourself.
What does OP_CHECKSIGFROMSTACKVERIFY leave on the stack?
OP_CHECKSIGFROMSTACK?
In what case can OP_CHECKSIGFROMSTACK fail and terminate script
execution?
What will cause OP_CHECKSIGFROMSTACK to succeed without checking the
signature?
What length is the data argument to OP_CHECKSIGFROMSTACK(VERIFY)?
How does OP_INTERNALKEY OP_CHECKSIG in Tapscript compare to key spend
in weight?
How can the Lightning Network be improved using OP_CHECKSIGFROMSTACK
combined with OP_CHECKTEMPLATEVERIFY (BIP-119)?
Questions relating to an open consideration
Why might a user of OP_CHECKSIGFROMSTACK(VERIFY) want to check multiple
stack items?
Is it generally secure to use OP_CAT to combine multiple items for use
with CSFS?
Should OP_CHECKSIGFROMSTACK(VERIFY) be extended to natively support
checking a signature against multiple stack items?
(If so, untested
code
and
BIN
changes are available).
<reardencode> right! I've actually found that in practical scripts this behavior can be useful since CSFS(V) are often used to authorize arguments to other ops, eg. a pubkey for delegation, so leaving items on the stack can be desirable.
<reardencode> as monlovesmango alluded, the case I was thinking of is any non-empty invalid signature terminates execution immediately; only a 0-length signature pushes 0 to the stack and continues execution
<reardencode> As I mentioned in the notes, I was originally going to make CSFS follow the semantics of other sigops in respective script types, but that pretty quickly showed itself to be a poor path both in code clarity, and in terms of making new things in bitcoin better than old things.
<reardencode> in a taproot key spend, the pubkey is taken from the scriptPubKey directly, so currently a tapscript single sig spend is 32 bytes + some overhead more costly. With OP_INTERNALKEY we can get rid of that extra 32 bytes, but we still have the overhead. So, what's the overhead?
<reardencode> so, compared to key spend, a depth0 1-sig script spend w/o INTERNALKEY costs 2 witness item lengths, plus a 33-byte script plus a 33-byte controlblock extra
<reardencode> so, compared to a key spend, the script spend using the inernalkay is 2 witness item lengths, plus a 2-byte script plus a 33-byte control block, so 37WU or 9.25vB more costly.
<reardencode> glozow: https://delvingbitcoin.org/t/ln-symmetry-project-recap/359 - basically the same as with ANYPREVOUT, except instead of <sig> | <apo_pubkey> OP_CHECKSIG, we have <sig> <hash> | OP_CTV <pubkey> OP_CHECKSIGFROMSTACK
<reardencode> the differences between APO and CTV are subtle, CTV commits to n-inputs where APO does not, which only matters sometimes, and APO commits to annex (if present) but CTV does not.
<reardencode> glozow: yeah, my thought process is that CTV alone offers only speculative use-cases, but CTV+IKEY+CSFS offers concrete known use-cases, as well as those speculative use-cases.
<cguida> glozow: CTV+CSFS allows emulating APO, so it gets us most (all?) of the stuff APO gets us, plus all the stuff CTV gets us, while minimizing surface area
<reardencode> to dive in on monlovesmango's idea of committing to multiple things - consider an inheritence use-case - with CSFS on multiple items, you can commit to a locktime, and a pubkey OR template - in this way you don't have to define all possible inheritence scenarios up front, but can delegate to specific keys after specific locktime, or specific templates after other lock times all by
<reardencode> monlovesmango: it helps - the other specific case that has come up is something that instagibbs and cguida hit for LN-Symmetry, the need to commit to both the next update tx CTV hash, and force the reveal of the current update tx's settlement tx's CTV hash. In instagibbs prototype he used the annex with APO for that. CTV does not commit to the annex, but CSFS on multiple items would
<reardencode> in the interest of time, I'll just say: It's not secure, because an attacker could shift 1 byte from the locktime into the delkey on the stack and render the delegate key an always successful key for the CHECKSIG operation while still being valid for the CSFS (the same data is verified after the CAT)
<reardencode> So, final question to think on after this: 3. Should OP_CHECKSIGFROMSTACK(VERIFY) be extended to natively support checking a signature against multiple stack items? (If so, untested code and BIN changes are available).
<reardencode> I think that'd be a great conversation to have - vector-CSFS vs. CAT vs. CTV-annex maybe something to discuss in a follow-up delving post