Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Integration: Namada

This page describes an example integration of ZAIR in Namada. You can find the latest source code hosted at GitHub (latest commit at the time of writing: 53f8a9c).

Getting Started

This section walks through a minimal end-to-end workflow: building an airdrop configuration, spinning up a local Namada chain, and claiming an airdrop.

1. Preparing a ZAIR Airdrop Configuration

Generate the proving parameters and airdrop config:

zair setup sapling --scheme sha256
zair setup orchard --scheme sha256
zair config build --network testnet --height 3663119

Then create an airdrop directory containing all the generated artifacts:

mkdir airdrop
mv config.json *.params *.bin airdrop/

The airdrop directory should contain the following files:

FileDescription
config.jsonAirdrop configuration
setup-sapling-pk.paramsSapling proving key
setup-sapling-vk.paramsSapling verifying key
setup-orchard-params.binOrchard proving parameters
snapshot-sapling.binSapling snapshot nullifiers
snapshot-orchard.binOrchard snapshot nullifiers

See zair config and zair setup for the full flag references.

2. Setting Up a Local Namada Chain

Compile the Namada executable:

make build

Compile WASM transactions and initialize a local test chain:

cd wasm && make all && cd ..
python3 ./scripts/gen_checksums.py
python3 ./scripts/gen_localnet.py -m release

Warning

If you encounter a compilation error with nam-blst, try building with CC=clang instead.

3. Copying Airdrop Configuration to Chain Config

Copy the generated airdrop directory into the chain's validator base directory:

# Note the chain ID starting with "local." from the output below
ls .namada/validator-0/
cp -r airdrop/ .namada/validator-0/<CHAIN_ID>/airdrop/

Note

This step initializes the airdrop state in genesis and must be completed before starting the chain.

4. Starting the Chain

Start the validator:

namada ledger run --base-dir .namada/validator-0

5. Claiming an Airdrop

With the chain running, submit a claim transaction using the desired account address:

namada client claim-airdrop \
  --base-dir .namada/validator-0 \
  --source <ADDRESS> \
  --seed-file-path <SEED_FILE> \
  --birthday <BIRTHDAY> \
  --gas-limit 300000

You can verify the claim by querying the account balance before and after:

namada client balance --base-dir .namada/validator-0 --owner <ADDRESS> --token NAM

Note

Use namada wallet list --base-dir .namada/validator-0 to list available account addresses.

See zair claim for more details on the claim pipeline.

Implementation

This section describes the Namada ZAIR implementation.

This integration adds ZAIR airdrop claiming to Namada via a custom ClaimAirdrop transaction. The AirdropVP validity predicate verifies each claim by checking nullifiers, signatures, value commitments, and zero-knowledge proofs.

Transactions

We introduce a new transaction type, ClaimAirdrop, with the following signature:

pub struct ClaimAirdrop {
    /// Token address to claim.
    pub token: Address,
    /// The target of the airdrop.
    pub target: Address,
    /// Claim data containing zk proof information.
    pub claim_data: ClaimProofsOutput,
}

The transaction can be submitted either via the existing CLI or the SDK.

Validity Predicate

Upon execution the transaction triggers a new custom validity predicate, AirdropVP, that runs a series of checks verifying the ZAIR airdrop. The steps to verify a ZAIR claim are outlined in the Verification section.

Storage

To support ZAIR integration, we extend Namada's base storage with new keys for storing necessary ZAIR data: Sapling verification keys/Orchard parameters, note commitment roots, nullifier gap roots, target IDs and airdrop nullifiers.

Verification

This section details how ZAIR claim submissions are verified inside the validity predicate.

Airdrop Nullifiers

To prevent double-claiming airdrop nullifiers must be correctly tracked and deduplicated. For Namada, we introduce a new storage key and functions to manipulate the airdrop nullifier storage. Finally, we add additional checks inside the validity predicate asserting that airdrop nullifiers for a given action have not already been claimed, are unique, and flushed to the store correctly.

