excerpted from Designing Bitcoin Contracts with Sapio
How do we think about Smart Contracts and CTV?
Before CTV, in most Bitcoin smart contracts, we think at the key-level. That
is, what is a complex set of signers and satisfactions to unlock a specific
coin. But once we unlock a coin, the smart contract usually does not encode
any further restrictions on how it may be spent.
You could think of this as "a key to a car". If it unlocks the car, you can
take the car wherever you want.
With CTV, we hope to encode a bit more information about how coins should
move by providing the paths that the coins must move through as well. So
rather than just being the key to a car, you could think of it a bit more
like the keys to train -- still required to start the engine, but you have to
stay on the tracks and there is a finite number of tracks to pick at any
juncture.
That's all a bit abstract. Think back to the Hello World example we saw earlier. We
created a coin with the following options:
- Alice and Bob Agree --> coin goes anywhere
- Timeout --> coins go back to Alice and Bob
Now imagine we wanted to change the rules a little. What if instead of rule 2
apply after a timeout, what if we wanted the timeout to be measured from the
time that Alice or Bob claimed they wanted to use the escrow.
This puts us in a little bit of a pickle. Sure we could just re-write the rules:
- Alice and Bob Agree --> coin goes anywhere
- Timeout since Alice or Bob requested --> coins go back to Alice and Bob
But Bitcoin doesn't have a script level notion of "since" a part of a witness was constructed. The CTV way to think of this script is to define a state machine with two states S in {Normal, Closing} and the rules:
S <-- Normal:
- Alice and Bob Agree --> coin goes anywhere
- Alice or Bob Requested --> (S <-- Closing)
S <-- Closing:
- Alice and Bob Agree --> coin goes anywhere
- Timeout since (S <-- Closing) --> coins go back to Alice and Bob.
What drives the transition from Normal to Closing? Just a standard Bitcoin transaction!
So What is Sapio
Sapio is an embedded domain specific language for defining these sorts of
state transition rules to build smart contracts for Bitcoin.
CTV is used as the mechanism to enforce that specific state transitions occur.
When we write a program in Sapio, we are designing an arbitrary state
machine that can run any program.
When we compile a Sapio program, we run that state machine to completion and merkelize the resultant program states into a fixed graph.
As such, Sapio is a very powerful framework for designing Bitcoin smart contracts, but we're constrained to the set of contracts where we can enumerate all possible end states.
BIP-119 Emulation
Changes to Bitcoin take a long time. The star player in making Sapio work is
BIP-119, and that might take a while to get merged. To get around this, Sapio
provides some tools to enable similar functionality today by emulating
BIP-119 with signatures.
The Default Emulator
Sapio CTV Emulators defines implementations of a local emualator that can be
used by sapio compiler library users. To use such an emulator, a user can
generate a seed and create a contract. After creating the contract and
binding it to a specific UTXO, a user should be able to delete the seed,
ensuring that only the compiled logic may be used. Alternatively, they can
retain the seed and promise not to improperly use it.
This crate also defines logic for servers that want to offer emulator
services to remote compilers. This is convenient since the emulator server
must be kept secure, so an organization may want it to be more tightly
safeguarded.
The emulator definitions include wrapper types that compose individual
instances of an emulator into a federated multisig. This is useful
for circumstances where a contract is between e.g. 2 parties and both
have a emulator server. Then the contract can be "immutable" unless
both collude.
To aid in experimentation, Judica, Inc operates a public emulator server for
regtest.
[
"tpubD6NzVbkrYhZ4Wf398td3H8YhWBsXx9Sxa4W3cQWkNW3N3DHSNB2qtPoUMXrA6JNaPxodQfRpoZNE5tGM9iZ4xfUEFRJEJvfs8W5paUagYCE",
"ctv.d31373.org:8367"
]
How it works
See the source code for more detailed documentation.
CheckTemplateVerify essentially functions as a self-signed transaction. I.e.,
imagine you could create a public key that could only ever sign a transaction
which matched a certain pattern?
To implement this functionality, we use BIP-32 HD keys with public derivation.
On initialization, a server picks a seed S and generates a root public key K
from it, and publishes K.
Users generate a transaction T and extract the CheckTemplateVerify hash H for
it. They then take H and convert it into a derivation path D of 8 u32's and 1
u8 for non-hardened derivation (see hash_to_child_vec
).
This derivation path is then applied to K to generate a key C. This key is
added with a CheckSig(SIGHASH_ALL) to the script in place of a CTV clause.
Then, when a user desires to spend an output with such a key, they create the
entire transaction they want to occur and send it to the the emualtor server.
Without even checking to see that the key is used in the transaction, the
server generates the template hash H' (which should equal H) and then signs,
returning the signature to the client.
Before creating a contract, clients may wish to collect all possible
signatures required to prevent an availability fault.
This scheme has the benefit that:
- contract specification can occur without any online processes
- The server has no intelligent logic, all guarantees are structural.
- Server is completely stateless.
- Availability/malfeasance can be controlled for with multisig
- 1:1 functionality mapping to CTV
The downside of this approach to emulation is that:
- It is somewhat inefficient for scripts which have many branched possibilities.
- No inherent mechanism to delete keys after use to protect against future exfiltration.
Why BIP-32
We use BIP-32 because it is a well studied primitive and derivation paths are
compatible with existing signing hardware. While it is true that a tweak of
32 bytes could be directly applied to the key more efficiently, easier
interoperability with existing tools seemed to be the best path.
Customizing Emulator Trait
This emulator trait crate is a base that exports a trait definition and some
helper structs that are needed across the sapio ecosystem.
Defining the trait in its own crate allows us to use trait objects in our
compiler internals without needing to have the compiler directly depend on
e.g. networking primitives.
As a user of the Sapio library, you can define your own custom emulator logic
but that's out of scope of this book.
Future Work
There is a plan to make
emulation more efficient based on Merkelization, but it is not yet
implemented because it messes with the current way the compiler works.
The efficiency issues are also solvable, more or less, with taproot.