Merkle Challenges
Overview
Merkle challenges provide cryptographic proof-based approval mechanisms using SHA256 Merkle trees. They enable secure, gas-efficient whitelisting and claim code systems without storing large address lists on-chain.
Key Benefits:
Gas Efficiency: Distribute gas costs among users instead of collection creators
Security: Cryptographic proof verification prevents unauthorized access
Flexibility: Support both whitelist trees and claim code systems
Scalability: Handle large user bases without on-chain storage
Interface Definition
export interface MerkleChallenge<T extends NumberType> {
root: string; // SHA256 Merkle tree root hash
expectedProofLength: T; // Required proof length (security)
useCreatorAddressAsLeaf: boolean; // Use initiator address as leaf?
maxUsesPerLeaf: T; // Maximum uses per leaf
uri: string; // Metadata URI
customData: string; // Custom data field
challengeTrackerId: string; // Unique tracker identifier
leafSigner: string; // Optional leaf signature authority
}
Basic Example
{
"merkleChallenges": [
{
"root": "758691e922381c4327646a86e44dddf8a2e060f9f5559022638cc7fa94c55b77",
"expectedProofLength": "1",
"useCreatorAddressAsLeaf": false,
"maxUsesPerLeaf": "1",
"uri": "ipfs://Qmbbe75FaJyTHn7W5q8EaePEZ9M3J5Rj3KGNfApSfJtYyD",
"customData": "",
"challengeTrackerId": "uniqueId",
"leafSigner": "0x"
}
]
}
Challenge Types
1. Claim Code Challenges
Create a Merkle tree of secret claim codes that users must provide to claim badges.
Use Case: Private claim codes, invitation systems, promotional campaigns
Process:
Generate secret claim codes
Build Merkle tree from hashed codes
Distribute codes privately to users with leaf signatures
Users provide code + Merkle proof in transfer
2. Whitelist Challenges
Create a Merkle tree of user addresses for gas-efficient whitelisting.
Use Case: Large whitelists, community access, gas cost distribution
Process:
Collect user addresses
Build Merkle tree from hashed addresses
Users provide their address + Merkle proof
System verifies address is in whitelist / valid proof
Gas Cost Distribution: Instead of the collection creator paying gas to store N addresses on-chain, each user pays their own gas for proof verification.
Understanding useCreatorAddressAsLeaf
The useCreatorAddressAsLeaf
field determines how the system handles the leaf value in Merkle proofs:
Whitelist Trees (useCreatorAddressAsLeaf: true
)
useCreatorAddressAsLeaf: true
)Purpose: Verify that the transaction initiator is in the whitelist.
How It Works:
Automatic Override: The system expects the provided leaf to be the initiator's BitBadges address ("bb1...")
Address Verification: Checks if the initiator's address exists in the Merkle tree
No Manual Leaf: Users don't need to provide their address as the leaf - the system handles it
Recommended Configuration:
Set
initiatedByList
to "All" (whitelist tree handles the restriction)Set
useCreatorAddressAsLeaf: true
Build Merkle tree from BitBadges addresses as leaves ["bb1...", "bb2...", "bb3..."]
Claim Code Trees (useCreatorAddressAsLeaf: false
)
useCreatorAddressAsLeaf: false
)Purpose: Verify that the user possesses a valid claim code.
How It Works:
Manual Leaf: User must provide the actual claim code as the leaf
Code Verification: System verifies the provided code exists in the Merkle tree
User Responsibility: Users must know and provide their claim code
Recommended Configuration:
Set
useCreatorAddressAsLeaf: false
Build Merkle tree from claim codes as leaves ["secret1", "secret2", "secret3"]
Post root hash on-chain as challenge
Distribute codes privately to users with leaf signatures
Security Features
Expected Proof Length
Critical Security Feature: All proofs must have the same length to prevent preimage and second preimage attacks.
// All proofs must match this length
expectedProofLength: '2'; // 2-level proof required
Design Requirement: Your Merkle tree must be constructed so all leaves are at the same depth.
Max Uses Per Leaf
Control how many times each leaf can be used:
"0"
or null
Unlimited uses
Public claim codes
"1"
One-time use
Single-use codes
"5"
Five uses maximum
Limited distribution
Critical Security Requirement: For claim code challenges (useCreatorAddressAsLeaf: false
), maxUsesPerLeaf
must be "1"
to prevent replay attacks.
Replay Attack Protection
⚠️ CRITICAL SECURITY RISK: Non-address trees (claim codes) are vulnerable to front-running attacks.
The Problem:
User submits transaction with valid Merkle proof
Proof becomes visible in mempool (public blockchain)
Malicious actor sees the proof and front-runs the transaction
Original user's transaction fails, attacker gets the badge
Why This Happens:
Merkle proofs for claim codes are reusable until consumed
Once in mempool, proofs are publicly visible
No built-in protection against proof reuse
The Solution: Leaf signatures provide cryptographic protection against this attack.
Challenge Tracking
Tracker System
Uses increment-only, immutable trackers to prevent double-spending:
{
collectionId: T;
approvalId: string;
approvalLevel: 'collection' | 'incoming' | 'outgoing';
approverAddress: string; // blank if collection-level
challengeTrackerId: string;
leafIndex: T; // Leftmost base layer leaf index = 0, rightmost = numLeaves - 1
}
Note the fact we use leaf indices to track usage and not leaf values.
Tracker Examples
1-collection- -approvalId-uniqueID-0 → USED 1 TIME
1-collection- -approvalId-uniqueID-1 → UNUSED
1-collection- -approvalId-uniqueID-2 → USED 3 TIMES
Important: Trackers are scoped to specific approvals and cannot be shared between different approval configurations.
Tracker Management
Increment-Only: Once used, the number of uses cannot be decremented
Immutable: Tracker state cannot be modified
Best Practice: Use unique
challengeTrackerId
for fresh tracking of new approvals
Leaf Signatures
Protection Against Front-Running
Leaf signatures provide cryptographic protection against front-running attacks on claim code challenges.
How It Works:
// Signature scheme
signature = ETHSign(leaf + '-' + bitbadgesAddressOfInitiator);
Security Mechanism:
Address Binding: Each proof is cryptographically tied to a specific BitBadges address
Replay Prevention: Even if proof is intercepted, it cannot be used by other addresses
Mempool Safety: Intercepted proofs in mempool are useless to attackers
Implementation
// Only Ethereum addresses supported currently
leafSigner: '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6';
Critical Benefits:
Front-Running Protection: Prevents attackers from stealing badges via mempool interception
Address-Specific: Each proof is cryptographically bound to the intended recipient
Mempool Safety: Makes intercepted proofs useless to malicious actors
Required for Claim Codes: Strongly recommended for all non-address tree challenges
⚠️ IMPORTANT: For claim code challenges, leaf signatures are not just recommended—they are essential for security against front-running attacks.
Merkle Tree Construction
Standard Configuration
import { SHA256 } from 'crypto-js';
import MerkleTree from 'merkletreejs';
// For claim codes
const codes = ['secret1', 'secret2', 'secret3'];
const hashedCodes = codes.map((x) => SHA256(x).toString());
// For whitelists
const addresses = ['bb1...', 'bb1...', 'bb1...'];
const hashedAddresses = addresses.map((x) => SHA256(x));
// Tree options (tested configuration)
const treeOptions = {
fillDefaultHash:
'0000000000000000000000000000000000000000000000000000000000000000',
};
// Build tree
const tree = new MerkleTree(hashedCodes, SHA256, treeOptions);
const root = tree.getRoot().toString('hex');
const expectedProofLength = tree.getLayerCount() - 1;
Critical Requirements
Same Layer: All leaves must be at the same depth
Consistent Proof Length: All proofs must have identical length
Test Thoroughly: Verify all paths work before deployment
Use Tested Options: Stick to the
fillDefaultHash
configuration
Transfer Integration
Providing Proofs
Include Merkle proofs in MsgTransferBadges:
const txCosmosMsg: MsgTransferBadges<bigint> = {
creator: chain.bitbadgesAddress,
collectionId: collectionId,
transfers: [
{
// ... other fields
merkleProofs: [
{
aunts: proofObj.map((proof) => ({
aunt: proof.data.toString('hex'),
onRight: proof.position === 'right',
})),
leaf: isWhitelist ? '' : passwordCodeToSubmit,
leafSignature: leafSignature, // if applicable
},
],
},
],
};
Proof Generation
// Generate proof for user submission
const passwordCodeToSubmit = 'secretCode123';
const leaf = isWhitelist
? SHA256(chain.bitbadgesAddress).toString()
: SHA256(passwordCodeToSubmit).toString();
const proofObj = tree.getProof(leaf, whitelistIndex);
const isValidProof = proofObj && proofObj.length === tree.getLayerCount() - 1;
// Create signature if needed
const leafSignature = signLeaf(leaf + '-' + chain.bitbadgesAddress);
Best Practices
Design Considerations
Tree Structure: Ensure all leaves at same depth
Proof Length: Test all proof lengths are identical
Tracker Management: Use unique IDs for fresh tracking
Security: MANDATORY - Enable leaf signatures for claim codes to prevent front-running
Testing: Verify all paths work before mainnet
Performance Optimization
Small Lists: For <100 users, consider regular address lists
Gas Distribution: Merkle trees excel with large user bases
Proof Verification: On-chain verification is gas-efficient
Storage: No on-chain storage of large lists required
Last updated