P2SH assurance contract

I meant that if you have a hash of refund outpoints you can’t simply require all outpoints of a tx to be equal to that hash. In cashscript terms with the op_checkdatasig preimage hack you could

require(tx.hashOutputs == hashRefundOutputs);

but with native introspection you’d do

require(hash160(refundOutputs)==hashRefundOutputs);

// need to parse refundOutputs

require(tx.outputs[0].value == valueOutput0);
require(tx.outputs[0].lockingBytecode == bytecodeOutput0);

require(tx.outputs[1].value == valueOutput1);
require(tx.outputs[1].lockingBytecode == bytecodeOutput1);

... //repeated for each outpoint

because the hash of all outpoints, which you can get from the preimage, did not get its own opcode. So you need to provide all the value & bytecode data in the unlocking script.

1 Like

ahh got it, you meant hash of all outputs, like an aggregate function

Improved the contract to allow for p2sh refunds. :+1:
Here’s the cashscript code, the repo contains a version with and without the stretch goals described at the very end. Both are commented as to hopefully make them understandable to anyone interested.
Any feedback or question are welcome :upside_down_face:

2 Likes

I looked into adding the option to cancel at any time to the p2sh setup. Instead of pledging directly to the payout chain, a pledger would fund a contract which allows for 1) the pledger to withdraw from at any time 2) anybody to initialize a refund after campaign expiry or 3) the funds to be added to a payoutchain right before campaign expiry.

The reason this solution is not as good as I initially thought is because a campaign can start multiple payout chains and lock pledges in complex and hard to recover ways. It would also add the requirement to keep track of many p2sh pledge utxos compared to just one campaign utxo.

This is really awesome, thanks for working on it!

Sorry if you’ve already discussed it, but if you’re still looking for options to allow pledges to be cancelled in any order: have you considered saving proof of each pledger’s contribution to a refund merkle tree in the covenant? E.g. a tree with a depth of 20 gives you >1 million available leaves. Each pledger can add to the pot and simultaneously prove that they have added the correct “refund leaf” (e.g. their refund P2PKH public key concatenated with the satoshi amount) in place of any empty leaf in the contract’s merkle tree (like this demo). If anyone wants to cancel their pledge, they can prove they own the refund address by signing a cancellation message, remove their leaf from the merkle tree, and withdraw their money.

(Note, if we eventually get CashTokens, this sort of merkle tree can be replaced by issuing NFTs that can be burned back to the covenant to get refunds. It’s the same user experience though, using NFTs just optimizes transaction sizes and allows for more complex refund addresses.)

1 Like

You’re right of course that this is the way to go for cancel-any-time, I’m a bit embarrassed to say I knew of your merkle tree example but did not consider to use it here :sweat_smile:.

When I was talking about a “tree structure” higher up, I was simply thinking about grouping inputs and refunds in groups of 10 or so but that was obviously not the way to go.

So the tradeoff of the refund merkle tree is that both pledge and refund transactions would become quite a bit bigger in size and computationally a lot more intensive with all the hashing. Also managing a large merkle tree is not trivial for me but a good excuse to learn how to work with more complex data structures I guess :grinning_face_with_smiling_eyes: ! That Cashtoken NFTs would also be great in this context really demonstrates to me how versatile they are :+1:

edit: maybe large merkle trees aren’t even currently possible with the 520 bytecode limit and 201 opcode limit?

1 Like

Yeah, we definitely need to improve our contract tooling to make merkle trees easier for developers to use. As of now, it can be worth it to drop features just to avoid needing merkle trees. :sweat_smile:

Some napkin math: the unlocking bytecode would require 21 bytes for each tree level (OP_PUSHBYTES_20 and the 20 byte hash), plus the actual value committed to the tree – a 20 byte public key hash, and an 8 byte satoshi amount. So for ~1.04 million leaves, that is 448 bytes in the unlocking bytecode (limited at 1650 bytes, A.K.A. MAX_TX_IN_SCRIPT_SIG_SIZE) for any spending paths which read or modify the merkle tree.

For the contract redeem bytecode itself (limited at 520 bytes and 201 opcodes), you need the hash (20 bytes), and each tier requires 8 bytes/7 opcodes, so 20 levels is only up to 180 bytes/140 opcodes. Adding the P2SH redeem bytecode to the 448 bytes above, we have ~630 bytes, well under that limit (of 1650 bytes) and still smaller than e.g. a large multisig transaction (>8 signatures).

So you’re well under all the limits for trees well into the millions. If you don’t need much other logic, with the current network limits you can actually support 2**27 (~134 million) leaves (with 11 opcodes and plenty of bytes remaining).

Relevant CHIPs: with CashTokens, you’d be able to fully offload state in this case (no merkle tree needed), so you’d save ~600 bytes on transaction size. With bounded loops the merkle tree verification compresses down to ~20 bytes, and with better targeted limits you’d easily have room for several such merkle tree validations (without increasing worst-case validation costs for full nodes).

1 Like

Thanks!

Is this not just half the picture, only the inclusion proof?
A new tree would also have to be constructed bringing the total to 18 bytes or 14 opcodes for each layer? I think this means that with 140 opcodes you could only have 10 layers or 1024 leaves.

Which sounds awesome!

1 Like

Ah, you’re right! I also failed to count quite a bit of (unoptimized) stack item duplication and juggling. :man_facepalming: (Sorry to mislead you!)

That demo also didn’t prevent malleability around the merkle path (e.g. someone pushing 2s rather than 1s for the OP_IFs; back in scope if we don’t have PMv3’s detached signatures). It also needs to be updated to use OP_HASH256 rather than OP_HASH160 (anticipating some 32-byte P2SH solution – I’m working on another forum post for that).

As currently written, thats ~2 extra bytes per level in addition to requiring two separate merkle tree validations. So that puts us at ~18 opcodes per level, not counting a malleability solution. (A more secure merkle tree also costs us 12 more bytes in the unlocking bytecode per level, but that isn’t a limiter, it just increases transaction size.)

Ouch, hopefully we can get at least 10 levels (plus other logic) within the current limits. I bet we can avoid the alt stack and do both merkle tree verifications simultaneously to save some bytes/opcodes. And of course, clients can be written to handle malleability gracefully, so we could save bytes there if necessary. I’ll try to experiment with that later this week.

1 Like

Ok, I put together a more optimized demo: Experimenting with Contract State Merkle Trees | Bitauth IDE (Aside: I think merkle tree state-management schemes can be made reasonably easy to build/audit with a reusable/composable “standard library”. Components like build_tree_level here could be well standardized.)

Hopefully more correct napkin math:

The contract redeem bytecode requires the 32-byte hash, the unrolled, simultaneous merkle tree validation (per tier: 17 opcodes, 18 bytes), and any other logic (in this example, 25 opcodes). This construction manages to trim malleability prevention down to 3 opcodes for each level, so dropping that (at the cost of wallet clients having to be a bit more intelligent) gets it down to 14 opcodes/15 bytes per level. This demo includes some sample leaf validation logic, so its overhead of ~80 bytes is probably a reasonable estimate beyond tree validation, so for 12 levels (12*18 bytes) that brings us to less than 300 bytes. (Well below the 520 byte push limit; the 201 opcode limit is the issue.) So with malleability protection: 11 levels (2,048 leaves), without: 12 levels (4,096 leaves).

The unlocking bytecode requires 34 bytes for each level (push opcode, 32 byte hash, and OP_1/OP_0 to indicate proof path), plus the values changed in the tree (the “before” and “after” leaf) – in this P2SH assurance contract case, a 20 byte public key hash and an 8 byte satoshi amount. So for 12 levels, unlocking bytecode is somewhere around ~460 bytes + ~300 byte redeem bytecode, for a total of ~760 bytes.

So we have a more definitive answer to what is immediately possible after May 2022. Not nearly as exciting as I thought, but maybe still useful in some cases. Tweaking my overview from above:

  • With better targeted limits we could easily do 4 of these 32-level merkle tree “leaf replacements” with room/hashes to spare (without increasing worst-case validation costs for full nodes).
  • With bounded loops this merkle tree verification compresses down to ~22 bytes (for both merkle trees!).
  • And with CashTokens, we could fully offload this state (no merkle tree needed), saving ~700 bytes on transaction size and allowing for an unlimited number of pledges (refunded in any order).
1 Like

Some context on how that plays out regarding minimum pledge in a 1000 BCH Flipstarter assurance contract, where the app does its best to ensure the contract can complete without requiring unreasonably large pledges:

First image: Today with 600 input slots using either the average method (what is implemented now) or the power curve method (which I would like to implement).

Second image: Same but with 4096 input slots (much better in both cases!)

2 Likes

Ok, as mentioned above:

I’m afraid just about any P2SH-based flipstarter construction is going to be vulnerable to this attack unless we developed some pre-commitment scheme for anyone who might possibly join the contract.

Alternatively, if we can get a solution locked in for 2023, any would-be attackers probably won’t bother investing in infrastructure. That would keep attack profitability low, but only if 20-byte P2SH-based campaigns stay below some limit. I’m not a good person to figure out what that limit is, hopefully other people can chime in and offer estimates.

Both of those options seem pretty rough though – it may be a better use of resources for people to focus on infrastructure that would be ready to deploy on upgrade day in 2023. (Would probably make for an exciting upgrade!)

2 Likes

Indeed! With the Cashtokens upgrade a fundraising campaign with any number of pledges that can each cancel at any time is possible! Keeping the same interactive design where users add pledges to a campaign covenant, the covenant would then keep a minting NFT to give each pledger a receipt. This receipt is an immutable NFT containing the pledge amount and refund address. At any time the pledger send this NFT to the campaign contract to undo his pledge.

Automatic refunds can be enabled by allowing anybody after the campaign expiration to send the NFTs back to the campaign contract and allow to withdraw the pledge amount to the refund address.

The new architecture would not need to add user input to the “simulated state” instead the state about refunds is kept in NFTs so the vulnerability of 20byte p2sh wouldn’t be present anymore!

A drawback of this interactive architecture is that there’s a need for a specialized wallet to interact with the campaign covenant. I am prototyping what the Cashtoken upgraded cashscript contract would look like assuming these new introspection commands.

1 Like

@bitcoincashautist and I also did some brainstorming on non-interactive designs for a p2sh flipstarter that are made possible by the Cashtokens upgrade. Imagine an open source website used by all campaigns were you put in your campaignID and refund address and it generates a p2sh address where you can send your pledge to, this would work with any normal BCH wallet. The campaign would have to keep track of all the separate pledge utxos, tally them up with an NFT and once the campaign-goal has been reached those utxos could be used a inputs for a payout transaction.

However there’s tradeoffs: first, a user can’t cancel at any time without a smart contract wallet which knows the locking script and can provide a signature to spend the funds and undo the pledge. Second, because it is difficult to audit the total amount raised the campaign might hide some contributions and raise more than the campaign-goal. When stretch goals are explicitly disallowed the campaign still might pick and chose utxos to add to the campaign contributions. Third, tallying up all the pledges requires as many transactions as there are pledges.

My takeaway from the discussion was that the non-interactive design might work well if canceling at any time is not a requirement and if all all pledges can be linked somehow (such as dusting the same address) so each pledger can know the total amount raised and so the utxos can be found if the campaign database were to crash.

2 Likes

Completed the 2nd version of the p2sh assurance contracts and added it to the same repository under the folder v2! It utilizes the Cashtokens features to enable cancel-at-any-time! :grinning_face_with_smiling_eyes:
The 2nd version consists of two separate contracts: one for the campaign and one for the cancel/refund NFT. I made a version with and without stretch goals like I did for v1.

Curious to hear any feedback! I think making this was a great way to learn the power of cashtokens!

3 Likes

