Subject: Re: [bitcoindev] Relax OP_RETURN standardness restrictions

From: Anthony Towns <[email-masked]>
Date: Mon, 12 May 2025 23:47:45 +1000

On Thu, Apr 17, 2025 at 06:52:34PM +0000, 'Antoine Poinsot' via Bitcoin Development Mailing List wrote:
> Bitcoin Core will by default only relay and mine transactions with at most a single OP_RETURN output, with a scriptPubKey no larger than 83 bytes. This standardness rule falls into the third category: it aims to mildly deter data storage while still allowing a less harmful alternative than using non-provably-unspendable outputs.
> 
> Developers are now designing constructions that work around these limitations. An example is Clementine, the recently-announced Citrea bridge, which uses unspendable Taproot outputs to store data in its "WatchtowerChallenge" transaction due to the standardness restrictions on the size of OP_RETURNs[^0].

The reason for limiting OP_RETURNs to 80 bytes of data is to encourage
developers to store hashes on the chain, rather than the actual
data. Why store 1GB when you can commit to it with a 32B hash, and save
99.9999968% in fees? In all this debate I haven't seen an analysis of
other alternatives Citrea/Clementine might use, rather than storing 144
bytes of data on chain.

As I understand it, the context in which they're using the data is the
following protocol:

 * we have three known groups: Operators, Watchtowers and Signers
 * we also have: Users and Challengers (who can be anyone)
 * we assume that there is at least 1 honest Operator, 1 honest Watchtower,
   1 honest Signer, and 1 honest Challenger, and >50% honest hashrate

When things go wrong, and one of the Operators tries to cheat, one way
they can do so is by publishing a claim that they posted a transaction
in a Bitcoin block that's not actually in the Bitcoin block chain. At
that point, an honest Watchtower observers the claim, and produces a
Groth16 proof that he has a more-work chain that does not contain the
block claimed by the Operator.

 * but what if the Operator was honest and the Watchtower was trying to cheat?

 * in that case, the claimed Groth16 proof can be evaluated via a sequence of
   transactions following the BitVM and will be found to be invalid

The Groth16 proof/evaluator here is acting as a light client (doing
header only evaluation), but with even less data than a traditional
light client -- it only needs the Groth16 proof, not the ~40MB of data
from all the block headers. While 40MB of data isn't much for a phone
or raspberry pi or similar, it is a lot if you need to manually provide
it to a BitVM program.

Because the Watchtower here may be dishonest, they can't just be expected
to publish a hash of their proof -- nobody else can generate the exact
same proof they would without their random inputs, and publishing their
random inputs as well as a commitment would also be more than 80 bytes.

Possibly a protocol like the following could work, however:

  * Operator claims block X with work W had the relevant tx
  * Watchtower observes block Y with work W+A does not include X in its history,
    waits until Y has 6 confirmations, and publishes Y's block hash and W+A
  * Challenger:
     - observes block Y, generates their own Groth16 proof that X is
       not in Y's ancestor set, and verifies this via BitVM, penalising
       the operator
     - observes block Z with work W+2*A with X in its ancestry but not
       Y, generates their own Groth16 proof for that, verifies this via
       BitVM, penalising the watchtower
     - observes block Z with work W+2*A with neither X nor Y in its
       ancestry, generates their own Groth16 proof for that, verifies
       this via BitVM, penalising the watchtower

This process is only triggered if either the Operator or Watchtower is
dishonest, and in that case it's likely the full BitVM protocol will
also be triggered, so the potential for a ~100 byte saving is probably
just lost in the noise.

However the point of all this is just to find a way of ensuring that the Operator
publishes a block hash that's actually in the block chain. But knowing what block
hashes are in the block chain is something bitcoind is already pretty good at,
so we could do that pretty differently. eg, Consider the hex string

  50 01 24 030c3500 00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054

A string like that could be interpreted as:

  * this is an annex entry
  * of type 1, "per-input height lock"
  * data length 36 bytes
  * 3-byte int, value 800,000
  * 32-byte hash, the block hash of block 800,000

And a consensus rule could be added that the presence of a type 1 annex
entry means a transaction is invalid unless the given block height
has been reached, and the hash of the block at that height (ends with)
the provided bytes.

That would ensure that the Operator's transaction does provide a real block
hash, which I believe is all that the Groth16 proof in question achieves.

I think that's probably sufficient for this use case -- you'd pull the
Operator's tx from the blockchain, then provide it to a BitVM program,
which would verify the witness was signed by the Operator, that the annex
data is of the appropriate length, and then go on to use the block hash
for further computation, presumably establishing some transaction is
included in the block by evaluating a merkle path. So that would remove
both the need to include the 144B groth16 proof, but would also avoid
an entire BitVM evaluation.

Having an annex entry acting as a per-input height lock seems somewhat
useful in general, and might, eg, allow multiple lightning payments to be
timed out in a single transaction, which might be nice. Being able to
commit to the block hash of your height lock is potentially useful when
dealing with controversial soft forks, hard forks, chain splits, or large
reorgs; as you can use the commitment to make a spend only valid on one
chain or the other yourself, rather than needing to try a double spend
race against yourself or collaborate with a miner to mix a new coinbase
output with your coins.

I think doing that via structuring the annex should be pretty feasible,
as a small number of unconditional contextual assertions from a well known
location in the traction should be easy to extract and operate on, in
the same way we treat tx's nVersion, nSequence and nLockTime time today.
In particular that sort of assertion can be validated independently of
executing scripts, which should make working out if a tx can remain in
the mempool after a reorg more straightforward.

Embedding it via an opcode in script seems somewhat more dangerous in two
ways -- you don't know which block hashes a script might reference until
you run the script (so have to give the script interpreter access to all
the blocks), and to cater for reorgs will need to track the most recent
block hash a script references and store that in the tx's mempool entry.
Those aren't impossible to deal with, but it seems better to me to have
keep the information that scripts can use as computation inputs very
local to the transaction being processed.

References:
 - https://citrea.xyz/clementine_whitepaper.pdf
 - https://x.com/0x_orkun/status/1918009290326950147
 - https://gnusha.org/pi/bitcoindev/[email-masked]/

Cheers,
aj

-- 
You received this message because you are subscribed to the Google Groups "Bitcoin Development Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email-masked].
To view this discussion visit https://groups.google.com/d/msgid/bitcoindev/aCH8AdZeBAc0UVfT%40erisian.com.au.