Technical guide
Why Commit-Reveal Actually Works (And Where It Quietly Doesn't)
A technical walkthrough of commit-reveal as implemented in TesseractBuffer, why it defeats mempool MEV, and the failure modes you still have to think about.
Commit-reveal is one of those primitives that sounds like cryptography theatre when you first see it (“you’re just doing two transactions instead of one?”) and starts to feel obvious after you understand the threat model. This is a tour through how Tesseract implements it, what attacks it actually neutralises, and the failure modes a serious integrator should still think about.
The mempool problem in one paragraph
When you submit a swap to a public mempool, every searcher in the world can read it before it lands in a block. If your trade is large enough to move a price, they can sandwich it: front-run your trade, let your trade move the price, then back-run to capture the spread. Estimates of total MEV extracted in 2024 ran into the billions; for some pools the realised slippage on a “fair” swap is close to 100% of the theoretical max. Private mempools (Flashbots Protect, MEV Blocker, etc.) help, but they trust a different operator and not all chains have an equivalent.
Commit-reveal solves this without trusting an operator. The trade-off is one extra round trip.
What TesseractBuffer does, mechanically
The relevant signature in TesseractBuffer.vy is:
@external
def buffer_transaction_with_commitment(
tx_id: bytes32,
origin: address,
target: address,
commitment: bytes32,
dependency: bytes32,
deadline: uint256,
swap_group_id: bytes32,
refund_recipient: address,
):
...
Notice what’s not in the arguments: the actual payload. The contract stores commitment = keccak(payload || secret) and nothing else from the trade itself.
A searcher reading the mempool sees:
tx_id(a random 32-byte ID),origin/target(addresses, but they’re protocol-level wallets, not “this trader is about to dump 5,000 ETH”),commitment(opaque 32 bytes),deadline,swap_group_id,refund_recipient.
There is nothing actionable in there. The commitment hash is computationally infeasible to invert. The swap_group_id might leak that you’re doing a multi-leg swap, but not which assets or amounts. Even knowing the addresses tells the searcher essentially nothing — the actual payload (token addresses, amounts, slippage) is hidden.
After the commitment is mined, the user sends:
@external
def reveal_transaction(
tx_id: bytes32,
payload: Bytes[512],
secret: bytes32,
):
assert keccak256(concat(payload, secret)) == self.commitments[tx_id]
self.payloads[tx_id] = payload
...
Now the payload is public. But — and this is the load-bearing part — the commitment is already in a block. Searchers cannot reorder the trade out of the block it’s in. They cannot insert a front-run, because front-running requires knowing what to front-run before the trade is mined. By the time the reveal makes the payload visible, the ordering battle is over.
Why two blocks of delay matter
The MIN_RESOLUTION_DELAY parameter (defaulting to 2 blocks) sits between reveal and resolve. This is what eats the flash loan vector.
A flash loan, by definition, must repay within the same transaction. A flash loan attack on a buffered swap would need to:
- Borrow assets.
- Manipulate state.
- Resolve the buffered swap at a price advantageous to the attacker.
- Repay the loan.
All in one transaction, all in one block. The 2-block delay between reveal and resolve makes step 3 impossible: the protocol will not let resolve_dependency execute until the commitment has aged enough. The flash loan must repay this block. The resolution can’t happen this block. The attack can’t close the loop.
This is a structural mitigation, not a probabilistic one. It works regardless of how clever the attacker is, because the constraint is “a flash loan executes in one block, the protocol requires two.”
Where the protection ends
Commit-reveal does not protect against:
Time-bandit reorgs on the source chain. If a chain’s consensus allows a deep enough reorg to remove the commitment block, a sophisticated attacker could in principle re-order the commitment and reveal. In practice, Ethereum and modern rollups have economic finality well within Tesseract’s deadline windows; this attack is theoretical. It does mean Tesseract’s safety margin shrinks proportional to a chain’s reorg depth, which is one reason the relayer tracks chain-specific finality requirements.
Targeted infrastructure attacks on the user. If the attacker controls your RPC endpoint, they see your reveal transaction before broadcasting it and can grief you in other ways (e.g. refuse to broadcast, charge you for fake gas estimates). Commit-reveal is a mempool defense, not an RPC defense. Use a real RPC.
Knowing the user identity is itself information. If you are the only person in the world who would post a 50,000 ETH swap, the address alone tells a searcher everything they need. Commit-reveal protects the trade contents, not the trader’s identity. For most users this is fine; for whales it argues for fresh addresses per swap.
Correlated side-channel leaks. If you commit on chain A and someone watches your wallet send a related approval on chain B, they may infer what’s coming. The protocol can’t fix social information leakage.
The cost: one extra transaction
The honest answer to “why doesn’t everyone use commit-reveal” is it costs an extra transaction. On Ethereum mainnet that’s economically painful; on L2s it’s negligible.
For Tesseract, the cost is roughly:
buffer_transaction_with_commitment: ~150,000 gasreveal_transaction: ~80,000 gasresolve_dependency: ~100,000 gas
Total: ~330,000 gas per leg on the source chain. On Base or Optimism this is sub-cent. On Arbitrum it’s a few cents. For a 4-figure swap that’s a rounding error; for a 6-figure swap it’s the cheapest insurance you’ll ever buy.
What the integrator has to do
If you’re integrating Tesseract into a wallet, DEX, or aggregator, here’s the checklist:
- Generate the secret client-side. It must come from a high-entropy source.
crypto.getRandomValuesin browser,os.urandomin Python,crypto/randin Go. Never derive it from anything the attacker can predict. - Hash with the canonical concatenation.
keccak256(payload || secret), exactly as the contract expects. Get this wrong and reveals will fail verification. - Hold the secret in memory until reveal. If you persist it, persist it encrypted; if the attacker grabs the secret before you reveal, they can grief you by revealing first with the wrong payload (which will fail the assertion and just waste your gas — but it’s still a UX bug).
- Watch the chain for commit inclusion. Don’t issue the reveal until the commit is at least 1 confirmation deep. If a reorg replaces the commit block, you’ll need to re-commit.
- Use a single broadcast endpoint for both transactions. If commit goes to RPC A and reveal goes to RPC B, you’ve widened your attack surface.
- Test the unhappy path. What happens if the user closes the tab between commit and reveal? You need to handle the refund path: after the deadline expires, anyone can trigger the refund and the user gets their gas back minus the failed commit cost.
The takeaway
Commit-reveal is not magic. It is a deliberate, slightly clunky two-phase protocol that trades one block of latency for structural defeat of mempool MEV and single-block flash loans. For protocols that hold real value and care about user outcomes, that trade is among the most lopsided wins available in DeFi design.
Tesseract bakes it into the base layer specifically so that integrators don’t have to reinvent it (badly) per dApp. If you’re building anything that does cross-rollup swaps in 2026 and you’re not committing trades before revealing them, you are leaking value to MEV bots — and the math says it adds up faster than most teams think.
For the contract source, see TesseractBuffer.vy on GitHub.