pkarr

Pkarr Quickstart

Get up and running with Pkarr in 5 minutes.

Add to Project

[dependencies]
pkarr = "7"
tokio = { version = "1", features = ["full"] }

Generate a Keypair

use pkarr::Keypair;

let keypair = Keypair::random();

// Get the public key as a z-base32 string (this is your "domain name")
let public_key_string = keypair.public_key().to_string();
println!("Public key: {}", public_key_string);

Create and Sign DNS Records

Use SignedPacket::builder() to create DNS records and sign them with your keypair.

use pkarr::{Keypair, SignedPacket};

let keypair = Keypair::random();

let signed_packet = SignedPacket::builder()
    // A record (IPv4 address)
    .a(
        "www".try_into().unwrap(),
        "93.184.216.34".parse().unwrap(),
        3600,
    )
    // TXT record
    .txt(
        "_foo".try_into().unwrap(),
        "bar".try_into().unwrap(),
        30,
    )
    // Sign with your keypair
    .sign(&keypair)
    .unwrap();

Record Types

The builder supports these common record types:

Use . as the name to create records at the apex (the public key itself).

Publish

use pkarr::{Client, Keypair, SignedPacket};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder().build()?;
    let keypair = Keypair::random();

    let signed_packet = SignedPacket::builder()
        .txt("_foo".try_into().unwrap(), "bar".try_into().unwrap(), 30)
        .sign(&keypair)?;

    println!("Publishing {} ...", keypair.public_key());

    let stored_on = client.publish(&signed_packet).await?;

    println!("Published successfully; stored on at least {stored_on} DHT nodes");
    Ok(())
}

Publishing sends your signed packet to the Mainline DHT and configured relays. The returned stored_on value means the packet was stored on at least that many DHT nodes. When several publishing backends are configured, the client uses the maximum count reported by any successful backend, not the sum, because different backends may store the packet on the same DHT nodes.

Resolve

use pkarr::{Client, PublicKey, ResolvePolicy};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder().build()?;

    // Parse public key from z-base32 string
    let public_key: PublicKey = "yqrx81zchh6aotjj85s96gdqbmsoprxr3ks6ks6y8eccpj8b7oiy"
        .try_into()
        .expect("Invalid public key");

    match client.resolve(&public_key, ResolvePolicy::CacheFirst).await {
        Ok(signed_packet) => {
            println!("Resolved packet:");
            println!("{}", signed_packet);

            // Iterate over specific records
            for record in signed_packet.resource_records("_foo") {
                println!("Record: {:?}", record.rdata);
            }
        }
        Err(pkarr::errors::ResolveError::NotFound) => {
            println!("No packet found for {public_key}");
        }
        Err(pkarr::errors::ResolveError::InvalidSignedPacket { seq }) => {
            eprintln!("The network contains an invalid signed packet at sequence {seq}");
        }
        Err(error) => {
            eprintln!("Resolve failed: {error}");
        }
    }

    Ok(())
}

Use resolve(&public_key, ResolvePolicy::NetworkOnly) when you need the latest network state, for example before publishing updates. A newer mutable item that is not a valid PKARR packet is returned as ResolveError::InvalidSignedPacket, not ResolveError::NotFound.

ResolvePolicy::CacheFirst only returns non-expired packets. Use ResolvePolicy::CacheOnly when expired packets are acceptable. CacheOnly checks the local cache first and may then query configured relay caches, but it never queries DHT nodes.

Complete Example

A copy-paste example that generates a keypair, publishes a record, resolves it, and prints the result.

use pkarr::{Client, Keypair, ResolvePolicy, SignedPacket};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Disable caching so resolving reads from the configured networks
    let client = Client::builder().cache_size(0).build()?;
    let keypair = Keypair::random();

    println!("Generated keypair with public key: {}", keypair.public_key());

    // 2. Create a signed packet with an A record
    let signed_packet = SignedPacket::builder()
        .a(
            "www".try_into().unwrap(),
            "93.184.216.34".parse().unwrap(),
            3600,
        )
        .txt(
            "_hello".try_into().unwrap(),
            "world".try_into().unwrap(),
            30,
        )
        .sign(&keypair)?;

    // 3. Publish to DHT and relays
    println!("Publishing...");
    let stored_on = client.publish(&signed_packet).await?;
    println!("Published successfully; stored on at least {stored_on} DHT nodes");

    // 4. Resolve it from the configured networks, bypassing local caches
    println!("Resolving...");
    match client
        .resolve(&keypair.public_key(), ResolvePolicy::NetworkOnly)
        .await
    {
        Ok(resolved) => {
            // 5. Print results
            println!("\nResolved packet:\n{}", resolved);
        }
        Err(pkarr::errors::ResolveError::NotFound) => {
            println!("No packet found on the configured networks");
        }
        Err(pkarr::errors::ResolveError::InvalidSignedPacket { seq }) => {
            eprintln!("The network contains an invalid signed packet at sequence {seq}");
        }
        Err(error) => {
            eprintln!("Resolve failed: {error}");
        }
    }

    Ok(())
}

Expected output:

Generated keypair with public key: <52-character z-base32 string>
Publishing...
Published successfully; stored on at least <count> DHT nodes
Resolving...

Resolved packet:
SignedPacket (<public_key>):
    last_seen: 0 seconds ago
    timestamp: <timestamp> <HTTP date>,
    signature: <signature>
    records:
        www.<public_key>  IN  3600  A  93.184.216.34
        _hello.<public_key>  IN  30  TXT  "world"

Next Steps