Merkle Root Verification is a project combining Solidity and JavaScript to generate and verify Merkle roots and proofs. The project leverages Foundry for Solidity development and Node.js for Merkle tree generation.
A smart contract on chain B has to verify an inclusion proof p
of a message m
on chain A's stateroot s
. It is given that stateroot s
stored on chain B is verified and stored by a trusted party.
For the sake of simplicity - I will be transactionsRoot of a block instead of stateroot.
- User Alice puts transaction
t
on chain A using contractc1
- Chain A now has state
s
- Generate merkle proof of transaction inclusion.
- Contract
c2
on chain B is fed states
, transactiont
and proofp
by trusted entity - Contract
c2
verifies that transactiont
is a part of states
- Forge: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- Cast: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- Anvil: Local Ethereum node, akin to Ganache, Hardhat Network.
- Node: Javascript runtime
/
├── JavascriptRootGenerator/
│ ├── node_modules/
│ ├── package-lock.json
│ ├── package.json
│ ├── src/
│ │ ├── MerkleTree.js
│ │ └── index.js
├── README.md
├── foundry.toml
├── lib/
├── remappings.txt
├── script/
│ └── SimpleStorage.s.sol
├── src/
│ ├── MerkleVerifier.sol
│ └── SimpleStorage.sol
- JavascriptRootGenerator - This is a JavaScript project built with node that uses
merkletreejs
andmerkle-patricia-tree
packages to generate merkle proofs from given transactions. - script/SimpleStorage.s.sol - This contains a dev script to deploy and execute the
SimpleStorage.sol
contract and send 4 transactions. - src/SimpleStorage.sol - Smart contract to help user send transactions on chain A
- src/MerkleVerifier.sol - Smart contract to verify inclusion of transaction in the Merkle Patricia Tree with the help of the merkle proof on chain B.
We will be using 2 local blockchain using Anvil. To start these chains open 2 terminal shells and type the following -
Shell 1
anvil --no-mining --port 8546
💡: --no-mining
is important so we are able to later put 4 transactions in one block and use it to generate MPT, proof and even see the valid transactionsRoot
Shell 2
anvil
This will start 2 endpoints on http://127.0.0.1:8546
and http://127.0.0.1:8545
respectively.
Upon running Shell 1
you will notice a list of private keys given by Anvil. We will use 1 of them to execute our deployment script.
To store it properly -
- Create a
.env
file in the root directory. - Paste the private key here. Your file should look like this -
PRIVATE_KEY=your-private-key
Awesome! Now the setup is complete.
- Open another shell and run the following command -
forge script script/SimpleStorage.s.sol:SimpleStorageScript --rpc-url http://127.0.0.1:8546 --broadcast
Output -
[⠊] Compiling...
No files changed, compilation skipped
Script ran successfully.
## Setting up 1 EVM.
==========================
Chain 31337
Estimated gas price: 1.758366569 gwei
Estimated total gas used for script: 666050
Estimated amount required: 0.00117116005328245 ETH
==========================
⠠ Sequence #1 on anvil-hardhat | Waiting for pending transactions
⠄ [Pending] 0x39c1929616bf4f99a166fbe0a0cc77e4b6efb343c4ec10f14fe3bd44f6a8862b
⠄ [Pending] 0x9870bf2d87a2739c20f30858a2b37bfdb30fb5f1019094d0b69573efee0f0e01
⠄ [Pending] 0x1f293988fd6cdba636dd269d86fb5ce82089fd4497b610182f7875f67bca1f39
⠄ [Pending] 0xef6d5929b4be98098d44a56956bef7afef123034cdc8de6002baa463607a6ac5
⠴ [00:00:09] [###############################################] 4/4 txes (0.0s)
⠴ [00:00:09] [-------------------------------------------] 0/4 receipts (0.0s)
- Now, we have to mine the transaction on Chain with port
8546
To do so, open another shell and run -
curl -X POST --data '{"jsonrpc":"2.0","method":"evm_mine","params":[],"id":1}' -H "Content-Type: application/json" http://127.0.0.1:8546
As soon as you execute this, the block will mine and you will be able to see your transaction on Shell 1
where we are running the first Anvil instance
- In a new shell, run the following -
cd JavascriptRootGenerator npm i
- Once dependencies are installed, run the following -
node src/index.js
As the above command runs successfully, you will be able to see -
- Block transactionsRoot
- List of transactions in the block
- Inclusion proofs
- Local verification of inclusion proofs
You can also use src/MerkleTree.js
to generate proofs using merkletreejs
package.
Simply run node src/MerkleTree.js
in the same shell.
Ouput -
Transaction Hashes: [
'0x2f26a882826c3fff1bc1a43e002e31a9fdb4a5ca8409aeb2b342089c75ef298f',
'0xead1b3fea791cf327be81e5ca3a6a1b299eb1d908cf68718bcec93e508d2c78e',
'0x94debd24e059f6b58e5db471fa32394fd76466de5e35f3f500dff447b1aa0466',
'0x70ba62bc05b444a7bf940f826b5f23662b753730edb349b46cc53ce23112c9b7'
]
Leaves: [
'c92bda097f45c1dda1ee3f5b5bbd77efd260f5201955fd895bfbad7a841efcf0',
'd586549164154944709501137e7c66d22ddac8badd8803b37ac8fab32a14678f',
'207aef649da9a8108d81698176734a6093d98ccc0ef38e3369684c5a62632edc',
'b37653b2a85c46f9b35042a39a989984f99f725f962a9d8ab8fa9f80274fa53b'
]
Merkle Tree:
└─ d2e96605814f21ff8bf384bda0591732172147038f3ce74a889700a667f97946
├─ e2a4a05f8b9d6d8e31f4e2b89c1ba34dbd541e88e2b3abbcdd1c51b2c5ffb181
│ ├─ c92bda097f45c1dda1ee3f5b5bbd77efd260f5201955fd895bfbad7a841efcf0
│ └─ d586549164154944709501137e7c66d22ddac8badd8803b37ac8fab32a14678f
└─ 0853a40961651a094014485dd7aede5cb2f1334873537a465fcded7201817f47
├─ 207aef649da9a8108d81698176734a6093d98ccc0ef38e3369684c5a62632edc
└─ b37653b2a85c46f9b35042a39a989984f99f725f962a9d8ab8fa9f80274fa53b
Merkle Root: d2e96605814f21ff8bf384bda0591732172147038f3ce74a889700a667f97946
Proof for transaction: 0x2f26a882826c3fff1bc1a43e002e31a9fdb4a5ca8409aeb2b342089c75ef298f
Proof: [
'0xd586549164154944709501137e7c66d22ddac8badd8803b37ac8fab32a14678f',
'0x0853a40961651a094014485dd7aede5cb2f1334873537a465fcded7201817f47'
]
Is the proof valid locally? true
As the above command runs successfully, you will be able to see -
- List of transactions
- Leaves of merkle tree
- Transaction proof [Important for later]
- Merkle root [Important for later]
- Merkle proof [Important for later]
- Local verification for inclusion proofs
💡: You will need a private key to deploy smart contract here. To obtain it, head over to Shell 2
where we instantiated Chain B and copy the private key like we did before. Foundry will request for it on the shell as soon as you run the following commands.
-
Open a new shell and run the following command -
forge create src/RootVerifier.sol:MerkleVerifier --interactive --broadcast --rpc-url http://127.0.0.1:8545
Output -
[⠊] Compiling... No files changed, compilation skipped Enter private key: Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 Transaction hash: 0xb4d3f171d283cad85bfce08bebe69107f5dca47283cf2bf86b
Now we will start verifying the Merkle Proof for transaction inclusion.
💡: You will need a private key to deploy smart contract here. To obtain it, head over to Shell 2
where we instantiated Chain B and copy the private key like we did before. Foundry will request for it on the shell as soon as you run the following commands.
-
In the current shell, run the following command -
cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 "setStateRoot(bytes32)" "0xd2e96605814f21ff8bf384bda0591732172147038f3ce74a889700a667f97946" --interactive --rpc-url http://127.0.0.1:8545
Let's break down this command and understand it -
cast send
- Sign and publish a transaction0x5FbDB2315678afecb367f032d93F642f64180aa3
- Address of the contract we want to interface with. This can be different for you, please ensure you copy this from the previous command where we deployMerkleVerifier.sol
"setStateRoot(bytes32)"
- We are calling this function within theMerkleVerifier.sol
smart contract. This will help us set the merkle root."0xd2e96605814f21ff8bf384bda0591732172147038f3ce74a889700a667f97946"
- This is the Merkle Root obtained from the Javascript code we have executed earlier. Please ensure that you add0x
to the beginning of the hex merkle proof obtained by running the javascript file.--interactive --rpc-url http://127.0.0.1:8545
- Allows us to input private key and send transaction to chain B
Output -
Enter private key: blockHash 0x47856617a79b94295e99ce666dc0d6ee7aa53453974ee5437a8095d17bf9a1fb blockNumber 2 contractAddress cumulativeGasUsed 45058 effectiveGasPrice 876563160 from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 gasUsed 45058 logs [{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0x49cdb3ad83652f622a15ea6bf86d99158af199d3f7ded7efdbec55579181a629","0xd2e96605814f21ff8bf384bda0591732172147038f3ce74a889700a667f97946"],"data":"0x","blockHash":"0x47856617a79b94295e99ce666dc0d6ee7aa53453974ee5437a8095d17bf9a1fb","blockNumber":"0x2","blockTimestamp":"0x6777c02c","transactionHash":"0x4e798615c23f7e51b3783db02037645941158420fdc69759deb7ab39f242c438","transactionIndex":"0x0","logIndex":"0x0","removed":false}] logsBloom 0x40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040020000000000000000000000000000000000000000000080000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000200000000000000000000000000000000000000000000000000010000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 root status 1 (success) transactionHash 0x4e798615c23f7e51b3783db02037645941158420fdc69759deb7ab39f242c438 transactionIndex 0 type 2 blobGasPrice 1 blobGasUsed authorizationList to 0x5FbDB2315678afecb367f032d93F642f64180aa3
-
In the same shell, run -
cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 "verifyMessage(bytes32, bytes32[])" "0x2f26a882826c3fff1bc1a43e002e31a9fdb4a5ca8409aeb2b342089c75ef298f" "[0xd586549164154944709501137e7c66d22ddac8badd8803b37ac8fab32a14678f,0x0853a40961651a094014485dd7aede5cb2f1334873537a465fcded7201817f47]" --interactive --rpc-url http://127.0.0.1:8545
Let's break down this command and understand it -
cast send
- Sign and publish a transaction0x5FbDB2315678afecb367f032d93F642f64180aa3
- Address of the contract we want to interface with. This can be different for you, please ensure you copy this from the previous command where we deployMerkleVerifier.sol
"verifyMessage(bytes32, bytes32[])"
- We are calling this function within theMerkleVerifier.sol
smart contract. This will help us verify the inclusion proof."0x2f26a882826c3fff1bc1a43e002e31a9fdb4a5ca8409aeb2b342089c75ef298f"
- This is the transaction proof obtained from the Javascript code we have executed earlier."[0xd586549164154944709501137e7c66d22ddac8badd8803b37ac8fab32a14678f,0x0853a40961651a094014485dd7aede5cb2f1334873537a465fcded7201817f47]"
- This is the merkle proof obtained from the Javascript code we have executed earlier.--interactive --rpc-url http://127.0.0.1:8545
- Allows us to input private key and send transaction to chain B
Output -
Enter private key: blockHash 0xedb71b6a903c5eadc3fc78af1454ae63da5c24faeac85187d1aaa174e916e105 blockNumber 3 contractAddress cumulativeGasUsed 26193 effectiveGasPrice 767321900 from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 gasUsed 26193 logs [] logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 root status 1 (success) transactionHash 0xa6e07316691a64e2779572948f30640c62450a69b3e7d7ce9aa5b695e86fa623 transactionIndex 0 type 2 blobGasPrice 1 blobGasUsed authorizationList to 0x5FbDB2315678afecb367f032d93F642f64180aa3
Upon decoding this output you can verify that
true
is returned.For beginners I also recommend deploying this contract on remix and play with it.
Feel free to contribute to this project by creating issues or opening pull request.