CHIP 2021-08 ZCEs: Zero-Confirmation Escrows

Zero-Confirmation Escrows (ZCEs) are contracts which enable instant, incentive-secure payments on Bitcoin Cash. They’re particularly useful in point-of-sale, ATM, and vending applications where payers have no prior or ongoing relationship with the payee.

Supporting wallets can add ZCEs to transactions to guarantee that the transaction will not be double-spent. Wallets can instantly make a long series of ZCE-secured payments using the same starting funds, and ZCEs require no holding periods or other delays in wallet user experiences.

ZCEs are a refinement of prior work made possible by improved contract tooling and the implementation of Double Spend Proofs (DSP) on the Bitcoin Cash network. They require no consensus changes and can be deployed without coordination. Once a critical mass of miners implement ZCE-claiming code, businesses can safely accept ZCE-secured transactions without delaying the payment experience to monitor the network.

The CHIP is here:

And @marty has already completed an end-to-end reference implementation. :rocket:

Reviews and feedback are deeply appreciated, either here or on GitHub issues. Thanks!

15 Likes

I read the spec and… I… I don’t know what to say.

If there are no serious bugs in this… this is just fucking genius.

It possibly ultimately solves the confirmation problem once and for all. Transactions will be instant and secure, just like that.

This is just so good it seems impossible.

5 Likes

Why is knowledge of the reclaim public key required for the miner claiming the forfeit? Shouldn’t proof of double spending be enough?

2 Likes

Thanks for reviewing the spec!

It’s enough to construct the unlocking bytecode, but the miner also needs to know what the full redeem bytecode is to create a transaction spending from the P2SH ZCE contract. Other than revealing the redeem bytecode, the reclaim transaction is also important for identifying ZCE-secured transactions, since they otherwise blend in for other network participants (ZCE outputs are just P2SH outputs, and can’t be differentiated from any other P2SH output until they are spent).

3 Likes

Fair enough.

My misunderstanding happened when tracing the contract and I assumed you needed to match the reclaim public key for both branches. That seemed like a weird requirement.

The last opcode on the first line is OP_EQUAL, I assumed OP_EQUALVERIFY.

For the miner to reclaim the output, they have to pass a value that doesn’t match the reclaim public key and that will be false for the OP_IF, so they don’t need to know that key.

The overall system does make memory pool handling harder. One of the big advantages of the first seen rule is that you don’t have to handle removal of long chains of transactions. The only exception been once per block received.

It may be worth adding a rule that miners shouldn’t build on ZCE outputs (other than the reclaim transaction).

2 Likes

Right, for efficiency, we choose the code path based on whether the item provided is the reclaim public key or something else. You can see the full execution for miner reclaims in Bitauth IDE – here’s the miner reclaim for the first input of a 1-tier merkle tree:

Thanks for digging in to this!

It’s worth noting that ZCE replacement is reasonably limited by 1) if there is any replacement, miners will be able to claim the largest ZCE of the available replacements, 2) replacements must take place within 5 seconds (this , and 3) replacements are ignored unless the ZCE grows by at least the replacements’ transaction fee (so the processing of replacements always costs a DOSer as much in additional ZCE “fees” as it would cost to create an equivalent-sized transaction).

So with all of that, if replacements ever happen, they pay the network for the trouble.

Even with those limitations, if there is still concern about DOS strategies based on replacing long transaction chains, we can simply disallow the use of ZCE transaction outputs for the 5 second lock-in period. (There’s no value beyond 5 seconds, since no honest nodes will accept replacements after that time.)

I’m not sure if it’s enough of a concern to justify adding that sort of additional logic, but 5 seconds of delay in the network accepting a transaction re-spending the ZCE output value probably won’t even be noticeable by users. (Especially since escrowed funds themselves already require a confirmation before they can be used by later ZCEs.)

I don’t have a strong preference either way; I would love to hear developers from the various node implementations weigh in.

1 Like

I am quite interested in the details regarding the “Eliminating One-UTXO-Per-Address Limitation” solution using the detached signature (PMv3) feature. Is there an outline or high level description on how it can be solved?

Currently many wallets do spend all UTXOs from the same address due to (valid) privacy reasons and given the number of services that sends all coins to one address (for instance tipping services, read.cash and peoples indifference to setup new withdrawal addresses on exchanges) it might be an UX issue to not be able to combine those outputs in one payment. And given that the inputs are required to have at least one confirmation the wallet can not combine those inputs to a new address and use that for the payment…

Edit: on a related note.
I think it would be a good idea to have a strong recommendation that wallets should not use an UTXO from block 661647 (BCH/XEC split) or earlier since there is no way for the wallet to know if those outputs have been spent or not on another chain.

2 Likes

Sure! You can see the SIGHASH_DETACHED” signing algorithm here. PMv3 allows multiple inputs to use a single signature covering the entirety of the transaction (“cross-input signature aggregation”). So there’s not much more to it – since all UTXOs from a single address would be signed with just one signature, it’s safe to use multiple UTXOs. The ZCE can only be claimed if the user’s wallet tried signing another message (like a conflicting transaction) with that same public key. So there’s no coordination needed between the ZCE and PMv3 CHIPs, but if PMv3 is deployed, we’ll get multi-UTXO-per-address for free.