@bitcoincashautist reviewed the contract and agrees it would work as intended! :grinning_face_with_smiling_eyes:
He catched a few sloppy mistakes I made so I just fixed them in the Cashscript code, thanks @bitcoincashautist ! :pray:

One mistake he found was non-obvious to me from the Cashtokens spec, that OP_UTXOTOKENCATEGORY doesn’t push any capability for an immutable NFT. Whereas I had assumed (and interpreted from the spec) that it would push a 0 :sweat_smile:

1 Like

I was reviewing and rewriting it in Script Assembly using BitAuthIDE, finished just now, here it is:

Flipstarter v2

Script assembly version of Flipstarter v2 contract from here:
www.github.com/mr-zwets/p2shAssuranceContract
Note: upon loading this template, you must edit scenarios and manually replace the bytecode stored pledge receipt NFTs with bytecode matching the temporary keys generated by your browser.
For this, z_helper script is provided, from where you can copy & paste the hex

1 Like

With the Cashscript v0.8.0 prerelease and the publication of cashc@next it’s now possible to compile the cashscript assurance contract upgraded with the CashTokens functionality down to bytecode!

They are just slightly bigger than their v1 counterparts with all the new cancel-any-time functionality.
v2 is 101 opcodes and 165 bytes (without constructor arguments) in size, compared to the 95 opcodes and 150 bytes of v1.

Next up is upgrading the Cashscript SDK so you can easily make transactions with the new token-features on chipnet.

2 Likes

With the proper Cashscript v0.8.0 release it’s now possible to use the CashTokens version of this contract on the Bitcoin Cash mainchain! With the upgraded SDK and the upgraded Cashscript-Playground it’s now easy to make transactions using the smart contracts functions.

There could be a non-custodial webapp for this that connects to the Paytaca browser extension for the smart contract functionality.

1 Like

So I’m using a p2sh contract utilizing the new TransactionBuilder class in Cashscript v0.8.0 on BCHouse based on the v2 p2shAssuranceContract.

Overview

The main difference from the v2 assurance contract is that I’m using several variations of the contract to allow deposits from non-WalletConnect wallets. This makes it:

  1. extremely simple to pledge to a campaign without connecting a wallet or using a web wallet.
  2. effectively “multi-threaded” (really queuing) so that multiple users can pledge at the same time without conflict.

The design of the contract is tailored more for platform usage though, since the address the user pays to is a covenant that requires the transaction spending the deposit to pledge the funds with the campaign utxo and output an NFT receipt (exactly like Mathieu’s v2 assurance contract) OR return the funds immediately.

So due to this covenant design, either the user’s wallet, the web browser, a platform like BCHouse, or an independent 3rd party (like unspent-phi) must execute the transaction. The web browser and the platform are in the best position to do that given the contract parameters.

Payment Requests

I go through the extra step of using BIP70, JPP, and JPPv2 on BCHouse to provide the contract parameters both in the memo field and url for users - allowing them in the worst case to recover from any potential issues with the platform. Some wallets like Bitcoin.com only provide the payment url to users after the fact while others give access to both the url and memo, so I had to use both unfortunately which can be a problem with legacy QR code readers.

Future work

Either way, a simple link to a 3rd party before payment can provide the contract parameters, for example, a separate site on IPFS like this implementation. An even better solution, is using a CashRPC implementation with the same parameters to store and verify the contract and destination address before funding, as well as act on the contract/utxos.

Additionally, the design of both v2 assurance contract and the BCHouse contract don’t allow for compatibility with anyonecanpay type of pledges. This is something that could be fixed by requiring any “over-pledging” to be returned to the pledger immediately and allowing payouts IFF the outputs match the campaign goal as opposed to the campaign utxo value being >= the campaign goal:

So: require(output[0].value === fundingGoal) not require(input[0].value >= fundingGoal)

Anyways, there’s a lot to be desired from my implementation for both wallet connect and non-walletconnect users. I haven’t really made this contract available for users on mainnet (minus my own tests and campaigns) for these reasons.

2 Likes