Oracles, Bonds, and Attestation Chains

Day 20: Rubin's Bitcoin Advent Calendar

on December 17, 2021

This post is syndicated from rubin.io.

Welcome to day 20 of my Bitcoin Advent Calendar. You can see an index of all the posts here or subscribe at judica.org/join to get new posts in your inbox

Today’s post is going to be a bit lighter weight than yesterday’s. We’ll cover some high level concepts around oracles and then look at some Sapio.

The genesis of this line of inquiry was a conversation with Robin Linus that led to a pretty cool whitepaper, so definitely read that if you find this post compelling.

Oracles

Oracles are cool! The most basic form of an useful bitcoin oracle is just a signing key that signs transactions or reveals information that it “should” according to some rule.

Protocols for oracles like discrete log contract oracles produce more generic “key material reveals”, that are more similar to releasing information that allows counterparties to decrypt the relevant signature.

One of the problems with oracles is that they can equivocate, that is, sign multiple conflicting statements. It would be nice if we could esnure that they would be consistent, no?

Bonded Oracles

In order to make the oracles consistent, what we can do is set up our oracles such that if the oracle ever signs two statements they reveal their private key to the world. The common way that this is done is via nonce reuse, which is essentially a way that you can extract a private key from a signature on messages m1 and m2 using the same nonce r1.

While revealing a key might be punishment enough, we can do one better. We can require that if a nonce is leaked, meaning some statement was equivocated, then a some bitcoin protected by that key can be ‘stolen’ by anyone.

But this form is a little problematic, for a few reasons. Reason one is that the oracle could cancel their bond and take it back while there are still contracts settling with their data that they then equivocate on.

The other issue is that the funds in the punishment could be claimed by anyone, including a miner or the oracle themselves, and especially if oracles are also miners!

To fix the first issue, we need to lock up the fund for e.g. 2 weeks and only use the oracle for the first week to permit 1 week gap in closing. This creates a new issue that bonds are always expiring, but maybe that’s OK.

To fix the second issue, we need a way of restricting where the funds go to definitely be out of reach of any bad guys, e.g. burned.

CTV Fixes This.

If you had checktemplateverify, you could stipulate that a bonded oracle must initiate a bond redemption on chain, at which point anyone can challenge it if they know the key and they are guaranteed sufficient time to post a challenge.

The second fix is that CTV can stipulate that the funds must be burned by sending to an OP_RETURN, not released to miners (which would be problematic if a miner was also an oracle).

Now our oracle is ready to sign all sorts of stuff, and we can make sure that for a given Nonce we never sign two conflicting statements.

DLCs?

We can now use this type of oracle for a DLC protocol. We just create the contract and then we sign+reveal using our staking key whatever messages are required. Any cheating, and anyone who detects it can burn our money.

Attestation Chains

One of the other cools things we can do with our Bonded oracle is to sign a chain of attestations.

For example, we could sign message 1, and then sign message 2, and then sign message 3.

We can turn this into a “blockchain” of sorts if when we sign m2 we include a hash of m1, and when we sign m3 we include a hash of m3.

But we can go a step further. If we’re careful, we can set it up so that ‘branching’ on any message in the chain (by equivocating/producing a conflicting statement) leaks the key of the bonded oracle with a trick I (think?) I came up with. Here’s roughly how it works:

message 1: INIT with PK K, nonce R1 for m2, 1 BTC at risk in output X
message 2: SIGN with K, R1 H(m1), nonce R2 for m3
message 3: SIGN with K, R2 H(m1), nonce R3 for m4

If the oracle were to ever branch, it would look like this:

message 1: INIT with PK K, nonce R1, 1 BTC at risk in output X
message 2: SIGN with K, R1 H(m1), nonce R2
message 3: SIGN with K, R2 H(m2), nonce R3
message 3': SIGN with K, R2 H(m3), nonce R3'

The leak would be able to extract K’s secret key via the reuse of R2.

While it might seem that you could ‘get away with it’, because we verify at each step that the last used nonce was from the prior step it cannot be forged. The commitment to H(mi) also makes it more difficult for an invalid signature to float around since from just the top you can know what all the other states should be.

Proof of Stake?

Essentially we’ve built a system for proof-of-stake on Bitcoin. Imagine you have 100BTC locked up in these contracts across 127 instances, and you want to run some system based on it.

You can just download the message signed at state Mn and see what the majority of signers voted for that slot.

Any signer who cheats gets their funds burned, and you’d learn to exclude them from consensus.

If you do need to have a ‘rollback’, you can do it by engineering your protocol to allow new updates to the chain of signatures to produce a rollback.

Partial Slashing

You can even implement partial slashing. Suppose you have 10 coins in a contract under key K1. If a cheat is detected, it authorizes a txn which burns 2 and puts the remaining 8 into key K2. The next round of slashing could put 6.4 under K3.

Alternatives to Burning

Burning sats is sad. What if instead of a burn, coins went into an annuity that would be claimable 100 years from now? That way, no economic agents around today can plan to cheat and capture the value of it, but the burned coins can serve a real function. While this is slightly less secure than a full burn, it’s also more secure since it creates an incentive to continue to build the chain.

Or donate to a well known chairty address/developer fund :p

To begin, we’ll define some ’type tags’. This is a technique in rust where we define empty structs that let us build a little state machine in the type system. You can read more on the technique here.

/// # Operational State
/// State where stakes should be recognized for voting
#[derive(JsonSchema, Deserialize)]
pub struct Operational;
/// # Closing State
/// State where stakes are closing and waiting evidence of misbehavior
#[derive(JsonSchema, Deserialize)]
struct Closing;
/// # Staking States (Operational, Closing)
/// enum trait for states
pub trait StakingState {}
impl StakingState for Operational {}
impl StakingState for Closing {}

