Sapio Tutorial Sneak Peak


We’ve been hard at work getting Sapio ready for release, but I figured it would be a good time to share some early documentation & basic tutorial to make Sapio a little more concrete.

Enjoy the sneak peak!

Why is Sapio Different?

Sapio helps you build payment protocol specifiers that oblivious third parties can participate in being none the wiser.

For example, with Sapio you can generate an address that represents a lightning channel between you and friend and give that address to a third party service like an exchange and have them create the channel without requiring any signature interaction from you or your friend, zero trusted parties, and an inability to differentiate your address from any other.

That’s the tip of the iceberg of what Sapio lets you accomplish.

Say more…

Before Sapio, most Bitcoin smart contracts primarily focused on who can redeem coins when and what unlocking conditions were required (see Ivy, Policy/Miniscript, etc). A few languages, such as BitML, placed emphasis on multi-transaction and multi-party use cases.

Sapio in particular focuses on transactions using BIP-119 OP_CHECKTEMPLATEVERIFY. OP_CHECKTEMPLATEVERIFY enables Bitcoin Script to support complex multi-step smart contracts without a trusted setup.

Sapio is a tool for defining such smart contracts in an easy way and exporting easy to integrate APIs for managing open contracts. With Sapio you can turn what previously would require months or years of careful tinkering with Bitcoin internals into a 20 minute project and get a fully functional Bitcoin application.

Sapio has intelligent built in features which help developers design safe smart contracts and limit risk of losing funds.

For more information on Sapio, check out this Reckless VR Talk Sapio: Stateful Smart Contracts for Bitcoin with OP_CTV and slides.

Show Me The Money! Sapio Crash Course:

Let’s look at some example Sapio contracts.

A Basic Pay to Public Key contract can be generated as follows:

class PayToPublicKey(Contract):
    class Fields:
        key: PubKey

    @unlock
    def with_key(self):
        return SignedBy(self.key)

Now let’s look at an Escrow Contract. Here either Alice and Escrow, Bob and Escrow, or Alice and Bob can spend the funds. Note that we use logic notation where (|) is OR and (&) is and. These can also be written as Or(a,b) and And(a,b).

class BasicEscrow(Contract):
    class Fields:
        alice: PubKey
        bob: PubKey
        escrow: PubKey

    @unlock
    def redeem(self):
        return SignedBy(self.escrow) & (SignedBy(self.alice) | SignedBy(self.bob)) | (
            SignedBy(self.alice) & SignedBy(self.bob)
        )

We can also write this a bit more clearly as:

class BasicEscrow2(Contract):
    class Fields:
        alice: PubKey
        bob: PubKey
        escrow: PubKey

    @unlock
    def use_escrow(self):
        return SignedBy(self.escrow) & (SignedBy(self.alice) | SignedBy(self.bob))

    @unlock
    def cooperate(self):
        return SignedBy(self.alice) & SignedBy(self.bob)

Until this point, we haven’t made use of any of the CheckTemplateVerify functionality of Sapio. These could all be done in Bitcoin today.

But Sapio lets us go further. What if we wanted to protect from Alice and the escrow or Bob and the escrow from cheating?

class TrustlessEscrow(Contract):
    class Fields:
        alice: PubKey
        bob: PubKey
        alice_escrow: Tuple[Amount, Contract]
        bob_escrow: Tuple[Amount, Contract]

    @guarantee
    def use_escrow(self) -> TransactionTemplate:
        tx = TransactionTemplate()
        tx.add_output(*self.alice_escrow)
        tx.add_output(*self.bob_escrow)
        tx.set_sequence(Days(10))
        return tx

    @unlock
    def cooperate(self):
        return SignedBy(self.alice) & SignedBy(self.bob)

Now with TrustlessEscrow, we’ve done a few things differently. A @guarantee designator tells the contract compiler to add a branch which must create the returned transaction if that branch is taken. We’ve also passed in a sub-contract for both Alice and Bob to allow us to specify at a higher layer what kind of pay out they receive. Lastly, we used a call to set_sequence to specify that we should have to wait 10 days before using the escrow (we could pass this as a parameter if we wanted though).

Thus we could construct an instance of this contract as follows:

key_alice = #...
key_bob = #...
t = TrustlessEscrow(alice=key_alice,
                    bob=key_bob,
                    alice_escrow=(Bitcoin(1), PayToPublicKey(key=key_alice)),
                    bob_escrow=(Sats(10000), PayToPublicKey(key=key_bob)))

The power of Sapio becomes apparent when you look at the composability of the framework. We can also put an escrow inside an escrow:

key_alice = b"0" * 32
key_bob = b"1" * 32
t = TrustlessEscrow(
    alice=key_alice,
    bob=key_bob,
    alice_escrow=(Bitcoin(1), PayToPublicKey(key=key_alice)),
    bob_escrow=(Sats(10000), PayToPublicKey(key=key_bob)),
)

t1 = TrustlessEscrow(
    alice=key_alice,
    bob=key_bob,
    alice_escrow=(Bitcoin(1), PayToPublicKey(key=key_alice)),
    bob_escrow=(Sats(10000), PayToPublicKey(key=key_bob)),
)
t2 = TrustlessEscrow(
    alice=key_alice,
    bob=key_bob,
    alice_escrow=(Bitcoin(1), PayToPublicKey(key=key_alice)),
    bob_escrow=(Sats(10000) + Bitcoin(1), t1),
)

# t3 throws an error because we would lose value
try:
    t3 = TrustlessEscrow(
        alice=key_alice,
        bob=key_bob,
        alice_escrow=(Bitcoin(1), PayToPublicKey(key=key_alice)),
        bob_escrow=(Sats(10000), t1),
    )
except ValueError:
    pass

Sapio will look to make sure that all paths of our contract are sufficiently funded, only losing an amount for fees (user configurable).

If you wanted to fund this contract from an exchange, all you need to do is request a withdrawal of the form:

>>> print(t2.amount_range[1]/100e6, t2.witness_manager.get_p2wsh_address())
2.0001 bcrt1qh4ddpny622fcjf5m02nmdare7wgsuys5sau3jh5tdhm2kzwg9rzqw2sk00

comments powered by Disqus