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);
Comparison with ETH Signature Challenges
Merkle challenges and ETH signature challenges are very similar. The main difference is that Merkle challenges must also check that the signed message was pre-committed to in the tree, whereas ETH signature challenges only need to check that the signature is valid and not used before.
For more information, see ETH Signature Challenges.
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