Next, we’ll define an interface that an implementation of a Staked Signer should implement:

By default something that is declared is given a default not-present implementation.

/// Functional Interface for Staking Contracts
pub trait StakerInterface
where
    Self: Sized,
{
    decl_guard!(
        /// The key used to sign messages
        staking_key
    );
    decl_guard!(
        /// the clause to begin a close process
        begin_redeem_key
    );
    decl_guard!(
        /// the clause to finish a close process
        finish_redeem_key
    );
    decl_then!(
        /// The transition from Operational to Closing
        begin_redeem
    );

    /// Why would anyone ever cheat!!
    #[then(guarded_by = "[Self::staking_key]")]
    fn cheated(self, ctx: sapio::Context) {
        let f = ctx.funds();
        ctx.template()
            // commit to metadata here for convenience, but really could be anywhere!
            // exercise for reader: what if we plugged in another instance of StakerInterface
            // that:
            // 1. switches to a new, unburned key
            // 2. pays 80% to the new StakerInterface
            // 3. pays 20% to an annuity that pays miners over e.g. 1000 blocks
            //    at some point in the far future.
            .add_output(f, &Compiled::from_op_return(&self.data.as_inner()[..])?, None)?
            .into()
    }
}

/// We can delcare the Contract impl for all valid Staker<T>
impl<T: 'static + StakingState> Contract for Staker<T>
where
    Staker<T>: StakerInterface,
    T: StakingState,
{
    declare! {then, Self::begin_redeem, Self::cheated}
    declare! {finish, Self::finish_redeem_key}
    declare! {non updatable}
}

Next, we’ll define the data required for our staker:

/// # Staker: A Bonded Signing Contract
/// Staker is a contract that proceeds from Operational -> Closing
/// During it's lifetime, many things can be signed with signing_key,
/// but should the key ever leak (e.g., via nonce reuse) the bonded
/// funds can be burned.
///
/// Burning is important v.s. miner fee because otherwise the staker
/// can bribe (or be a miner themselves) to cheat.
#[derive(JsonSchema, Deserialize)]
pub struct Staker<T: StakingState> {
    /// # Timeout
    /// How long to wait for evidence after closing
    timeout: AnyRelTimeLock,
    /// # Signing Key
    /// The key that if leaked can burn funds
    signing_key: PublicKey,
    /// # Redemption Key
    /// The key that will be used to control & return the redeemed funds
    redeeming_key: PublicKey,
    /// # Data
    /// Arbitrary hash of metadata that is needed to start the attestation chain
    data: sha256::Hash,
    /// current contract state.
    #[serde(skip, default)]
    state: PhantomData<T>,
}

Next, we’ll define the StakerInterface when our channel is operational. At this phase, funds can either be burnt or the redeeming key can start the process of withdrawing.

impl StakerInterface for Staker<Operational> {
    /// redeeming key
    #[guard]
    fn begin_redeem_key(self, _ctx: Context) {
        Clause::Key(self.redeeming_key)
    }
    /// begin redemption process
    #[then(guarded_by = "[Self::begin_redeem_key]")]
    fn begin_redeem(self, ctx: sapio::Context) {
        let f = ctx.funds();
        ctx.template()
            .add_output(
                f,
                &Staker::<Closing> {
                    state: Default::default(),
                    timeout: self.timeout,
                    signing_key: self.signing_key,
                    redeeming_key: self.redeeming_key,
                },
                None,
            )?
            .into()
    }
    /// staking key
    #[guard]
    fn staking_key(self, _ctx: Context) {
        Clause::Key(self.signing_key)
    }
}

Lastly, for closing we should not be able to “loop” back into Closing or Operational, so we do not implement the begin_redeem logic.

impl StakerInterface for Staker<Closing> {
    #[guard]
    fn finish_redeem_key(self, _ctx: Context) {
        Clause::And(vec![Clause::Key(self.redeeming_key), self.timeout.into()])
    }
    #[guard]
    fn staking_key(self, _ctx: Context) {
        Clause::Key(self.signing_key)
    }
}

Attestation Chain

In order to start the attestation chain, the data field should be the hash of something like:

struct AttestationStart {
    /// # Nonce
    /// a nonce element
    first_nonce: [0u8; 32],
    /// # Key
    /// the key to sign with (for convenience, should match the StakedSigner's
    /// staking key)
    key: PublicKey,
    /// # Purpose
    /// useful to have some sort of description (machine readable) of what this attestor
    /// is signing for
    purpose: Vec<u8>
}

To start using the attestation chain, we build a linked list of Attest signatures as described below:

enum Either<T, U> {
    Left(T),
    Right(U)
}
struct Attest {
    /// # Signature
    /// the signature over the below data fields
    sig: Signature,
    /// # Message
    /// whatever info the protocol expects to be signed
    message: Vec<u8>,
    /// # Nonce
    /// a nonce element
    next_nonce: [0u8; 32],
    /// # Height
    /// what # signature is this
    height: u64,
    /// # Previous Attestation
    /// the last attestation. we either keep a hash or the actual value
    prev: Either<Hash, Either<Box<Attest>, AttestationStart>>
}

It would be possible – but perhaps overkill – to instead encode this structure as a Sapio contract with continuation branches. I’ll leave that as an exercise for the reader for now!

Galaxy Brain Time

What if we used this staked signer to coordinate a decentralized mining pool where the stakers sign off on work shares they have seen…

That’s All Folks!


comments powered by Disqus