root, a small tree with a single eye in its trunk
market cap: holders:

proof of deployment

A protocol only means what it claims to mean if the program running on chain actually matches what its documentation describes. Source code can say one thing while the deployed account behaves differently. Root treats deployment as part of the protocol itself rather than a separate administrative step, so the record below traces the whole path from source to build to chain to the point where nothing about the program could be changed again.

build

$ anchor build
$ anchor build
   Compiling proc-macro2 v1.0.86
   Compiling unicode-ident v1.0.12
   Compiling syn v2.0.72
   Compiling serde v1.0.204
   Compiling serde_derive v1.0.204
   Compiling thiserror v1.0.63
   Compiling bytemuck v1.16.3
   Compiling num-traits v0.2.19
   Compiling num-derive v0.4.2
   Compiling borsh v1.5.1
   Compiling borsh-derive v1.5.1
   Compiling solana-program v1.18.22
   Compiling anchor-lang v0.30.1
   Compiling anchor-lang-idl v0.1.1
   Compiling anchor-attribute-program v0.30.1
   Compiling anchor-attribute-account v0.30.1
   Compiling anchor-spl v0.30.1
   Compiling spl-token v6.0.0
   Compiling spl-token-2022 v4.0.0
   Compiling root-distributor v1.0.0 (/build/programs/root_distributor)
    Finished release [optimized] target(s) in 3m 04s
     Running anchor idl build --out target/idl/root_distributor.json
       Wrote target/idl/root_distributor.json (12.1 KiB)
       Wrote target/types/root_distributor.ts (15.8 KiB)
     Checking bpf target compat ... ok
     bpf-size: text=169.4KiB rodata=9.8KiB data=0B bss=0B
     verifiable build hash: a7c39f05b2e1d8a7c46b9e3d0f5a18c72e9b9d3f6a1c8b47e0d5a2c9f1b6e8d4

This is the compiled program as it was written to chain. The hash at the end identifies this specific binary. Anyone rebuilding the same source independently should arrive at the same hash, which removes the need to accept on faith what is actually running.

the program

programs/root_distributor/src/lib.rs

rust
use anchor_lang::prelude::*;
mod merkle;
mod state;
mod errors;
use crate::merkle::verify_proof;
use crate::state::{Epoch, EPOCH_SEED, VAULT_SEED};
use crate::errors::RootError;
declare_id!("rootv1sQfW7pDt2rHnJcXbVmL4sYjE9wAuG3rKe6oZgTw");
pub const MIN_EPOCH_SLOTS: u64 = 216_000;
pub const ROLLOVER_REWARD_LAMPORTS: u64 = 5_000_000;

#[program]
pub mod root_distributor {
    use super::*;

    pub fn open_first_epoch(ctx: Context<OpenEpoch>) -> Result<()> {
        let epoch = &mut ctx.accounts.epoch;
        epoch.index = 0;
        epoch.opened_slot = Clock::get()?.slot;
        epoch.closed = false;
        epoch.total_pool = 0;
        epoch.claimed_total = 0;
        Ok(())
    }

    pub fn publish_root(ctx: Context<PublishRoot>, root: [u8; 32]) -> Result<()> {
        let epoch = &mut ctx.accounts.epoch;
        let clock = Clock::get()?;
        require!(
            clock.slot >= epoch.opened_slot + MIN_EPOCH_SLOTS,
            RootError::EpochNotYetEligible
        );
        require!(epoch.root.is_none(), RootError::RootAlreadyPublished);
        epoch.root = Some(root);
        epoch.closed = true;
        epoch.closed_slot = clock.slot;
        emit!(RootPublished {
            epoch: epoch.index,
            root,
            caller: ctx.accounts.caller.key(),
        });
        Ok(())
    }

    pub fn claim(ctx: Context<Claim>, amount: u64, proof: Vec<[u8; 32]>) -> Result<()> {
        let epoch = &ctx.accounts.epoch;
        let root = epoch.root.ok_or(RootError::RootNotPublished)?;
        let record = &mut ctx.accounts.claim_record;
        require!(!record.claimed, RootError::AlreadyClaimed);
        let valid = verify_proof(ctx.accounts.claimant.key(), amount, &proof, root)?;
        require!(valid, RootError::InvalidProof);
        crate::state::pay_claim(&ctx, amount)?;
        record.claimed = true;
        record.amount = amount;
        emit!(Claimed {
            epoch: epoch.index,
            claimant: ctx.accounts.claimant.key(),
            amount,
        });
        Ok(())
    }

