Skip to content

This guide walks you through verifying a backend-signed message on-chain. By the end, you’ll understand the core Range verification flow.

  • Solana CLI installed
  • Anchor installed
  • Node.js 18+
  • A Solana wallet with devnet SOL

The verification flow has three steps:

  1. Initialize Settings - Create a Settings PDA with your trusted signer’s public key
  2. Backend Signs Message - Your backend creates {timestamp}_{pubkey} and signs it
  3. Verify On-chain - User submits transaction with the signature and message
  1. Clone and Build the Range Program

    Terminal window
    git clone https://github.com/ZKLSOL/range.git
    cd range
    anchor build
  2. Generate TypeScript Client

    Range uses Codama to generate type-safe clients from the IDL.

    Terminal window
    # Install dependencies
    yarn install
    # The client is pre-generated in codama-ts-range/
    # To regenerate after IDL changes:
    yarn codama
  3. Initialize Settings

    Create a Settings account that stores your trusted signer’s public key:

    import { Connection, Keypair } from '@solana/web3.js';
    import { buildInitializeSettingsInstruction } from './codama-ts-range-custom';
    const connection = new Connection('https://api.devnet.solana.com');
    const admin = Keypair.generate(); // Your admin keypair
    const rangeSigner = Keypair.generate(); // Backend's signing keypair
    const instruction = await buildInitializeSettingsInstruction({
    admin: admin.publicKey,
    rangeSigner: rangeSigner.publicKey,
    windowSize: 60n, // 60 seconds validity window
    });
    // Sign and send transaction...
  4. Backend: Sign a Message

    Your backend creates a time-bound message and signs it:

    import nacl from 'tweetnacl';
    function createSignedMessage(userPubkey: string, signerKeypair: Keypair) {
    const timestamp = Math.floor(Date.now() / 1000);
    const message = `${timestamp}_${userPubkey}`;
    const signature = nacl.sign.detached(
    Buffer.from(message),
    signerKeypair.secretKey
    );
    return {
    signature: new Uint8Array(signature),
    message: Buffer.from(message),
    timestamp,
    };
    }
  5. Verify On-chain

    The user includes the backend’s signature in their transaction:

    import { buildVerifyRangeInstruction } from './codama-ts-range-custom';
    // User receives signature and message from backend
    const { signature, message } = await fetchFromBackend(userPubkey);
    const instruction = await buildVerifyRangeInstruction({
    signer: userPubkey, // Transaction signer (must match message)
    admin: settingsAdminPubkey, // Admin who created the Settings
    signature: signature,
    message: message,
    });
    // Sign and send transaction...

    If verification succeeds, the program emits a VerificationSuccess event.

The on-chain program performs these checks:

CheckWhat It ValidatesError If Failed
SignatureEd25519 signature against range_signerCouldntVerifySignature
TimestampWithin window_size of current timeTimestampOutOfWindow
PubkeyMessage pubkey matches transaction signerWrongSigner

See the tests/range.ts file for a complete working example including:

  • Settings initialization
  • Message signing
  • Verification
  • Error handling