The value of MAST in p2sh-like payments

This topic comes up often, taproot and its advantages. But honestly 90% of the advantage is MAST.

Ok, I probably lost most of you here. So lets do a very brief overview.

  • P2SH (pay to script).
    The idea is that a single script is passed into the ‘input’ or unlocking script to be executed. Important detail is that script can’t call it and thus we avoid recursion.
  • Merklized Abstract Syntax Trees (MAST).
    The idea here is that the input provides a merkle-proof (several hashes) as well as a script. Those together can be hashed into the merkle-root which makes up the address the user initially paid to.
  • taproot.
    this is mostly btc specific stuff, it removes limits and adds some opcodes or something. I’m sure others can comment more, but on top of mast there is little of value for BCH here.

P2SH has 2 main advantages, one is the ability to have any type of script and still have a single simple address to pay to. Making QR codes stay short.
The second is one of privacy. The script is not available until the money is being spent which makes attacks massively harder.

The first advantage, having a simple address, is something we can obsolete by moving to better payment protocols. BIP70 already solves this, we will see this continue to be improved I’m sure.

Privacy is still in need to be solved, though.

Notice that p2sh ALSO solves another issue, that the current relay rules are such that only standardized scripts are accepted by the network. This isn’t even a consensus rule and I think it adds no value to the network. We could remove that ‘standardness’ rule if we wanted to.

Privacy

P2SH adds privacy and discourages brute-forcing attacks on payments already mined by hiding the actual script. Which is nice.

MAST is actually specifically designed for this, though. It even goes a step further by allowing unlocking to happen with only a part of the unlocking script. A multisig fallback section of a script will thus never have the actual public keys exposed for most payments. As a quick example.

P2SH sucks!

As a designer, the p2sh design is not giving me happy thoughts. It is hacky in things like how a full node realizes something IS a p2sh payment, it literally needs a outsider detecting it. Which violates the virtual-machine concepts of script…

Can we do better? Do we want to?

A bip114 tried to do mast, it is naturally based on segwit and soft forks, and it was rejected anyway. I think it is pretty easy to do better.

We can have a single opcode; “OP_MAST_VERIFY”.
Tx-Output:

  • push-sha256 (the merkle-root)
  • OP_MAST_VERIFY

Tx-Input

  • any pushes needed by the subscript.
  • push-subscript
  • push-merkle-proof

The opcode takes the 3 items from stack, verifies that the merkle-proof matches the root.
The opcode then executes the script ‘in-place’.

Maybe smart ways stand out to avoid it recusing (it shouldn’t!), but if all else fails that can just become a boolean in the VM. The above way consumes the stack before it is executed so recursing won’t be possible to exploit for looping anyway.

a better p2sh

the usage of p2sh is single digit percentage on chain today, as we grow our capabilities and our research into useful stuff the actual usage of smart contracts will likely go up.
I’m hoping we can provide a better way to do that than p2sh, something that most people will prefer and thus that the amount of p2sh scripts goes down. Partly from an estetics point of view, partly because we may at one point have a rule where transaction-version 10 can’t create p2sh anymore and you need v10 to do fun stuff. You know something that makes sense because p2sh definitey is an ugly hack.

Not a “proposal”, more a request for comments. Who wants to add to this wild idea?

I think you’ll find that many don’t agree with this. I for one love P2SH.

Let’s disentangle the discussion and stick to MAST, I love the idea of MAST, too!
I think everyone will agree that MAST will be an useful addition to BCH capabilities, so let’s focus on how to best get that functionality?

To get MAST, we need 2 things:

  1. a way to execute externally loaded code (external from the PoV of locking script, it can come from unlocking data of the same input, or from another input in the same TX - see “sidecar” contract design pattern)
  2. a way to verify a merkle proof

We could bundle it into a single opcode as you propose, or we could have lower level primitives that allow one to implement the OP_MAST_VERIFY by using other opcodes in VM.

If we want to implement it in VM, there are 3 upgrades that together would let us have MAST in a developer-friendly way:

  1. OP_EVAL or OP_EXEC
  2. Bounded loops
  3. Increased VM limits

PS we actually already have some capability to do MAST, but it’s not really developer-friendly as it requires juggling with “sidecar” UTXOs (dust amounts whose only purpose is to execute some additional code):

  1. We can emulate OP_EVAL by verifying code executed by a “sidecar” input: Emulating OP_EVAL using Bitcoin Cash Native Introspection Opcodes (OP_UTXOBYTECODE) | by bitcoincashautist | Medium
  2. We can run the unrolled algo and verify smaller branches
3 Likes

It looks like our colleagues over at BitcoinUnlimited are considering having a dedicated opcode, even when the VM supports loops & OP_EXEC: Draft: Add OP_MERKLE (!412) · Merge requests · nexa / nexa · GitLab

Quoting Paul Church:

What this new proposed opcode would enable is the ability for smart-contracts to control the write access to distributed databases where the single source of truth is the UTXO set and where all the actual data is stored off-chain. This allows smart-contracts to have arbitrary constraints to give the permission to generate, validate, and update databases of up to ~4 billion data entries (i.e. a 32-bit merkle tree). Smart-contract can then themselves use the data in their operations. For example, our implementation of MAST contract paths is essentially a version of it where the tree is locked upfront and is no ability to update or append to the tree. This new opcode proposal gives smart-contracts essentially limitless permanent storage without bloating the UTXO set.

1 Like

Not saying if this is good or not but I have two questions for clarification.

I think that the “single simple address to pay to.” is still possible with the new transaction type that you defined.
Add a new cashaddress type and encode the 32 byte merkel-root as payload. Any sending wallet would use OP_DATA_32 <merkel root> OP_MAST_VERIFY as the locking script.

Why would you want that requirement? If you can’t have any signatures on the stack the output becomes an anyone-can-spend as soon as a spending input is revealed.

1 Like

if this is wrapped in p2sh32 it would work the same with same security properties, without needing to add yet another address type

Yeah you need to allow data pushes in addition to the merkle path verification. Let’s say that path A reveals a leaf that requires a signature with key A, and path B reveals a leaf that requires a signature with key B.

You still gotta push the actual signature in addition to the leaf.

This would allow for a single address to be spendable from a bunch of different keys, which is pretty cool and would suit QC-resistant schemes which rely on one-time use keys/secrets (one such prompted this thread).

3 Likes

It’s a hack. I think it is universally agreed to be a hack that is bolted on in the most hackish way.

Check how a full node decides to actually execute the push instead of using it as input for an opcode, and you’ll see the point: it is a hack on top of a cleanly designed interpreter.

Hacks provide users something, so I understand you love it. But if we can provide the same (or better) in a non-hackish way, I think that adds value.

It was implemented in a hacky way but the functionality is not a hack. In an alternate history the feature could’ve been implemented as a HF using the same method we used to extend output format for CashTokens, a magic prefix like:

PFX_CONSTRAINT_COMMIT <hash> [locking data]

which would’ve avoided mixing layers and wasting 1 or 2 bytes in the encoding. The input would then have a matching:

PFX_CONSTRAINT_REVEAL <constraint> [unlocking data].

Verification of hash == hash(constraint) would not be done by VM but by TX consensus rules layer.

After hash verification, there would only be one run of the VM, to execute the concatenated bytecode: [unlocking data] [locking data] <constraint>.

I wrote about this here.

Also, had BIP-12 been implemented instead of BIP-16, then we’d also have avoided mixing layers and equivalent functionality to P2SH would’ve been achieved with:

  • OP_DUP OP_HASH160 {20-byte-hash-value} OP_EQUALVERIFY OP_EVAL or
  • OP_DUP OP_HASH256 {32-byte-hash-value} OP_EQUALVERIFY OP_EVAL.

Would we still call the standard locking script P2SH or would we call it P2E?

3 Likes

hence this thread where the same (and more) functionality is suggested to be made available in a non-hacky way…

Maybe we can stay on topic?

So how about addressing the alternatives laid out above:

  • OP_EVAL alone would enable MAST verification & execution, within a single input
  • Loops CHIP would make the verification bytecode more compact
  • Limits CHIP would make verification of deeper trees possible
  • Dedicated OP_MERKLEVERIFY would further compact the verification bytecode

What is the benefit of entangling verification+execution like you proposed? How about we untangled them and had OP_MAST_VERIFY only verify without executing, and having OP_EVAL do the execution?
Then, each of them would be useful for other applications, too, while you’d still get the functionality with something like:

  • locking script: <root hash> OP_MERKLEVERIFY OP_EVAL
  • unlocking script: <leaf script> <proof stuff>

Both have been proposed for Bitcoin (as a SF) at some point:

2 Likes

I understand you really like op-eval, this is the nth time you mention it in this thread… You’re not actually selling it, though. I have no idea why you are so attracted to it. (that is NOT an invitation to explain it HERE!!)