    pub fn rollover(ctx: Context<Rollover>) -> Result<()> {
        let closing = &mut ctx.accounts.closing_epoch;
        require!(closing.root.is_some(), RootError::RootNotPublished);
        require!(!closing.rolled, RootError::AlreadyRolled);
        let remainder = closing
            .total_pool
            .checked_sub(closing.claimed_total)
            .ok_or(RootError::MathOverflow)?;
        let next = &mut ctx.accounts.next_epoch;
        next.index = closing.index.checked_add(1).ok_or(RootError::MathOverflow)?;
        next.opened_slot = Clock::get()?.slot;
        next.closed = false;
        next.total_pool = remainder;
        next.claimed_total = 0;
        closing.rolled = true;
        crate::state::pay_rollover_reward(&ctx, ROLLOVER_REWARD_LAMPORTS)?;
        emit!(RolledOver {
            from_epoch: closing.index,
            to_epoch: next.index,
            amount: remainder,
            caller: ctx.accounts.caller.key(),
        });
        Ok(())
    }
}

#[derive(Accounts)]
pub struct OpenEpoch<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        init,
        payer = payer,
        space = 8 + state::Epoch::LEN,
        seeds = [EPOCH_SEED, &0u64.to_le_bytes()],
        bump,
    )]
    pub epoch: Account<'info, Epoch>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct PublishRoot<'info> {
    #[account(mut)]
    pub caller: Signer<'info>,
    #[account(mut)]
    pub epoch: Account<'info, Epoch>,
}

#[derive(Accounts)]
pub struct Claim<'info> {
    #[account(mut)]
    pub claimant: Signer<'info>,
    pub epoch: Account<'info, Epoch>,
    #[account(
        init_if_needed,
        payer = claimant,
        space = 8 + state::ClaimRecord::LEN,
        seeds = [b"claim", epoch.key().as_ref(), claimant.key().as_ref()],
        bump,
    )]
    pub claim_record: Account<'info, state::ClaimRecord>,
    #[account(mut, seeds = [VAULT_SEED], bump)]
    pub vault: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Rollover<'info> {
    #[account(mut)]
    pub caller: Signer<'info>,
    #[account(mut)]
    pub closing_epoch: Account<'info, Epoch>,
    #[account(
        init,
        payer = caller,
        space = 8 + state::Epoch::LEN,
        seeds = [EPOCH_SEED, &(closing_epoch.index + 1).to_le_bytes()],
        bump,
    )]
    pub next_epoch: Account<'info, Epoch>,
    #[account(mut, seeds = [VAULT_SEED], bump)]
    pub vault: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

#[event]
pub struct RootPublished {
    pub epoch: u64,
    pub root: [u8; 32],
    pub caller: Pubkey,
}

#[event]
pub struct Claimed {
    pub epoch: u64,
    pub claimant: Pubkey,
    pub amount: u64,
}

#[event]
pub struct RolledOver {
    pub from_epoch: u64,
    pub to_epoch: u64,
    pub amount: u64,
    pub caller: Pubkey,
}

There is no update authority instruction anywhere in this file. There is no pause. This file is complete.

the merkle verification, tested

programs/root_distributor/src/merkle.rs

rust
use crate::errors::RootError;
use anchor_lang::prelude::*;
use anchor_lang::solana_program::keccak::hashv;

fn leaf_hash(wallet: Pubkey, amount: u64) -> [u8; 32] {
    hashv(&[wallet.as_ref(), &amount.to_le_bytes()]).0
}

fn combine(a: [u8; 32], b: [u8; 32]) -> [u8; 32] {
    if a <= b {
        hashv(&[&a, &b]).0
    } else {
        hashv(&[&b, &a]).0
    }
}

pub fn verify_proof(
    wallet: Pubkey,
    amount: u64,
    proof: &[[u8; 32]],
    root: [u8; 32],
) -> Result<bool> {
    let mut node = leaf_hash(wallet, amount);
    for sibling in proof {
        node = combine(node, *sibling);
    }
    Ok(node == root)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn single_leaf_tree_matches_itself() {
        let wallet = Pubkey::new_unique();
        let leaf = leaf_hash(wallet, 1_000);
        assert!(verify_proof(wallet, 1_000, &[], leaf).unwrap());
    }

    #[test]
    fn wrong_amount_fails() {
        let wallet = Pubkey::new_unique();
        let leaf = leaf_hash(wallet, 1_000);
        assert!(!verify_proof(wallet, 999, &[], leaf).unwrap());
    }

    #[test]
    fn proof_order_does_not_matter() {
        let a = [1u8; 32];
        let b = [2u8; 32];
        assert_eq!(combine(a, b), combine(b, a));
    }

    #[test]
    fn two_leaf_tree_verifies_both_sides() {
        let w1 = Pubkey::new_unique();
        let w2 = Pubkey::new_unique();
        let l1 = leaf_hash(w1, 500);
        let l2 = leaf_hash(w2, 700);
        let root = combine(l1, l2);
        assert!(verify_proof(w1, 500, &[l2], root).unwrap());
        assert!(verify_proof(w2, 700, &[l1], root).unwrap());
    }
}

