Skip to content

Verification Fails for createVerifyLeafInstruction and createTransferInstruction #118

@MoizYousuf

Description

@MoizYousuf

I am encountering verification issues with both the createVerifyLeafInstruction function from the @solana/spl-account-compression library and the createTransferInstruction function from the @metaplex-foundation/mpl-bubblegum library.

I have implemented a local function to calculate the Merkle tree root and leaf, and the calculations match the proof provided in the tree. However, when using either createVerifyLeafInstruction or createTransferInstruction, the verification fails, indicating that the leaf or root does not match.

Steps to Reproduce

  • Implement a local function to calculate the Merkle tree root and leaf.

  • Verify that the local calculations match the provided proof tree.

  • Use createVerifyLeafInstruction to verify the proof, or createTransferInstruction to perform a transfer operation.

  • Observe that both functions return errors indicating verification failures.

What I've Done

  • Created a local function to calculate the Merkle tree root and leaf, ensuring they match the proof data.

  • Verified the proof using the local implementation, confirming its correctness.

  • Attempted to use createVerifyLeafInstruction and createTransferInstruction, both of which returned errors.

Expected Behavior

Both createVerifyLeafInstruction and createTransferInstruction should work correctly when the root and proof match the expected values.

Actual Behavior

Both functions fail, returning errors indicating mismatches in the leaf or root, even though local calculations confirm the proof is valid.

Snippets

Here’s the local function I used to calculate the Merkle tree root and verify the proof:


import { PublicKey } from "@solana/web3.js";
import bs58 from "bs58";
import { MerkleTree, MerkleTreeProof } from "../edge/utils";
import * as web3 from "@solana/web3.js";
import { fetchAssetProof, fetchDasAssets } from "../edge/utils";
import { connection } from "../tests/_base";
import { createVerifyLeafInstruction } from "@solana/spl-account-compression";
import { createTransferInstruction } from "@metaplex-foundation/mpl-bubblegum";
import { Asset } from "../edge/types";
import { GraphQLError } from "graphql";
import { Proof } from "../edge/generated";
import {
  SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
  SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
import { BN } from "@coral-xyz/anchor";

/**
 * @param base58Str - The base58-encoded string.
 * @returns The decoded Buffer.
 */
function decodeBase58(base58Str: string): Buffer {
  return Buffer.from(bs58.decode(base58Str));
}

const secretKeyToKeyPair = (secretKey: string) => {
  return web3.Keypair.fromSecretKey(bs58.decode(secretKey));
};

const secretKeyToPublicKey = (secretKey: string) => {
  return secretKeyToKeyPair(secretKey).publicKey;
};

/**
 * Verifies the Merkle proof.
 * @param data - The Merkle tree data from Helius.
 * @returns Whether the proof is valid or not.
 */
function verifyHeliusMerkleProof(data: Proof): boolean {
  const { root, proof, node_index, leaf } = data;

  // Decode the inputs
  const decodedRoot = decodeBase58(root);
  const decodedLeaf = decodeBase58(leaf);
  const decodedProof = proof.map(decodeBase58);

  // Create the proof object
  const merkleProof: MerkleTreeProof = {
    root: decodedRoot,
    leaf: decodedLeaf,
    leafIndex: Number(node_index),
    proof: decodedProof,
  };

  // Verify the proof
  const isValid = MerkleTree.verify(merkleProof.root, merkleProof, true);

  console.log(`Is proof valid? ${isValid}`);
  return isValid;
}

/**
 * Calculate the root from the proof (if verification is not needed).
 * @param data - The Merkle tree data from Helius.
 * @returns The calculated root as a base58 string.
 */
function calculateRoot(data: Proof): string {
  const { proof, node_index, leaf } = data;

  // Decode the inputs
  const decodedLeaf = decodeBase58(leaf);
  const decodedProof = proof.map(decodeBase58);

  // Calculate the root
  const calculatedRoot = MerkleTree.hashProof({
    leaf: decodedLeaf,
    leafIndex: Number(node_index),
    proof: decodedProof,
    root: Buffer.alloc(0), // Placeholder
  });

  return bs58.encode(calculatedRoot);
}

// Example usage with Helius data
// const heliusData = {
//   root: "CDyDYWz85pz3L2VrfHACaFy5ZQeeNHWKKEs4PnsC2ZZ7",
//   proof: [
//     "DY4JYxvipo95t4vJVFw3aWTpFh23APAvV4ZpB2rTVg7M",
//     "PiHTnb2o3DyUUyzxxW3Sq5Z6KQBumyqanxXU75b5DeJ",
//     "8MEcfbEBU5VTJycRELCBRb6Vx83xLZsW3UBnJRWBzWnf",
//     "1HKxXeZGRptnBjxNzrhV65Ez9EDHRQZgGMNj635eoKm",
//     "XHQLXz9F1w4KKwnbUurjK4SDipABUy4saZBNNVfRUqr",
//     "Gj51WoA4GmabvHCgX4g2eZs1cT7TjBJLytLPp99HMWcE",
//     "5egQR2WEpNMsxVjMAW9XSdzPdZ3RBcZizyBVjQzjdZvx",
//     "H8efNp7nfRQe6CUxLm96pvePbQhUsrJjwYdPG1f35SZ1",
//     "DJtTArdE9DAbNPr5bf6Hcqe2LjKFhSUTzpvfruRPy8Xi",
//     "BxPxMzHLHNk7EHmH2XS38krtHx6UCdwVc9UHNq2EQasD",
//     "D9UBcydgqYyTMMuh34AtFJXjy5SQof9W2FF8G9JB4zhm",
//     "AAvZnTFwCsjHj12K219EW6sJp351B3yzzmUD3muj6R1S",
//     "5hPFGEZubYQriWjiB1nAu2tdLa8HbPax292qPwhbFjpH",
//     "9XsA1PzMVeycm9DqoG586vA6PKgLcPaGrtLJuYgtBefs",
//     "ESsTwGbxmfN4BMyCQ87SS3fdyfjn4vHQ62nYm9JN1iyc",
//     "FjV56x6MT13Y4Zr5pLaH4RWSZKY8hnaBqBnzP8H1C9Wf",
//     "GP2ccSDMGyEscNcR6e1SGzdPPYgtYpgxcHZ3seKXpic4",
//     "GtJ8SF7pWdb9NUdxm3QxnBnem9KzFdgVNFYwjZpVNF5s",
//     "39kHwi5HFPrKCpGg1menD5qY5nFkZeP82o3r3QV5u9SR",
//     "D5Rty4haKVQFbuqgLz8bYHBYicQdnheNzUcgickt1mry",
//   ],
//   node_index: 1310948,
//   leaf: "BBjtQRXQNF2mViUu3wQNewnQH6f7gKakiYwuXP23MmyM",
//   tree_id: "BXMBo3FKjihFk5F7Mj8UdByNrufmzhfC4Ng6qcUA5VDs",
// };

const shyftData: Proof = {
  root: "2v5UkPQwxzCkQozJxKuDpwrvMASCrCxZwVejVAfbeTdj",
  proof: [
    "DY4JYxvipo95t4vJVFw3aWTpFh23APAvV4ZpB2rTVg7M",
    "PiHTnb2o3DyUUyzxxW3Sq5Z6KQBumyqanxXU75b5DeJ",
    "8MEcfbEBU5VTJycRELCBRb6Vx83xLZsW3UBnJRWBzWnf",
    "1HKxXeZGRptnBjxNzrhV65Ez9EDHRQZgGMNj635eoKm",
    "XHQLXz9F1w4KKwnbUurjK4SDipABUy4saZBNNVfRUqr",
    "Gj51WoA4GmabvHCgX4g2eZs1cT7TjBJLytLPp99HMWcE",
    "5egQR2WEpNMsxVjMAW9XSdzPdZ3RBcZizyBVjQzjdZvx",
    "H8efNp7nfRQe6CUxLm96pvePbQhUsrJjwYdPG1f35SZ1",
    "oyLn6pYXu1aZyonjbuz6Nhf5o2wq5ihhiENguw7LzzL",
    "5okG88hBY4fRhP3nG9aENkdQP7AvPPgWyT3AAUm9dDZu",
    "CYes99Ag6kRdKVxi8qfs1Qnd4Vz73HZwMUQJoKAVtL58",
    "CU2kc4HjgbXzwUoY2aJwv3WkZ6SwWpyMK3PHsTmMuTfa",
    "ErbbNWA7AhsBiwz7gejRyRDfchUfLabKidmfP6qv7Hiw",
    "BE7NpZpnjq3i7MayEiK4CQ9vGaNhaaDetyZgbuWDbi1q",
    "88GAnAHfR4pAeWkbhozwmtbk5tGHgVdhN6AR8MXbqyvD",
    "HFphNMSTH2JgaXY87D7WBv6yNrhrcdU1hk5xkqXK1DEU",
    "8mgGPAHc3kLmMBXv3ohHNbBNPqVHt9uBaU82MvdsonLr",
    "Fw2mwmpc8h8vKcYrq9hJedt9otrzpNkMXde4jsoZLNnG",
    "4bqevQpTCzhJjDR5ZsZALQkFvHST4Xcfi3CQpCgwAKGy",
    "G6xXqFURgb15jEe4N18irqn3hakZmc2SB1MA6hTNhe4t",
  ],
  maxDepth: 20,
  node_index: 1310948n,
  leaf_index: undefined,
  leaf: "9AL3ULsY446kVzgfQ9Nu3Nf91jAkXUbg9Ar7LgEtMA2E",
  tree_id: "BXMBo3FKjihFk5F7Mj8UdByNrufmzhfC4Ng6qcUA5VDs",
  canopy_depth: 0,
};

const driverKeypair = web3.Keypair.fromSecretKey(
  bs58.decode(process.env.DRIVER_KEYPAIR)
);

async function compileAndSendTransaction(
  action: string,
  instructions: web3.TransactionInstruction[],
  payerKey: web3.PublicKey,
  signers: web3.Signer[]
) {
  const { blockhash, lastValidBlockHeight } =
    await connection.getLatestBlockhash();
  const versionedTx = new web3.VersionedTransaction(
    new web3.TransactionMessage({
      instructions,
      payerKey,
      recentBlockhash: blockhash,
    }).compileToV0Message()
  );
  versionedTx.sign(signers);

  return connection.simulateTransaction(versionedTx);
}

async function main(mintAddress: string, secretKey?: string) {
  const assetsArr = await fetchDasAssets(
    { mintList: [new web3.PublicKey(mintAddress)] },
    process.env.RPC_URL
  );

  const asset = assetsArr[0] as Asset;
  const proof = mintAddress ? await fetchAssetProof(mintAddress) : shyftData;

  if (!proof) throw new GraphQLError("Could not find asset proof");
  console.log("proof", proof);

  // Verify the proof
  verifyHeliusMerkleProof(proof as any);

  // Calculate the root from the proof
  const calculatedRoot = calculateRoot(proof);
  console.log("Calculated Root:", calculatedRoot);

  const t = await compileAndSendTransaction(
    `Verify leaf`,
    [
      createTransferInstruction(
        {
          compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
          leafDelegate: secretKey ? secretKeyToPublicKey(secretKey) : driverKeypair.publicKey,
          merkleTree: new web3.PublicKey(proof.tree_id),
          leafOwner: secretKey ? secretKeyToPublicKey(secretKey) : driverKeypair.publicKey,
          logWrapper: SPL_NOOP_PROGRAM_ID,
          newLeafOwner: secretKey ? secretKeyToPublicKey(secretKey) : driverKeypair.publicKey,
          treeAuthority: new web3.PublicKey(
            "BN1pKRWZC9xuDKoaLBqdKfvsQeGQ3fu12QuwunZMKFvY"
          ),
          anchorRemainingAccounts: proof.proof.map((x) => ({
            pubkey: new web3.PublicKey(x),
            isWritable: false,
            isSigner: false,
          })),
        },
        {
          root: Array.from(bs58.decode(proof.root)),
          index: Number(proof.leaf_index),
          creatorHash: Array.from(bs58.decode(asset.compression.creatorHash)),
          dataHash: Array.from(bs58.decode(asset.compression.dataHash)),
          nonce: new BN((proof.leaf_index || 0).toString()),
        }
      ),

      createVerifyLeafInstruction(
        {
          merkleTree: new web3.PublicKey(proof.tree_id),
          anchorRemainingAccounts: proof.proof.map((x) => ({
            pubkey: new web3.PublicKey(x),
            isWritable: false,
            isSigner: false,
          })),
        },
        {
          index: Number(proof.leaf_index),
          leaf: Array.from(bs58.decode(proof.leaf)),
          root: Array.from(bs58.decode(proof.root)),
        }
      ),
    ],
    secretKey ? secretKeyToPublicKey(secretKey) : driverKeypair.publicKey,
    secretKey ? [secretKeyToKeyPair(secretKey)] : [driverKeypair]
  );
  console.log("TTTT", t);
  console.dir(asset, { depth: null });
}

main(process.argv[2], process.argv[3]);

Questions

  1. Could there be an issue with how these functions process the proof and inputs?
  2. If I am using these functions incorrectly, could you provide guidance or examples of proper usage?

Thank you for your assistance!

image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions