Withdrawals (L2 → L1)
A rollup executes transactions off the Main Chain, but value that should return
to L1 must be proven there. Each settlement batch commits its L2→L1 messages
(withdrawals) as a binary Merkle root, withdrawals_root. To withdraw, you prove
that your withdrawal's leaf is committed in a finalized batch's root by carrying
the sibling-hash path to that root in MsgExecuteWithdrawal.
The RDK ships the building blocks: binary-Merkle utilities, a proof assembler,
and the executeWithdrawal transaction.
The leaf encoding, the hash function, and odd-node handling must match the
network's withdrawals_root construction for the proof to verify on-chain. The
helpers default to a common convention (SHA-256, hash each leaf, duplicate the
last node on odd levels, no domain separation) but are fully configurable —
override MerkleOptions to match the chain exactly. A proof built with the wrong
convention will be rejected.
This guide and the RDK cover withdrawals only. There is no deposit message in the module, so L1→L2 deposits are not part of this kit's surface.
The flow
- Collect the batch's withdrawal leaves and find your withdrawal's index.
- Assemble the Merkle proof with
assembleWithdrawalProof. - Verify the assembled root matches the batch's
withdrawals_root. - Submit
MsgExecuteWithdrawalwithtx.executeWithdrawal(orqorollup withdraw), once the batch is finalized.
Assemble the proof
assembleWithdrawalProof turns the full list of a batch's withdrawal leaves and
your leaf's index into the sibling-hash proof the message carries, plus the
computed root.
import { assembleWithdrawalProof, hexToBytes } from "@qorechain/rdk";
// The batch's withdrawal leaves, in order, and your withdrawal's index.
const leaves = batchLeavesHex.map((h) => hexToBytes(h));
const withdrawalIndex = 3;
const proof = assembleWithdrawalProof(leaves, withdrawalIndex);
console.log(proof.proof); // Uint8Array[] — sibling hashes, leaf level first
console.log(proof.withdrawalsRoot); // Uint8Array — compare against the batch's
console.log(proof.withdrawalIndex); // 3
If your network uses a different leaf hash or odd-node rule, pass MerkleOptions
to every helper so the construction stays consistent:
import { assembleWithdrawalProof } from "@qorechain/rdk";
const options = {
hash: myHashFn, // default: SHA-256
hashLeaves: true, // hash each input leaf before building the tree
duplicateOdd: true, // on an odd level, duplicate the last node
};
const proof = assembleWithdrawalProof(leaves, withdrawalIndex, options);
Verify before you submit
Confirm the assembled root matches the batch's committed withdrawals_root, and
sanity-check the proof locally with verifyBinaryMerkleProof:
import { verifyBinaryMerkleProof } from "@qorechain/rdk";
const ok = verifyBinaryMerkleProof(
leaves[withdrawalIndex], // the leaf
withdrawalIndex,
proof.proof, // sibling hashes
proof.withdrawalsRoot, // the root the proof builds to
);
console.log(ok); // true if the leaf is committed under that root
You can also compute a root directly with binaryMerkleRoot(leaves, options) or
build a raw proof with binaryMerkleProof(leaves, index, options).
Submit MsgExecuteWithdrawal
buildExecuteWithdrawalInput combines the recipient/amount details with the
assembled proof, then tx.executeWithdrawal broadcasts it. The signer's address
is the submitter. The batch must be finalized (past its challenge window for
optimistic settlement, or proof-verified for ZK).
import { buildExecuteWithdrawalInput } from "@qorechain/rdk";
const tx = await rdk.connectTx(signer, { gasPrice: "0.025uqor" });
const input = buildExecuteWithdrawalInput({
submitter: tx.address,
rollupId: "my-roll",
batchIndex: 7,
recipient: "qor1recipient...",
denom: "uqor",
amount: "1000000",
withdrawal: proof,
});
const res = await tx.executeWithdrawal({
rollupId: input.rollupId,
batchIndex: input.batchIndex,
withdrawalIndex: input.withdrawalIndex,
recipient: input.recipient,
denom: input.denom,
amount: input.amount,
proof: input.proof,
});
console.log(res.transactionHash);
The CLI equivalent
qorollup withdraw reads a JSON spec, assembles the proof, and submits it. Use
--dry-run to assemble and report the proof without broadcasting.
qorollup withdraw --file withdrawal.json --dry-run
qorollup withdraw --file withdrawal.json
withdrawal.json:
{
"rollupId": "my-roll",
"batchIndex": 7,
"recipient": "qor1recipient...",
"denom": "uqor",
"amount": "1000000",
"leaves": ["<hex leaf 0>", "<hex leaf 1>", "..."],
"index": 3
}
The CLI uses the default Merkle convention. If your network differs, assemble the
proof in code with the matching MerkleOptions instead.
Next
- Monitoring — confirm a batch finalized before withdrawing.
- Settlement paradigms — when a batch is final.
- qorollup reference — the
withdrawcommand.