This is a fantastic point. I think the best solution for both privacy and UX is PMv3, but it would probably be a good idea to recommend a safe, maximum-privacy strategy which works for now.

Without PMv3 I’m afraid we can’t fix the UX part, but privacy-wise, wallets should probably add all UTXOs for used addresses to the second reclaim transaction. It’s always broadcasted at the same time, and merging those inputs immediately doesn’t lose any further privacy. (If the wallet waits around to use them later, future transactions can be easily connected.) I’ll open an issue about that, thanks!

Ya, unfortunately, even that wouldn’t be enough. A lot of users have hierarchical-deterministic wallets from before the BCH/BTC split (and BSV and XEC), so wallets which could possibly have been imported (or are otherwise simply shared) are vulnerable, even if the UTXOs are new (if the user continued making transactions on the other chains).

I think that scenario is technically covered by Wallet UTXO Selection criteria 2, but maybe it would be valuable to add a more explicit caveat (with some bolding) at the end of the section… I’ll try to do that too, thanks!

@TierNolan thanks for bringing this up – I opened an issue:

The question for node developers is: how expensive is mempool handling of ZCE replacements if attackers can build long chains of transactions for each replacement? (Each replacement costs the attacker its minimum transaction fee, since the ZCE must be larger by at least that value.)

Do we need to prevent further transactions from building on reclaim transactions until the 5 second replacement window is over?

I’m now leaning toward “yes” for this, since the cost is quite low for a solution which provides complete protection (even for particularly naive node implementations).

Just out of curiosity so I get the vulnerability correct: Is it only the miner that finds the next block that can perform such “ZCE sniping” by keeping an index and do lookups of public keys on different chains?

As a pure DOS, the attack sequence, it would work as follows.

Attacker sends a chain of transactions.

A → B → C → … → Z

The attacker then sends B’ which is a ZCE transaction with a low escrow amount (e.g double the tx fees for the whole chain) and also C’ which is the reclaim transaction.

The replacement means that B and C are replaced by B’ and C’. This requires removing B to Z from the memory pool.

Part of the complexity is that the code must handle replacement. When they were removing the tx chain limit, they found that removing it completely made things much cleaner for the code. It took more effort to calculate the limit than handling long chains of transactions.

The developers can comment on exactly how it works now, but having to remove transactions during normal memory pool operation (rather than when handling new block arrivals) may be more complex than it appears.

Did you consider having special outputs that can be fast spent? You would have to move some money to these “pre-charged” outputs, so it is less user friendly, but that could be handled by wallets. These special outputs could have special rules to protect against long chains.

2 Likes

Eh… I clearly didn’t think about this long enough yesterday. If you just add the unused UTXOs as inputs to the reclaim transaction, that defeats the purpose of not adding them to the first transaction. :laughing:

So if we’re starting with a wallet with N same-address UTXOs, the best we can do is spend one in the ZCE, then merge the other UTXOs with the output of the ZCE reclaim transaction after 11 blocks have passed (ideally using CashFusion). And even that is messy, since the ZCE reclaim transaction outputs could otherwise be re-used in a further ZCE transaction after just 1 block (and they can even be passed through CashFusion within that block).

Another (possibly better) strategy would be for wallets to merge same-address UTXOs in advance (using CashFusion), and spending only to specially-derived “ZCE-ready” outputs which the wallet can be sure will not be re-used by other wallet clients sharing the same imported HD key. (Or at least the other wallet clients can be expected to take the same ZCE precautions.)

The downside of that approach is that must happen at some specific time before the ZCE transaction (if the user even opens the wallet), and it’s also hard for wallets to anticipate when the last payment to an address might be received (to reduce the number of merges needed, which leak access-timing information for the owner).

Anyways, I’m not sure if we can really make a general recommendation for all wallets on this issue. The issue is fully solved by PMv3 though, so it’s also probably not worth wallets expending a lot of development resources on temporary patches.

1 Like

Sort of, but we can also expect that any claimable-ZCE will be quickly claimed. If “naive” miners don’t run that logic themselves, someone else likely will create the claim transaction and broadcast it. (And anyone between them and a naive miner can swap the output to themselves.) I expect many miners will also ultimately run general-purpose, anyone-can-spend claiming logic (which will appreciatively pluck the double-spend info out of any transactions broadcasted by the earlier parties, claiming it without the reconnaissance work).

So we can expect that someone will grab the ZCE, even if very few miners are cognizant of their being a network-wide race.

Ah, right, thanks! I was focusing on the scenario where B is already a lower-escrow ZCE amount, and gets replaced with a B’ of a higher escrow amount. (The second case can be eliminated by disallowing spending of ZCE transaction outputs for the 5 second lock-in period.)

The non-ZCE chain replacement is definitely harder to handle without having some limit on unconfirmed transaction chains.

