Designing `bdk_chain` of BDK v1.0

@evanlinjin

Whoami

  • Evan Lin
  • Started contributing to BDK (Bitcoin Dev Kit) in 2022
  • Implemented a significant portion of the BDK v1.0 refactor

Agenda

  • Brief intro to Bitcoin wallets
  • Brief intro to BDK
  • BDK v1.0 crates
  • Design choices of `bdk_chain`
    • Non-ambiguous representation
    • Monotone data structures
    • Always consistent

On-Chain Wallets

What do they do?

On-chain wallets...

  • Manages keychains
    • Keychains are deterministic chains of spks
    • BIP-32: Deterministic trees of keypairs
    • Bitcoin scripts are expressive
  • Monitors the blockchain
    • Fetch relevant transactions only
    • Block-by-block chain sources
    • ScriptPubKey chain sources
  • Creates and sends transactions
    • Determine witness data
    • Coin selection

BDK (Bitcoin Dev Kit)

How does it make your life easier?

BDK

  • Wallet library written in Rust
  • Tools (crates, structs, methods, traits) that...
    • Manages keychains
    • Monitors the blockchain
    • Creates and sends transactions

BDK is modular

  • Core crates...
    • `bdk_chain`: Structures to index and store chain data
    • `bdk_coin_select`: 0-dependency crate for coin selection
  • Chain-source crates...
    • `bdk_electrum`, `bdk_esplora`: spk-based
    • `bdk_bitcoind_rpc`: block-based
  • Easy-to-use crates...
    • `bdk_wallet`

`bdk_chain`

Some design choices

Ambiguous Representation is Bad

Example of ambiguous representation:
blockchain.scripthash.get_history(scripthash)

[
  { "height": 200004, "tx_hash": "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" },
  { "height": 215008, "tx_hash": "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" }
]

Ambiguous Representation is Bad

get_history(scripthash) -> [ { confirmation_height, txid }, ... ]

Why is this bad?

  • Reorgs happen: same height, different block
  • Wallets track multiple spks
  • Inconsistent history

Non-Ambiguous Representation is Good

In bdk_chain, you need to "anchor" a transaction to a block_height AND block_hash.

pub struct BlockId { pub height: u32, pub hash: BlockHash }

pub trait Anchor {
    fn anchor_block(&self) -> BlockId;
    /* Hidden methods */
}

Does this mean BDK is incompatible with Electrum?

No.

Consistent Transaction Histories from Electrum?

  • Method 1
    Get chain tip before and after doing all `get_history` calls.

     
  • Method 2
    Use `blockchain.transaction.get_merkle` against fetched transactions.
  • For 1, we need a way to know if the tip is still in the best chain.
  • For 2, we need to know whether confirmation blocks are still in the best chain.
  • We need a way to know which blocks are in the best chain.

crate `bdk_electrum`

`bdk_chain::ChainOracle`

Is the transaction anchored to a block that belongs in the chain identified by `chain_tip`?

pub trait ChainOracle {
    fn is_block_in_chain(
        &self,
        block: BlockId,
        chain_tip: BlockId,
    ) -> Result<Option<bool>, Self::Error>;

    /* ... */
}

pub struct BlockId { pub height: u32, pub hash: BlockHash }

Monotone Data Structures

  • Updates can be applied in any order (to the structure) and the resultant state will be the same.
  • No such thing as invalid representation, elements/contained data cannot conflict.
  • Very good for async.
  • Easy to work with, hard to screw up.

`bdk_chain::TxGraph`

Example of a monotone data structure.

  • Fancy set of `bitcoin::Transaction`s
  • Each transaction can have multiple `Anchor`s
  • Each transaction has a `last_seen` (in mempool) timestamp
pub struct TxGraph<A> {
    txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>, u64)>,
    spends: BTreeMap<OutPoint, HashSet<Txid>>,
    anchors: BTreeSet<(A, Txid)>,
}

Putting it all together

Extrapolating a consistent history of transactions

impl<A: Anchor> TxGraph<A> {
    pub fn list_chain_txs<'a, C: ChainOracle + 'a>(
        &'a self,
        chain: &'a C,
        chain_tip: BlockId,
    ) -> impl Iterator<Item = CanonicalTx<'a, Transaction, A>> { /* … */ }
}

How `TxGraph::list_chain_txs` works

For each transaction Tn:

  • If one of Tn’s anchor points to a block in chain (identified by `chain_tip`), then Tn is confirmed in this chain.
  • If Tn conflicts with a confirmed transaction, Tn cannot exist in this chain.
  • If Tn conflicts with any unconfirmed transaction(s), Tn can only be in the best chain if it’s last_seen value is greater than all conflicts (tie-break by txid).
pub fn list_chain_txs(&self, chain: &impl ChainOracle, chain_tip: BlockId) -> impl Iterator<Item = CanonicalTx> { /* … */ }

Summary

  • `TxGraph` is a set of transactions, `Anchor`s and last_seen timestamps.
  • `TxGraph` is monotone and can be updated in any order. No invalid representation.
  • `ChainOracle` gives us a view of the chain identified with `chain_tip` (blockhash+height).
  • We can always get a consistent view of transactions using `TxGraph::list_chain_txs`.

BDK is

  • Modular
  • Extendable
  • Consistent

Join us on Discord