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:
| File | Description |
|---|---|
config.json | Airdrop configuration |
setup-sapling-pk.params | Sapling proving key |
setup-sapling-vk.params | Sapling verifying key |
setup-orchard-params.bin | Orchard proving parameters |
snapshot-sapling.bin | Sapling snapshot nullifiers |
snapshot-orchard.bin | Orchard 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
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/
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
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(())
}
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:
- Airdrop Nullifiers
- Message Targets
- Message Signature
- Value Commitment
- ZK Proof