So while I understand your enthusiasm, it does feel a little like hyjacking this actual p2sh(like) based thread. Feels like “why not nano!” level…
If you want to propose all those changes, start a thread on that instead? It is pretty distracting here, on this one-opcode suggestion that we haven’t really seen any discussion on as a result.

Personally I prefer a simple solution that simply solves the problem laid out in the OP. A clean solution that allows MAST as well as what p2sh today does with no need to learn anything more. And, as Jonas so nicely pointed out, it allows us to keep using a simple address and bip21 style address for such things.

Cheers

For the record I think realistically it will be the path of least resistance (and thus most widely used) to have the whole thing wrapped in P2SH.

locking bytecode: OP_HASH160 <hash160(redeem_script)> OP_EQUAL
unlocking bytecode: [<data>, ...] <merkel_leaf> <merkel_proof> <redeem_script>
redeem script: <merkel_root> OP_MAST_VERIFY

Sending wallets wouldn’t have to know anything about MAST, new opcodes and new cashaddress types.

haha, good observation. Yeah, I understand that will be the first usage for sure. Not directly a bad thing, mind you.

Your proposal would enable OP_EVAL but disguised as something else and with additional steps.
Anyone that wants to use OP_EVAL on a specific script could achieve it by just pushing a corresponding merkel-proof and merkel-root as stack items to your defined OP_MAST_VERIFY opcode and it will be executed accordingly. Just more space on stack and some more computations needed…

Regarding if P2SH is a hack or not I think it is what it is and it’s a thing we have to live with, I wasn’t around when it was discussed over a decade ago. Removing it from the node code (where the “uglyness” have to be handled) means that any coins still locked by P2SH will be anyone-can-spend if the redeem script is known. Not going to happen.

Quote from the BIP itself, authored by Gavin:

Recognizing one ‘special’ form of scriptPubKey and performing extra validation when it is detected is ugly. However, the consensus is that the alternatives are either uglier, are more complex to implement, and/or expand the power of the expression language in dangerous ways.

You’d have to have a similar rule as specified in the OP_EVAL BIP (https://github.com/bitcoin/bips/blob/master/bip-0012.mediawiki):

If there are any OP_EVALs in the deserialized script they are also executed, but recursion is limited to a depth of 2.

Otherwise, given a locking script such as <merkel root> OP_MAST_VERIFY
And the following unlocking script: <merkel root> <merkel proof> <full_script> <full_script> <merkel proof>
And full_script: OP_DUP OP_2 OP_PICK OP_4 OP_PICK OP_MAST_VERIFY
You’d end up in endless recursion. (Note: haven’t carefully looked at this code, but hopefully you get the idea)

Please, start a new topic if you insist on discussing an alternative opcode…

This is confusing and generally destructive to actual discourse we’re trying to have here :frowning:

We’ve now had 80% of this topic discussing something that it is not about.

This is why we can’t have nice things :frowning:

Good thing, there are no objections or better suggestions, so that is positive.

I am discussing your opcode.

It does two distinct things:

I am saying that you can bypass the first one by pushing unnecessary data to just do the second part, and that is something users will do.
It is very on-topic to have a discussion if it’d be better to split these two distinct things into two distinct opcodes.

3 Likes

Seriously, if you want to make that proposal for doing it differently, do that elsewhere… I stated so 4 times here on this thread. Your (plural) insistance to bring up another way, still without any rationale why it would be better, is called trolling. I have been a reddit moderator for years, I know how it works.

If you have an actual feedback on the actual opcode, like problems or otherwise, feel free to do them here.

This is not reddit, this is supposed to be a serious forum for people building.

Signal / noise ratio down to 10%.

Lets close this down, this is not useful.

I’ll answer this obvious mistake…

You actually quoted me on the answer to your own question just above that. The design set out to consume the stack items, meaning they are no longer present on stack. Therefore they can’t duplicate them as they are now owned by the mast opcode. Executing the script will obviously not see “itself” on stack anymore.

From OP:

You can have the code to be executed loaded from some OP_RETURN using: OP_OUTPUTBYTECODE OP_2 OP_SPLIT OP_NIP as many times as you need the bytecode.

As you could see in my example the needed stack elements is pushed twice before the first execution of the opcode. So when OP_MAST_VERIFY is executing the script (when the three items is consumed) it copies the three items still left on the stack (script, merkel root and merkel proof) and runs OP_MAST_VERIFY on those. It could be remedied by enforcing a clean stack, but we have already concluded that it would make the opcode useless.

I genuinely have no clue at this point what kind of feedback you are asking for. I’m pointing out that the opcode can be (ab)used into doing something but discussions about that something is trolling?

3 Likes