Subject: Re: [bitcoin-dev] Merkleize All The Things

From: Johan Torås Halseth <[email-masked]>
Date: Fri, 26 May 2023 13:45:17 +0200

Hi, Salvatore.

As a further exploration of this idea, I implemented a
proof-of-concept of OP_CICV and OP_COCV in btcd[1] that together with
OP_CAT enables a set of interesting use cases.

One such use case is, as mentioned earlier, CoinPools[2]. The opcodes
let you easily check the "dynamically committed data" of an input you
are spending, and enforce a new commitment on the output. The idea is
to have the set of participants in the pool, and their balances, be
the UTXOs committed data, and  use this to validate the legitimacy of
a transaction, determining whether it permits a peer to exit with a
portion of the pooled funds.

Doing what you suggested above, having the input and output commit to
a merkle tree of participants and balances, we are able to quite
elegantly verify the coin pool exit clause. Here is a working example
of how that could look like: [3]. Obviously this lacks a lot before it
is a working CoinPool implementation, but it demonstrates how
OP_C[I/O]V introduces "memory" to Bitcoin script.

Having done this exercise, I have a few suggestions on how one could
further extend the proposal:

1. In the current proposal for OP_CHECKOUTPUTCONTRACTVERIFY, the
opcodes check whether the output key Q is key X tweaked with data D
and taproot T: Q == tweak(tweak(X,D), T).

OP_CHECKINPUTCONTRACTVERIFY on the other hand, works on the input
internal key, and does not care about the taptree on the input: P ==
tweak(X,D), where Q = tweak(P, T). In most cases this is probably good
enough, since you are already executing the current script and that
way know the spender has provided the correct taproot.

However, in the coin pool script mentioned above, I found that I
wanted to re-use the same taproot for the output (recursively). I
believe this would be a quite common use case. To solve this I
committed the taproot as part of the data itself: D' = hash(T+D),
which was then verified by OP_CICV. If you are aware of more efficient
alternatives, I am eager to hear them.

A simpler way IMO, would be to make OP_CICV and OP_COCV symmetrical:
Have OP_CICV take an optional taproot and do the same check as is done
for the output: Q == tweak(tweak(X,D), T).

2.To make fully functioning CoinPools, one would need functionality
similar to OP_MERKLESUB[4]: remove some data from the merkle tree, and
remove a key from the aggregated internal key.This suggestion may
surpass the intended scope of this proposal, and would likely
necessitate the availability of multiple EC operations to accommodate
various key schemes. If we had opcodes for adding and removing keys
from the internal key this would be even more powerful.

I look forward to hearing your thoughts on these suggestions and
further exploring the possibilities of the proposal!

Cheers,
Johan

[1] https://github.com/halseth/btcd/pull/1/commits/90a4065bdcd8029fe3325514a250490cba66fddd
[2] https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2020-June/017964.html
[3] https://github.com/halseth/tapsim/tree/matt-demo/examples/matt/coinpool
[4] https://github.com/ariard/bips/blob/coinpool-bips/bip-merklesub.mediawiki

On Fri, May 5, 2023 at 11:18 PM Salvatore Ingala
<[email-masked]> wrote:
>
> On Thu, 4 May 2023 at 10:34, Johan Torås Halseth <[email-masked]> wrote:
> >
> > It sounds like we can generalize the description of the construct to:
> > Access to (the hash of) embedded data of inputs and outputs, and the
> > enforcement of output keys and (static) taptrees. In other words, as
> > long as you can dynamically compute the output embedded data in
> > Script, you can enforce more or less anything (since you can make the
> > output script enforce presenting a witness "satisfying" the embedded
> > data).
> >
> > Does that sound about right?
>
> Yes. Fraud proofs allow us to extend beyond what Script can do (with the
> necessary tradeoffs), but there is plenty that can be done without them.
>
>
> > For instance, I believe you could simulate coin pools pretty easily:
> > Commit to the set of pubkeys and amounts owned by the participants in
> > the pool, and an output taptree where each participant has their own
> > spending path. Now, to exit the pool unilaterally, the participant
> > must present a proof that their pubkey+amount is committed to in the
> > input and an output where it is no longer committed.
>
> I don't think one would want to have a tapleaf for each participant:
> that would make you pay log n hashes just to reveal the tapleaf, and
> then you still need to pay log n hashes to access the embedded data.
>
> Instead, the "unilateral withdrawal Script" can be the same for all the
> participants. The witness would be the Merkle proof, plus perhaps some
> additional information to identify the leaf in the tree (depending on
> how the Merkle tree is implemented). In a complete Merkle tree for
> N = 2^n participants, the witness could contain the n hashes that allow
> to prove the value of the leaf, plus n bits to identify the path to the
> leaf (0/1 for 'left/right" child), since Script doesn't have enough
> opcodes to extract the bits from the leaf index.
>
> The data in the leaf can contain a commitment to all the information
> relevant for that participant (e.g.: their balance and pubkey, in a
> CoinPool construction).
>
> Then, the same witness can easily be reused to compute the new Merkle
> root after the data in the leaf is modified (for example, setting the
> amount to 0 for one participant).
>
>
> > A question that arises is how one would efficiently (in Script) prove
> > the inclusion/exclusion of the data in the commitment. One could
> > naively hash all the data twice during script execution (once for the
> > input, once for the output), but that is costly. It would be natural
> > to show merkle tree inclusion/exclusion in script, but perhaps there
> > are more efficient ways to prove it?
>
> A Merkle tree as described above commits to an entire vector that you
> can index positionally. That's quite versatile, and easier to handle
> than more complex constructions like accumulators with exclusion proofs.
>
> A Merkle proof for 2^7 = 128 participants requires about 8 hashes, so
> around 250 bytes in total of witness size; 2^10 = 1024 should bring that
> to the ballpark of 350 bytes.
>
> Best,
> Salvatore Ingala