/// Checks if airdrop nullifiers have already been used.
fn check_airdrop_nullifiers<'ctx, CTX>(
    ctx: &'ctx CTX,
    claim_data: &ClaimProofsOutput,
    revealed_nullifiers: &mut HashSet<Key>,
) -> Result<()>
where
    CTX: VpEnv<'ctx> + namada_tx::action::Read<Err = Error>,
{
    for nullifier in claim_data.nullifier_iter() {
        let airdrop_nullifier_key = airdrop_nullifier_key(nullifier);

        // Check if nullifier has already been used before.
        if ctx.has_key_pre(&airdrop_nullifier_key)? {
            return Err(VpError::NullifierAlreadyUsed(reversed_hex_encode(
                nullifier,
            ))
            .into());
        }

        // Check if nullifier was previously used in this transaction.
        if revealed_nullifiers.contains(&airdrop_nullifier_key) {
            return Err(VpError::NullifierAlreadyUsed(reversed_hex_encode(
                nullifier,
            ))
            .into());
        }

        // Check that the nullifier was properly committed to store.
        ctx.read_bytes_post(&airdrop_nullifier_key)?
            .is_some_and(|value| value.is_empty())
            .then_some(())
            .ok_or(VpError::NullifierNotCommitted)?;

        revealed_nullifiers.insert(airdrop_nullifier_key);
    }

    Ok(())
}

See Airdrop Nullifiers for more details.

Message

ZAIR supports a standard signature scheme over implementation-specific binary-encoded messages. For Namada we demonstrate an example of this by defining a custom Message used to verify claim submissions:

pub struct Message {
    /// The target of the airdrop.
    pub target: Address,
    /// Amount to claim.
    pub amount: u64,
    /// Commitment value randomness.
    pub rcv: [u8; 32],
}

A claimant provides their binary-encoded message along with their proofs to ZAIR and signs the message to generate a standard signature cryptographically linking the message to the hash of the proof. The signature proves the claimant controls the private spending key associated with the proof.

To verify the validity of the signature we first compute the message hash and the proof hash. Then, using ZAIR's public API we compute a signature digest and verify it:

/// Verifies that the Sapling spend-auth signature is valid.
fn verify_signature(
    target_id: &[u8],
    proof: &SaplingSignedClaim,
    message_hash: &[u8; 32],
) -> Result<()> {
    let proof_hash = hash_sapling_proof_fields(
        &proof.zkproof,
        &proof.rk,
        proof.cv,
        proof.cv_sha256,
        proof.airdrop_nullifier.into(),
    );

    let digest =
        signature_digest(Pool::Sapling, target_id, &proof_hash, message_hash)
            .map_err(|_| VpError::InvalidSpendAuthSignature)?;
    zair_sapling_proofs::verify_signature(
        proof.rk,
        proof.spend_auth_sig,
        &digest,
    )
    .map_err(|_| VpError::InvalidSpendAuthSignature)?;

    Ok(())
}

Note

The signature uses both a target id and a separate pool identifier.

Value Commitment

For the value commitment scheme we choose SHA256. Extracting amount and rcv from the Namada message we compute the value commitment and assert that it's equal to the signed one:

/// Checks that the SHA256 value commitment is valid.
///
/// This computes that `cv = SHA256(b'Zair || LE64(amount) || rcv)`.
fn check_sha256_value_commitment(
    cv: &[u8; 32],
    Message { amount, rcv, .. }: &Message,
) -> Result<()> {
    let computed_cv = compute_cv_sha256(*amount, *rcv);
    if computed_cv != *cv {
        return Err(VpError::ValueCommitmentMismatch.into());
    }

    Ok(())
}

See Value Commitments for more details.

Proof Verification

Finally, the zero-knowledge proof is verified using ZAIR's public standard verifier API. If any check fails, the validity predicate rejects the transaction.

For more details on zero-knowledge proof verification, see the corresponding proof sections for:

Summary

On success, the claimed tokens are credited to the target address and the airdrop nullifier is recorded to prevent double-claiming. On failure, the transaction is rejected and no state changes occur.

The verification flow runs in this order:

  1. Airdrop Nullifiers
  2. Message Targets
  3. Message Signature
  4. Value Commitment
  5. ZK Proof