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: 463f11c5f).

Table of Contents

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
gaptree-sapling.binSapling gap tree
gaptree-orchard.binOrchard gap tree

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_PATH> \
  --account-id <ACCOUNT_ID> \
  --birthday <BIRTHDAY> \
  --sapling-snapshot <SAPLING_SNAPSHOT> \
  --orchard-snapshot <ORCHARD_SNAPSHOT> \
  --sapling-gap-tree <SAPLING_GAPTREE> \
  --orchard-gap-tree <ORCHARD_GAPTREE> \
  --gas-limit 300000

CLI Parameters

ParameterRequiredDescription
--sourceYesThe claiming address
--seedYes64-byte seed as hex
--account-idYesZIP-32 account index for deriving keys
--birthdayYesShielded transaction scan start height
--sapling-snapshotConditionalSapling nullifier snapshot (required for Sapling pools)
--orchard-snapshotConditionalOrchard nullifier snapshot (required for Orchard pools)
--sapling-gap-treeOptionalSapling gap tree for faster claiming
--orchard-gap-treeOptionalOrchard gap tree for faster claiming
--lightwalletd-urlOptionallightwalletd gRPC endpoint
--gas-limitYesGas limit for the transaction

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. The message format differs between the Plain and SHA256 implementations - see the respective sections below.

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.

Signature Verification

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.value,
        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.

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:

Plain Implementation

This section describes the Plain value commitment implementation. The message structure is simpler as it does not require value commitment randomness.

Plain Message

pub struct Message {
    /// The target of the airdrop.
    pub target: Address,
    /// Amount to claim.
    pub amount: u64,
}

Plain Value Commitment

For the plain value commitment scheme we simply compare the proof value directly against the message amount:

/// Checks that the plain value commitment is valid by comparing the proof
/// value directly with the message amount.
fn check_plain_value_commitment(
    value: u64,
    Message { amount, .. }: &Message,
) -> Result<()> {
    if value != *amount {
        return Err(VpError::ValueCommitmentMismatch.into());
    }

    Ok(())
}

See Value Commitments for more details.

SHA256 Implementation

This section describes the SHA256 value commitment implementation. The message includes value commitment randomness (rcv) which is used in the commitment computation.

SHA256 Message

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

SHA256 Value Commitment

For the SHA256 value commitment scheme we extract amount and rcv from the Namada message and compute the value commitment, asserting 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.

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 (Plain or SHA256)
  5. ZK Proof

References