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
- Implementation
- Verification
- Plain Implementation
- SHA256 Implementation
- Summary
- References
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 |
gaptree-sapling.bin | Sapling gap tree |
gaptree-orchard.bin | Orchard 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
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_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
| Parameter | Required | Description |
|---|---|---|
--source | Yes | The claiming address |
--seed | Yes | 64-byte seed as hex |
--account-id | Yes | ZIP-32 account index for deriving keys |
--birthday | Yes | Shielded transaction scan start height |
--sapling-snapshot | Conditional | Sapling nullifier snapshot (required for Sapling pools) |
--orchard-snapshot | Conditional | Orchard nullifier snapshot (required for Orchard pools) |
--sapling-gap-tree | Optional | Sapling gap tree for faster claiming |
--orchard-gap-tree | Optional | Orchard gap tree for faster claiming |
--lightwalletd-url | Optional | lightwalletd gRPC endpoint |
--gas-limit | Yes | Gas 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
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(())
}
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:
- Airdrop Nullifiers
- Message Targets
- Message Signature
- Value Commitment (Plain or SHA256)
- ZK Proof