Disconnecting Simplicity Expressions
Simplicity’s disconnect combinator enables user-defined signature hash modes, delegation, and more
By Russell O’Connor
We recently completed the functional implementation of Simplicity for Elements, which allows any Simplicity program to be executed on the Elements regtest network. The implementation now supports the
disconnect combinator, which allows part of a Simplicity program to be supplied when funds are redeemed rather than when funds are received. We will explore the applications of this feature in this article.
Simplicity’s Merkle Roots
With BIP 16’s pay-to-script-hash (P2SH), Bitcoin developers realized that it is sufficient to commit to the hash of a Bitcoin script when funds are sent and scripts only need to be revealed at redemption time. This allows Bitcoin addresses to have a fixed length independent of the complexity of the script that controls those funds.
We achieve the same result with Simplicity by committing to a Merkle root of the abstract syntax tree of a Simplicity program. This Merklized Abstract Syntax Tree (MAST) has several benefits:
- As with P2SH, Simplicity addresses have a fixed length independent of the complexity of the Simplicity program committed to.
- Any unexecuted branches in the program can be pruned at redemption time, decreasing program size and increasing privacy.
- The computation of the Merkle root of shared subexpressions can be shared.
When P2SH funds are redeemed in bitcoin, a script and its initial stack are provided. The consensus rules enforce that the hash of the provided script matches the commitment of the funds being redeemed.
Simplicity programs do not have an initial stack or inputs. Instead, Simplicity has a primitive
witness expression that contains data such as signature values that are excluded from Simplicity’s commitment Merkle root (CMR). These
witness values behave as inputs because they can be freely set when funds are redeemed.
The disconnect Combinator
disconnect combinator composes two subexpressions, but only the first of these subexpressions is included in the expression’s CMR. The second expression is “disconnected” from the commitment, allowing for it to be freely programmed at redemption time. Instead, the second expression’s CMR is passed as an argument to the first expression. This allows the first expression to constrain the disconnected expression.
Signature values in Bitcoin include one byte for the signature hash type. This signature hash type allows the signer to choose among six possible signature hash modes. We can view Bitcoin’s signature hash type as an extremely tiny programming language — a language with only six expressions — that lets one program the signature hash function. Unlike Script, which must be committed at the time funds are received, this signature hash programming is selected at signing time. In order to be secure, the signed message includes the sighash type used to generate the signature hash.
Universal Signature Hash Modes
CheckSigHash Simplicity program uses the
disconnect combinator to allow the signer to build their own custom signature hash mode which can include and exclude whichever transaction components they wish for their digest. The CMR of their chosen signature hash expression is combined with the output of their expression to form the signed message. A witness combinator holds a digital signature for that message which is verified using the Schnorr signature algorithm against a public key fixed by the
CheckSigHash program. Because the signature commits to the CMR of the signature hash mode expression, the disconnected expression cannot be altered without creating a new signature.
While any custom Simplicity expression can be used to create a transaction digest, we will provide jets to accelerate common signature hash modes, including
SigHashAll and novel modes such as
SigHashAnyPrevout. For an example transaction using the universal signature hash used with the
SigHashAll mode, see our gist.
The arbitrary Simplicity expressions used to create custom signature hash modes can also include assertions. Assertions can be used to enforce requirements such as:
- A lower bound on the transaction’s timelock
- A minimum amount sent to a (change) address
- A covenant
- A signature from another party
One can delegate control of funds to another party by using a signature hash mode that returns a fixed zero value but requires another
CheckSigHash for that party’s public key. By signing the CMR of such a delegating signature hash mode, the owner of the public key can delegate control of all their funds to another public key. The owner of the new public key can compose this delegating signature with their own signatures to transfer funds. The original public key maintains control of the funds and can still transfer funds with new signatures using a regular signature hash function. When the original owner moves their funds to a new public key in this way, the delegated control is revoked.
Delegation can be combined with custom transaction digests as well. By creating a digest that includes the transaction’s previous outpoint, the delegation can be limited to a single unspent transaction output. By additionally including a single (change) output in the digest, only part of the funds are left available to the delegatee. This allows a treasurer to delegate limited spending ability to a trusted assistant.
Loops without Bounds
disconnect combinator enables other features as well. One can constrain the disconnected expression to a single fixed CMR. While this may appear useless, it enables a trick where one can pass the expression’s own CMR as an argument to the expression itself. Then the expression can use its own
disconnect combinator to call another copy of itself. Such an expression creates an unbounded loop that commits to arbitrarily many nested copies of itself, which can be terminated by pruning the expression at a depth past which no more copies are executed.
This trick allows one to circumvent Simplicity’s bounded execution model and commit to an unbounded loop. However, at redemption time, this loop must be “unrolled” as many times as it is going to be executed, and thus still allows static analysis of Simplicity programs at redemption time. Due to subexpression sharing, the main body of the loop is shared between each iteration of the loop, meaning only a small fragment of code ends up replicated.
The drawback of using this technique is that you cannot bound the redemption cost of your Simplicity program in advance; therefore, we don’t expect this trick to be commonly used.
disconnect combinator now implemented, we have completed the functional implementation of Simplicity. We have seen how to use the
disconnect combinator to enable features such as:
- Universal signature hash modes
- Unbounded loops
A more in-depth presentation of the
disconnect combinator can be found in the Delegation chapter of the Simplicity Technical Report.
We continue to work towards a production-ready release of Simplicity. While the language functionality is now complete, there is more work to be done:
- Anti-denial-of-service mitigations, including a cost model for resource usage.
- Create a wide selection of jets to reduce the cost of common expressions.
- Update the transaction introspection primitives to support new Taproot features.
In addition to the consensus implementation of Simplicity, we are building high-level tools for generating Simplicity programs. One of the tools under development is a Simplicity target for the rust-miniscript library, allowing users to compile Miniscript policies to Simplicity and recover those policies from Simplicity programs.
Keep Up To Date With Developments
To keep up with the latest Simplicity developments, make sure you sign up for our official newsletter below and subscribe to the simplicity-dev mailing list.
Developers who want to explore potential uses of Simplicity are encouraged to check out the Elements branch and start playing around, but we’d stress that for now, this should only be attempted by expert users.