Solidity Merkle Tree – Using JavaScript to Generate Hex Proof for Solidity Merkle Tree Validation

ethereumjskeccakmerklesolidity

Here is my js code of generating hex proof for solidity verification

const express = require('express')
const { MerkleTree } = require('merkletreejs');
const { bufferToHex } = require('ethereumjs-util');
const keccak256 = require('keccak256');
const url = require('url');
const cors = require("cors");
const app = express();

app.use(cors())

//1_000_000_000_000e18
//10000000000000000000000000000

let whitelist = [
    {addr: "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", amount: 20000000000000000000000000}, //10_000_000_000e18
    {addr: "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", amount: 20000000000000000000000000},
]

const allLower = whitelist.map(item => item)
const leafNodes = allLower.map(item => keccak256(item.addr, item.amount))
console.log('leafNodes: ', leafNodes)
const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true })

function getProof(addr, amount) {
    let index = allLower.findIndex(item => item.addr === addr && item.amount === amount);
    const hexProof = merkleTree.getHexProof(leafNodes[index])
    console.log('hexProof: ', hexProof, typeof hexProof)
    return hexProof
}

and my solidity verification function takes 2 inputs which are amount(uint256) and merkleProof (byte32). I kept getting invalid merkleProof error.
I am guessing it should be the way I am using js keccak256 is wrong as it should only take one paramemters for input but I am not sure. Can some one helps please

Here is solidity merkleproof verification code

function claimTokens(uint256 amount, bytes32[] calldata merkleProof) public {
        bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount));
        bool valid = MerkleProof.verify(merkleProof, merkleRoot, leaf);
        require(valid, "valid proof required.");
        require(!claimed[msg.sender], "Tokens already claimed.");
        claimed[msg.sender] = true;
    
        emit Claim(msg.sender, amount);

        _transfer(address(this), msg.sender, amount);
    }

Best Answer

There are several issues with your code. For the example, I'll use Web3.js for some utility functions but feel free to use whatever suits you best. I'd personally use the keccak256 implementations from either Web3.js or Ethers, but I'll keep yours here.

First, 20000000000000000000000000 is too big of a value for the JavaScript Number type to be represented safely, so you may end up with nasty errors due to that. Use string representations / Big Numbers whenever possible when dealing with the blockchain. Then, you must make sure that your input matches exactly what will be hashed on the solidity side. uint256 are encoded on 32 bytes.

Second, the implementation of keccak256 that you are using only expects one parameter and doesn't even consider the other ones, leading to the following behavior :

console.log(keccak256("0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"))
// <Buffer 59 31 b4 ed 56 ac e4 c4 6b 68 52 4c b5 bc bf 41 95 f1 bb aa cb e5 22 8f bd 09 05 46 c8 8d d2 29>

console.log(keccak256("0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", web3.eth.abi.encodeParameter("uint256", "20000000000000000000000000")))
// <Buffer 59 31 b4 ed 56 ac e4 c4 6b 68 52 4c b5 bc bf 41 95 f1 bb aa cb e5 22 8f bd 09 05 46 c8 8d d2 29>

So if you want to stick to that implementation, you must provide them as a single parameter.

With this implementation :

const keccak256 = require("keccak256");
const { MerkleTree } = require("merkletreejs");
const Web3 = require("web3");

const web3 = new Web3();

let whitelist = [
  {
    addr: "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
    // ABI encode the amount : 32 bytes integer as a hex string
    amount: web3.eth.abi.encodeParameter(
      "uint256",
      "20000000000000000000000000"
    ),
  },
  {
    addr: "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
    // ABI encode the amount : 32 bytes integer as a hex string
    amount: web3.eth.abi.encodeParameter(
      "uint256",
      "20000000000000000000000000"
    ),
  },
];

const allLower = whitelist.map((item) => item);

// For each element : concatenate the two hex buffers
// to a single one as this keccak256 implementation only
// expects one input
const leafNodes = allLower.map((item) =>
  keccak256(
    Buffer.concat([
      Buffer.from(item.addr.replace("0x", ""), "hex"),
      Buffer.from(item.amount.replace("0x", ""), "hex"),
    ])
  )
);

const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });

function getProof(addr, amount) {
  let index = allLower.findIndex(
    (item) => item.addr === addr && item.amount === amount
  );

  return merkleTree.getHexProof(leafNodes[index]);
}

// Provide amount as a 32 bytes hex string
const proof = getProof(
  "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
  web3.eth.abi.encodeParameter("uint256", "20000000000000000000000000")
);

console.log("Root : ", merkleTree.getHexRoot());
// Root :  0x399f97e5a37a31d24b746bb2d8fa5212fbe3c0f7ceb4b69ce3c9f37d0379ff72

console.log("Proof : ", proof);
// Proof :  ['0x15e7001d27767868de62fcb73e3d14ac4d28c60dda34b0f259e79440b8f27e36']

You can successfully verify on the solidity side. I used the following contract :

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract Example {
  
  function verify(uint256 amount, bytes32[] memory proof) public view returns (bool) {
    bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount));
    bytes32 root = 0x399f97e5a37a31d24b746bb2d8fa5212fbe3c0f7ceb4b69ce3c9f37d0379ff72;
    return MerkleProof.verify(proof, root, leaf);
  }

}

I hope this answers your question, and I'd advise for unifying everything : use the abi coder and the keccak256 implementation of a single library (i.e., Web3 or Ethers) to avoid those issues.