I would much rather we avoid any setup requirements, since they are almost guaranteed to regularly cause UX delays. Especially if the setup isn’t necessary for typical operation, but only as a sort of signaling mechanism for how honest nodes should handle broadcasting. (Also, I’m not confident this wouldn’t set us back to having all unconfirmed transactions incentivize fraud-as-a-service mining. Unconfirmed transaction chains can always be killed by a paid-off miner, and the more they’re relied upon, the more likely we are to see those miners in the wild.)

I think there are probably some reasonable solutions though. Maybe something that takes advantage of the 5 second lock-in period? (Reminder: 5 seconds is ~2x the expected time it takes for the entire network to hear a transaction, so all commercial ZCE activity can be expected to happen within this time. ZCEs which attempt to e.g. replace a transaction from 1 minute ago would never be accepted by a merchant, and are almost certainly malicious/DOS attempts.) How crazy would it be for node implementations to keep the last 5 seconds of mempool updates in some sort of pre-commit log? After 5 seconds, the “rollback instructions” can be safely thrown away.

I’ll have to think about it more. I’d love to hear from node implementation devs on this topic!

It depends on exactly on how much change is made. Your system doesn’t require a consensus rule change, though it does depend on node non-consensus/policy changes.

To prevent more than one transaction being built on an output, there needs to be covenant functionality. You need to place a restriction on the scripts of the outputs of the transaction that spends the current output.

Maybe a hard fork would be acceptable to have a flag or something. It requires that all outputs of the next transaction are unspendable until 1 block has passed.

@bitjson

Hey Jason, please don’t forget to make some Flipstarter to let us throw money at you, your invention is potentially genius beyond belief.

3 Likes

Testing to see if mentioning @escrowtim will allow him to post here.

4 Likes

Excited to announce that I’ve implemented Zero-Confirmation Escrows for BCHN in the following merge request:

Improvements to the ZCE Specification

While implementing this MR (and after discussing with @bitjson and @marty), it became clear that the implementation would be simpler (and more secure) with one change to the spec, which I have incorporated into the MR:

Make the ZCE locking script standard (rather than wrapping it in P2SH). That way, nodes can determine whether a transaction is ZCE-secured simply by inspecting it (without needing to wait for the subsequent escrowReclaimTx to reveal the redeem script).

This MR also features a more optimized version of the ZCE locking script (courtesy of @bitjson) that saves a few bytes of data and no longer requires the altstack.

DoS Mitigation

A special thanks to @TierNolan, who identified a potential DoS issue involving the replacement of long chains of unconfirmed transactions.

This MR addresses that issue by ensuring that a ZCE-secured transaction may only replace a double-spend (and its descendants) if doing so is economically rational (i.e. the value of the escrow on the ZCE-secured transaction exceeds the cumulative value of the miner fees on the transactions it replaces).

Any peers that repeatedly send ZCE-secured transactions without adequate escrow to warrant replacement get banned.

In my testing, I found that removing an unconfirmed chain of transactions from the mempool is ~30x faster than adding it:

* The data in the above table was collected by running the chain_of_double_spends_replaced_by_zce unit test in my MR and varying the length of the unconfirmed transaction chain.

Additionally, after setting up a local network of 3 interconnected BCHN regtest nodes, I discovered that nodes are able to propagate chained transactions over p2p at a maximum rate of ~30 tx/s. And since an attacker has only 5 seconds to build his chain of conflicting unconfirmed transactions, the maximum number of chained conflicting transactions that can be propagated network-wide in under 5 seconds is ~150, implying a worst-case replacement time of ~2.5ms per the table above.

Therefore, the low computational cost of replacement, combined with the DoS mitigation code above, should provide adequate DoS protection.

Proposed Testing and Activation Timeline

Because deploying ZCE requires a standardness change, it is critical that all participants in the BCH ecosystem (especially miners and merchants) activate ZCE standardness at the same time to ensure that non-ZCE zero-confirmation transactions remain secure. Therefore, the May 2023 upgrade would be an ideal time to activate ZCE on mainnet and would give us almost a full year to fully vet ZCE on testnet to ensure there are no unforseen issues.

Acknowledgments

Thank you to everyone who made Double-Spend Proofs a reality, especially @tom, @freetrader, @cculianu, and @im_uname. This MR would not have been possible without DSPs, which enabled ZCE to be implemented in less than 600 lines of code (excluding tests). And thank you to @bitjson and @marty who authored the ZCE CHIP and answered questions and made improvements to the spec as I was implementing this MR, as well as Awemany who pioneered the original Zero-Conf Forfeits mechanism on which ZCE is based.

Would love to get feedback on this implementation!

8 Likes

Q: would it be possible to make the P2SH hash constant for all contracts - by extracting variable parts into another output - entangled with the “main” contract using the new introspection opcodes. By entangled I mean: have each of the output’s redeem script require spending the other one in the same TX. Something like this: Introspection opcodes are so cool, here's a teaser : btc

1 Like

What is the business case for this?

So far no merchant seems to even be asking for double spent proofs, why does one feel a need to improve upon it?
The CHIP is awefully hand-wavy on this.

Small observation; a collaborating miner that mines a double spend (the only thing that DSP doesn’t protect against) is also not going to get stopped by this one. Which means that the confidence level can not honestly be advertised as better.

Not feeling the support at this time.

2 Likes