to share some learnings in this area:
the schnorr trick with CAT lets you get an inputs scriptpubkey, prevout, and index onto the stack. You can also get the outputs onto the stack. if you enforce that “this” input’s scriptpubkey matched an output scriptpubkey, then you can enforce that the spend is always happening back to the same taproot address. You can use this to build state machines where different states for your contract are different tapleafs that enforce the validity of a state transition (if you want all the gory details on this technique including the schnorr math, I gave a talk at Bitcoin++ that walks through it: https://youtu.be/U5qcL0hI30k?t=13016)
A technique I had played with in an early vault prototype was to pass state from TX_n to TX_n+1 by:
- putting a little state commitment in an output of TX_n
- in TX_n+1, reconstruct the (as @ajtowns pointed out above) witness-stripped transaction on the stack, asserting the state commitment in the output from TX_n (either an OP_RETURN or a normal output if you’re committing to a SPK)
- HASH256 the transaction on the stack to get the TXID, assert that it matches the prevout of the input
- you now have previously-committed state on the stack!
the popular name for this technique now is the “state caboose” (you pull it along). there are lots of cool things you can do with that (like committing to a withdrawal destination in a vault) but as @salvatoshi pointed out, one really cool thing is you can do a constant-sized inductive proof of contract validity. Here’s how it works:
Suppose you have a contract that is instantiated in TX FOO. let the TXID FOO be the contact instance ID. To spend a UTXO encumbered by FOO along, you have some set of validity rules for each step (for example, signature checks, amount validation logic, timelocks, whatever). Additionally, there is a “contract history” check that you do. There are two cases you have to check for contract history (implemented as different tapleafs):
- the parent TXID is FOO
- the parent transaction spent from the contract scriptpubkey to the contract scriptpubkey
if you have those two checks, then you always know that a UTXO in the contract is valid, because it came from either a valid state, or from the instance genesis. In the inductive case (coming from a valid state), you need to pass in a constant-ish amount of witness data (you can have different numbers of inputs and outputs, but its a bounded amount, so you in practice you write a script for the worst case), which means that you can actually use this in Script. The spender may need to do some extra work and state management, but the amount of data that hits the blockchain is bounded to a relatively small size.
An extension on this that we’ve done some experimenting with is that you can delegate your contract validity rules to another script. Here’s the short version:
- in tx_n you make a commitment that says “in the next transaction, I want my UTXO to obey the rules of this other contract (scriptpubkey)”. This commitment is made in the state caboose described earlier
- in tx_n+1, you do your normal contract history check (the inductive bit above) and then you check if the input at index 0 (or whatever) is the scriptpubkey of the contract you delegated to. if not, fail
- now your contract instance UTXO has to follow the rules of that other contract! this is super useful as either an upgrade/extension mechanism
we’ve been experimenting with this stuff assuming only OP_CAT, but other opcodes make things a lot cleaner or open up other interesting avenues. for example, CCV or some TAPTWEAK makes state carrying cleaner and removes some technical limitations on the size of transactions in these contracts. CSFS lets us more easily do authenticated delegation (you can delegate to an approved contract) which you can do with just CAT using some funky signing over the state caboose but its a lot easier to do with CSFS. more-specialized introspection opcodes would make the whole thing easier to reason about and would have smaller scripts.