Sui.

Post

Share your knowledge.

290697tz.
Jul 24, 2025
Expert Q&A

How do I verify a Merkle proof inside a Move smart contract

I'm trying to understand this aspect of the Sui Network because I'm either building, debugging, or deploying something that touches this area. I want a detailed explanation of how this mechanism or feature works, along with relevant CLI usage, Move code structure, or architectural concepts. My goal is to gain enough clarity to apply this knowledge in a real project—whether that's a custom smart contract, an NFT system, a wallet integration, or a DeFi tool. The Sui Network has unique features compared to EVM chains, so I'm particularly interested in what sets it apart and how that affects development best practices. It would help to have sample code, command line examples, or typical errors to watch for, especially when using the Sui CLI, SDK, or deploying on localnet/testnet. Ultimately, I want to avoid common mistakes, follow the best security principles, and ensure that the functionality I’m working on behaves as expected under realistic conditions.

  • Sui
  • Architecture
  • Move
5
5
Share
Comments
.

Answers

5
BigSneh.
Jul 24 2025, 14:24

How Merkle Proofs Work (Quick Refresher)

A Merkle proof consists of:

A leaf hash (typically the hash of the user's address or data)

A sequence of sibling hashes up to the root

The Merkle root, which is stored on-chain

A direction flag per level (left or right sibling)

You reconstruct the root by hashing the leaf with each sibling in the correct order and comparing the result to the stored root.


✅ Move + Sui-Compatible Approach

Move doesn’t support:

Arbitrary-length vectors (gas cost constraint)

Bitwise operations or SHA-256 (only Blake2b is available via Sui's crypto)

Generic third-party libraries (no OpenZeppelin equivalent yet)

But you can verify Merkle proofs using recursive hashing with BLAKE2b, as long as the data is structured right.


🛠️ Basic Merkle Verification Strategy in Sui Move

🔹 Step 1: Store the root hash

struct MerkleRoot has key { id: UID, root: vector, // 32-byte hash }

🔹 Step 2: Verify Proof

use sui::hash; use sui::crypto; use 0x1::vector; use 0x1::error;

public fun verify_merkle_proof( leaf: vector, proof: vector<vector>, index: u64, expected_root: vector ): bool { let mut hash = leaf;

let mut i = 0;
let len = vector::length(&proof);

while (i < len) {
    let sibling = vector::borrow(&proof, i);
    if ((index >> i) & 1) == 0 {
        // Current node is left, sibling is right
        hash = crypto::blake2b256(vector::concat(&hash, sibling));
    } else {
        // Current node is right, sibling is left
        hash = crypto::blake2b256(vector::concat(sibling, &hash));
    };
    i = i + 1;
};

hash == expected_root

}

index determines the sibling arrangement: we use bit shifts to guide L/R hashing.


🧪 Sample Usage

entry fun claim_reward( merkle_root: &MerkleRoot, user: address, proof: vector<vector>, index: u64 ) { let leaf = crypto::blake2b256(address::to_bytes(user));

let valid = verify_merkle_proof(
    leaf,
    proof,
    index,
    &merkle_root.root
);

assert!(valid, error::invalid_argument(0));

// Continue: mint NFT, give SUI, etc.

}


CLI Deployment

You can test locally by generating proofs off-chain using a Merkle tree library in TypeScript or Python. Then:

  1. Use sui move build to compile.

  2. Use sui client publish to deploy.

  3. Call claim_reward with:

A correct leaf (your address)

Proof array of hashes

index of your leaf

Stored Merkle root


Common Mistakes to Avoid

Problem Fix

Hash size mismatch Use blake2b256 for 32-byte output consistently Wrong sibling order Make sure your off-chain proof provides order + direction Gas too high Avoid deep trees or large vectors Data mismatch Double-check how you're encoding leaf data (address::to_bytes, etc.) Index misalignment Your index must be the correct path for that leaf in the Merkle tree


Testing the Contract

Here’s a basic test setup:

#[test_only] public fun test_verify_merkle() { let leaf = crypto::blake2b256(address::to_bytes(@0x123)); let proof = vector::empty<vector>(); // Add real sibling hashes here... let root = crypto::blake2b256(leaf); // Only for 1-node tree

let valid = verify_merkle_proof(leaf, proof, 0, root);
assert!(valid, 101);

}


🔄 Off-chain Proof Generator (TypeScript)

import { MerkleTree } from 'merkletreejs' import { blake2b } from 'blakejs'

function hash(x: string): Buffer { return Buffer.from(blake2b(Buffer.from(x), null, 32)) }

const leaves = ['0x123...', '0x456...'].map(hash) const tree = new MerkleTree(leaves, hash, { sortPairs: true })

const proof = tree.getProof(leaves[0]).map(x => x.data.toString('hex')) const index = 0 // depends on position in tree


Summary

Merkle proofs can be verified inside Move contracts using blake2b256. You must provide: leaf, sibling proof, and leaf index in the tree. You manage ordering logic manually since there's no bitwise type in Move. Generate trees + proofs off-chain (JS/Python) and use CLI/SDK to test. Store the MerkleRoot as a vector on-chain (via object/resource).


Let me know if you'd like:

A full airdrop claim system template using Merkle proofs

An NFT-gated whitelist mint

Scripts for batch proof generation from CSV

I'm happy to walk you through the integration.

2
Best Answer
Comments
.
Morgan.
Sep 7 2025, 22:20

Okay, let's dive into Sui's object model, particularly the distinction between Owned and Shared Objects, which is a cornerstone of Sui's design and a major differentiator from EVM chains. Understanding this is crucial for building efficient and scalable dApps.

Sui's Object-Centric Model: A Paradigm Shift Unlike EVM chains where state is primarily managed within accounts (e.g., an ERC-20 token is a number in a mapping within the token contract's state), Sui's blockchain is a global, programmable storage of objects. Every piece of data—from an NFT to a smart contract itself—is an object with a unique ID and a specific owner (or lack thereof, in the case of shared objects). This allows for a more direct, granular approach to state management and transaction processing.

1. Owned Objects: The Power of Parallelism

  • How it Works: An owned object is exclusively controlled by a single Sui address. When you create an object, it's typically owned by the address that created it. To mutate an owned object (e.g., change its properties, transfer it), the transaction must be signed by its current owner. Because only one owner can initiate a transaction on their owned object at a time, Sui's execution layer (Mysticeti/Narwhal/Bullshark) can process many such independent transactions in parallel. No global consensus is needed for two separate users to interact with their distinct owned objects simultaneously. This is Sui's secret sauce for high throughput.
  • Move Code Structure: Owned objects are typically represented by structs with the key and store abilities. For instance, a simple NFT struct MyNFT has key, store { id: UID, value: u64 }. Transferring an owned object uses sui::transfer::transfer(my_nft_object, recipient_address). To create an owned object that's initially owned by the transaction sender, you'd typically return it from an entry function.
  • CLI Usage:
    • sui client publish <path_to_package>: Publishes your Move package, creating an owned Package object.
    • sui client call --function create_my_nft --package <package_id> --module <module_name> --gas-budget 0.1: Calls a function that might create and transfer an NFT to your address (as the sender).
    • sui client transfer --recipient <recipient_address> --object-id <object_id> --gas-budget 0.1: Directly transfers an owned object.
    • sui client object <object_id>: Shows details, including the Owner field, which will be a specific address.
  • Best Practices: Design your dApps to leverage owned objects as much as possible. For example, instead of a single global vault holding all users' funds (a shared object), create an individual vault object for each user (an owned object). This maximizes parallelism and reduces contention.

2. Shared Objects: When Consensus is Needed

  • How it Works: A shared object is not owned by any single address. Multiple addresses can interact with and mutate it. Because concurrent modifications by different users could lead to conflicting states, any transaction interacting with a shared object must go through the full consensus protocol. This ensures that operations on that specific shared object are processed sequentially, maintaining consistency. While this introduces a bottleneck for that specific object, it doesn't block parallel execution of transactions involving other independent owned objects.
  • Move Code Structure: Shared objects are created explicitly using sui::transfer::share_object(object_to_share). When an entry function in Move needs to modify a shared object, its argument type will be &mut MySharedObject or &mut sui::object::Object<MySharedObject>. The sui::object::UID inside the shared object identifies it globally.
  • CLI Usage:
    • You create an object and then share it in an entry function: sui client call --function create_and_share_pool --package <package_id> --module <module_name> --gas-budget 0.1.
    • Interacting with a shared object: sui client call --function deposit_to_pool --package <package_id> --module <module_name> --args <pool_object_id> <amount> --gas-budget 0.1.
    • sui client object <object_id>: Shows Owner: Shared.
  • Best Practices: Shared objects should be used sparingly and only when truly necessary, for example, for global registries, immutable constants, or liquidity pools where multiple users genuinely need to interact with the same instance. When designing shared objects, optimize for minimal contention (e.g., batching operations if possible, or using smaller, more numerous shared objects if the logic allows).

Sui vs. EVM & Development Best Practices:

  • EVM Account Model: In EVM, all state changes go through the global state machine, implicitly requiring global consensus for every transaction. This limits parallelism. Funds are balanceOf[address], not distinct token objects.
  • Sui Parallelism: The object model allows Sui to execute a significant portion of transactions in parallel. Transactions touching only owned objects don't need global consensus. This is a massive throughput advantage.
  • Architectural Implications: Instead of monolithic smart contracts holding all user data, Sui encourages a design with many small, owned objects. For instance, a DeFi lending protocol might issue each user an owned Loan object instead of tracking all loans in a single, large shared contract. This makes your dApp inherently more scalable.
  • Security: Mutable references (&mut) to shared objects are powerful but must be handled carefully. Ensure your access control logic within Move prevents unauthorized modifications. If a contract is shared, anyone can call its entry functions, so robust checks are vital.
  • Common Mistakes & Errors:
    • Over-reliance on Shared Objects: New developers often default to shared objects, replicating an EVM-like global state. This negates Sui's parallelism benefits and can lead to transaction failures due to contention.
    • Incorrect Object Ownership: Attempting to mutate an owned object without being its owner will fail. Forgetting to transfer an object you created to the intended owner can also be an issue.
    • Missing &mut for Shared Object Args: When calling a function that modifies a shared object, the object reference in the Move entry function must be &mut. Forgetting this leads to compilation errors or runtime failures if the function tries to modify it.
    • Transaction Dry Runs/Debugging: Use sui client dry-run to test transactions locally without spending gas. When a transaction involving shared objects fails due to contention, you'll often see ExecutionStatus::Failure { status: MoveAbort ... 0x...::shared_object_module::Error::ObjectContention } or similar. Retrying after a short delay might help, or redesigning the contract to reduce contention.

By embracing Sui's object model and prioritizing owned objects, you'll build more efficient, scalable, and resilient applications on the Sui network.

8
Comments
.
Owen.
Owen4662
Jul 30 2025, 17:17

Sui does not provide a built-in Move library for Merkle proof verification. To verify a Merkle proof in a Move smart contract, you must implement the verification logic manually. Define a function that takes the leaf, the root, the proof (as a vector<vector<u8>>), and the index, then compute the hash path iteratively using a cryptographic hash function like BLAKE2b (available via sui::hash). Compare the computed root to the expected one. Ensure all input validations are in place to prevent malleability or bypass attacks. Since complex computations increase gas costs, optimize by limiting proof depth and avoiding storage-heavy operations. This approach is commonly used for light clients, bridge validations, or off-chain data attestations.

7
Comments
.
shamueely.
Jul 24 2025, 14:23

To verify a Merkle proof in a Move smart contract on the Sui Network, you need to reconstruct the Merkle root from the given leaf and proof nodes inside your smart contract, and then compare that result to the known root. In Sui, this process is slightly different from what you're used to on EVM chains because of the object-centric architecture and the limited built-in hashing functions. Instead of using keccak256 like in Solidity, you work with blake2b256, which is the supported hashing algorithm in Sui's standard library. First, you hash the leaf data using blake2b256, then loop through the vector of proof nodes (sibling hashes), combining each one with the current hash by concatenating them in sorted order, and rehashing at every step. This continues until you compute the root, which you then compare against the known root passed as a parameter or stored on-chain.

Here’s how you might structure that logic in Move:

use sui::hash::blake2b256;
use std::vector;

public fun verify_merkle_proof(
    leaf: vector<u8>, 
    proof: vector<vector<u8>>, 
    root: vector<u8>
): bool {
    let mut hash = blake2b256(&leaf);
    let len = vector::length(&proof);
    let mut i = 0;
    while (i < len) {
        let sibling = vector::borrow(&proof, i);
        let concat = if hash < *sibling {
            vector::concat(&hash, sibling)
        } else {
            vector::concat(sibling, &hash)
        };
        hash = blake2b256(&concat);
        i = i + 1;
    };
    hash == root
}

In your Node.js or Sui CLI environment, you can simulate the same hash logic off-chain using tools like merkletreejs to generate leaves, proofs, and the root, and pass those to your contract call. When deploying or testing this on localnet or testnet, you can publish your Move module with sui move build and sui client publish, then call your function with sui client call passing the correct serialized leaf and proof values. A common mistake is misordering the concatenation of hash pairs or not hashing the initial leaf properly. You should also validate that proof items are always 32 bytes long and that the result of your final hash matches the expected root exactly.

Here’s a CLI example:

sui client call \
  --package 0xYourPackage \
  --module your_module \
  --function verify_merkle_proof \
  --args 0xYourLeaf '[0xSibling1, 0xSibling2]' 0xRootHash \
  --gas-budget 10000000

The Sui-specific difference lies in how objects are managed—you might store your Merkle root in a has key object, then only allow specific admin capabilities to update that root. For security, always track usage of proofs to avoid replay attacks and log successful claims with emit_event for transparency. To go deeper, visit Sui’s Move documentation for more examples and architectural patterns tailored to object-based state management.

3
Comments
.

Do you know the answer?

Please log in and share it.