-
Notifications
You must be signed in to change notification settings - Fork 72
Description
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
createVerifyLeafInstructionto verify the proof, orcreateTransferInstructionto 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
createVerifyLeafInstructionandcreateTransferInstruction, 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
- Could there be an issue with how these functions process the proof and inputs?
- If I am using these functions incorrectly, could you provide guidance or examples of proper usage?
Thank you for your assistance!
