benny b's blog

5D34 2974 4CB5 F205 76ED A743 B518 AFC2 A836 3DD9

Introduction to DlcDevKit

tl;dr: dlcs are so back

When I was first exploring the DLC ecosystem I wanted an easy way to prototype my idea. There are excellent libraries for DLCs but the application tooling fell short to quickly prototype. I found this a hard stop for application developers. If someone new to bitcoin development were to start with DLCs they would be encumbered with Bitcoin implementation details before even starting their application. In part, this is why DLCs never got their chance to shine in the open market.

This lead me to create dlcdevkit (or ddk). An application development kit that manages contract creation, management, storage, message handling, and comes with a bdk wallet out of the box. Shifting the focus for application developers to focus on their ideas, not bitcoin internal complexities.

ddk is written in rust btw 🦀. It uses rust-dlc under the hood for contract management, execution, and creation of DLC transactions. rust-dlc lacks a direct wallet interface so ddk is packaged with bdk in a nice bow for you to create your next billion dollar idea.

Usage of ddk

Creating a fully features DLC application is straightforward. App developers just need to answer 3 questions.

ddk composes to three main traits that consumers will implements to handle those operations internally. If you are familiar with the project ldk-node then the ddk API will feel very familiar.

First a full example

use ddk::builder::Builder;
use ddk::storage::SledStorage;
use ddk::transport::lightning::LightningTransport; // with "lightning" feature
use ddk::oracle::KormirOracleClient;
use bitcoin::Network;
use std::sync::Arc;

#[tokio::main]
fn main() {
    let transport = Arc::new(LightningTransport::new([0u8;32], 9735, Network::Signet));
    let storage = Arc::new(SledStorage::new("/tmp/ddk")?);
    let oracle_client = Arc::new(KormirOracleClient::new("https://kormir.dlcdevkit.com").await?);

    let ddk: ApplicationDdk = Builder::new()
        .set_seed_bytes([0u8;32])
        .set_network(Network::Regtest)
        .set_esplora_path("http://mutinynet.com") // shouts out mutiny
        .set_transport(transport.clone())
        .set_storage(storage.clone())
        .set_oracle(oracle_client.clone())
        .finish()
        .expect("skill issue");

    ddk.start().expect("skill issue");
}

The builder takes in information like seed_bytes for the internal wallet, network to be on, esplora host, and then the consumer creates the necessary traits for transport, storage, and oracle. ddk have three example crates out of the box to quickly get started.

Transport Trait

DLC participants need some way to communicate with each other. Passing messages such as Offers, Accept, and Sign. Typically app developers would have to build and maintain this logic by themselves but ddk abstracts this with the Transport trait.

#[async_trait]
/// Allows ddk to open a listening connection and send/receive dlc messages functionality.
pub trait Transport: Send + Sync + 'static {
    /// Name for the transport service.
    fn name(&self) -> String;
    /// Open an incoming listener for DLC messages from peers.
    async fn listen(&self);
    /// Get messages that have not been processed yet.
    async fn receive_messages<S: Storage, O: Oracle>(
        &self,
        manager: Arc<DlcDevKitDlcManager<S, O>>,
    );
    /// Send a message to a specific counterparty.
    fn send_message(&self, counterparty: PublicKey, message: Message);
    /// Connect to another peer
    async fn connect_outbound(&self, pubkey: PublicKey, host: &str);
}

LightnigTransport example that is available

Consumers implement a listener for which ddk will listen on for incoming connections. Then a function for receving messages, this is passed to the manager to handle the corresponding DLC message. And then of course functions for sending messages and connecting to counterparties.

This opens up an opportunity for consumers to implement a transport listener for whatever platform they want to build on. You can create a nostr implementation for monile applications that have dunamic IP addresses. You canbuild a client/server model for passing messages between counterparties. Or you could use the lightning gossip layer!

Storage Trait

The storage trait is used for the storage of contracts, wallet changesets, and counterparty peer information. It is a super trait of the rust-dlc/dlc-manager trait. It is technically a super trait for bdk but there is a wrapper struct that is created because of the bdk::PersistedWallet trait bounds.

/// Storage for DLC contracts.
pub trait Storage: dlc_manager::Storage + Send + Sync + 'static {
    ///// Instantiate the storage for the BDK wallet.
    fn initialize_bdk(&self) -> Result<ChangeSet, WalletError>;
    /// Save changeset to the wallet storage.
    fn persist_bdk(&self, changeset: &ChangeSet) -> Result<(), WalletError>;
    /// Connected counterparties.
    fn list_peers(&self) -> anyhow::Result<Vec<PeerInformation>>;
    /// Persis counterparty.
    fn save_peer(&self, peer: PeerInformation) -> anyhow::Result<()>;

    // For another blog!
    // #[cfg(feature = "marketplace")]
    fn save_announcement(&self, announcement: OracleAnnouncement) -> anyhow::Result<()>;
    // #[cfg(feature = "marketplace")]
    fn get_marketplace_announcements(&self) -> anyhow::Result<Vec<OracleAnnouncement>>;
}

Oracle Trait

There is not much to the oracle client. Similar to the storage trait it is a super trait to rust-dlc/dlc-manager storage trait. All that is required is get_announcement() and get_attestation(). If consumers are interested in creating their own oracle implementation, I recommend using kormir.

/// Oracle client
pub trait Oracle: dlc_manager::Oracle + Send + Sync + 'static {
    fn name(&self) -> String;
}

How it glues together

Now with all of the traits implemented and built with the ddk::builder::Builder. Calling the start() method starts the listeners, processes dlc messages, checks contracts, and syncs the on-chain wallet.

pub fn start(&self) -> anyhow::Result<()> {
        let mut runtime_lock = self.runtime.write().unwrap();

        if runtime_lock.is_some() {
            return Err(anyhow!("DDK is still running."));
        }

        let runtime = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()?;

        let manager_clone = self.manager.clone();
        let receiver_clone = self.receiver.clone();
        runtime.spawn(async move { Self::run_manager(manager_clone, receiver_clone).await });

        let transport_clone = self.transport.clone();
        runtime.spawn(async move {
            transport_clone.listen().await;
        });

        let transport_clone = self.transport.clone();
        let manager_clone = self.manager.clone();
        runtime.spawn(async move {
            transport_clone.receive_messages(manager_clone).await;
        });

        let wallet_clone = self.wallet.clone();
        runtime.spawn(async move {
            let mut timer = tokio::time::interval(Duration::from_secs(10));
            loop {
                timer.tick().await;
                wallet_clone.sync().unwrap();
            }
        });

        let processor = self.sender.clone();
        runtime.spawn(async move {
            let mut timer = tokio::time::interval(Duration::from_secs(5));
            loop {
                timer.tick().await;
                processor
                    .send(DlcManagerMessage::PeriodicCheck)
                    .expect("couldn't send periodic check");
            }
        });

        *runtime_lock = Some(runtime);

        Ok(())
    }

Flexibility

Like I mentioned before, ddk is flexible for whatever platform you are building your app on. Want to use nostr for transport? Create a nostr transport! Create a relational database with sql or go document based with mongo. You can create a gRPC server or a REST service.

ddk-node

Don't want to code and start using DLCs? Check out ddk-node. A pre-built node and cli using the pre-build components maintained in dlcdevkkit.

ddk-node cli example

Installation

Start building with dlcdevkit today!

$ cargo add ddk

Start executing DLCs today!

$ cargo install ddk-node

Currently, ddk requires an async runtime which is not merged yet with rust-dlc so you may have to install or add with the github link. But there is a passing PR

$ cargo add --git https://github.com/bennyhodl/dlcdevkit.git ddk

$ cargo install --git https://github.com/bennyhodl/dlcdevkit.git ddk-node

Contribute to ddk

I am looking for contributers and users of dlcdevkit. I extend an invitation to participate.

Star the project on GitHub Read the documentation Follow me on twitter