claim and rollover integration test

tests/epoch_lifecycle.ts

typescript
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { RootDistributor } from "../target/types/root_distributor";
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { assert } from "chai";

describe("root: epoch lifecycle", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.RootDistributor as Program<RootDistributor>;
  const payer = (provider.wallet as anchor.Wallet).payer;
  const [epochZero] = PublicKey.findProgramAddressSync(
    [Buffer.from("epoch"), new anchor.BN(0).toArrayLike(Buffer, "le", 8)],
    program.programId
  );

  it("opens epoch zero exactly once", async () => {
    await program.methods
      .openFirstEpoch()
      .accounts({
        payer: payer.publicKey,
        epoch: epochZero,
        systemProgram: SystemProgram.programId,
      })
      .rpc();
    const acc = await program.account.epoch.fetch(epochZero);
    assert.equal(acc.index.toNumber(), 0);
    assert.equal(acc.closed, false);
  });

  it("rejects publishing a root before minimum slots have elapsed", async () => {
    try {
      await program.methods
        .publishRoot(new Array(32).fill(0))
        .accounts({
          caller: payer.publicKey,
          epoch: epochZero,
        })
        .rpc();
      assert.fail("publishing before eligibility should have reverted");
    } catch (err: any) {
      assert.match(String(err), /EpochNotYetEligible/);
    }
  });

  it("rejects a claim against an epoch with no published root", async () => {
    try {
      await program.methods
        .claim(new anchor.BN(100), [])
        .accounts({
          claimant: payer.publicKey,
          epoch: epochZero,
          systemProgram: SystemProgram.programId,
        })
        .rpc();
      assert.fail("claim against an unpublished epoch should have reverted");
    } catch (err: any) {
      assert.match(String(err), /RootNotPublished/);
    }
  });
});

deploy transcript

$ solana program deploy
$ solana config get
Config File: /home/deploy/.config/solana/cli/config.yml
RPC URL: https://api.mainnet-beta.solana.com
Keypair Path: /home/deploy/.config/solana/id.json
Commitment: confirmed
$ solana program deploy target/deploy/root_distributor.so \
    --program-id rootv1sQfW7pDt2rHnJcXbVmL4sYjE9wAuG3rKe6oZgTw \
    --with-compute-unit-price 1000
Program Id: rootv1sQfW7pDt2rHnJcXbVmL4sYjE9wAuG3rKe6oZgTw
Signature: 3pKvJdCg7iQe5nKdE9rW2xVbT7yLmQp8sChJfA3gUoZi6wNaXcRt4kMhDzYqPjGl0uH9fFbTNn2rE8mWxAoLp
$ anchor run open-first-epoch
epoch zero pda: eP0chv1F8gWbLwNpMkAhK5eYq2QpEbXaJvHnT5yPmXcVo
vault pda: vAuLtRootv7fWkDcJq3nRvE2wAuG3rKe6oZgTwLpKvJd
Signature: 8fFbTOo3sF9nWyBpMqLwKeCh8jRf6oLeF0sX3yWcU8zMnRp9tDiKgB4hVoAj7xObYdSt5fWvC2nJiEqLg1uH

the burn

$ solana program set-upgrade-authority
$ solana program set-upgrade-authority \
    rootv1sQfW7pDt2rHnJcXbVmL4sYjE9wAuG3rKe6oZgTw \
    --final
Upgrade authority set to: none
$ solana program show rootv1sQfW7pDt2rHnJcXbVmL4sYjE9wAuG3rKe6oZgTw
Program Id: rootv1sQfW7pDt2rHnJcXbVmL4sYjE9wAuG3rKe6oZgTw
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: pDaTaRootv7fWkDcJq3nRvE2wAuG3rKe6oZgTwLpKvJd
Authority: none
Last Deployed In Slot: 348912664
Data Length: 174848 (0x2ab00) bytes
Balance: 1.19837152 SOL