Inspiration

Two people want to buy something together online. Neither wants to send money first. One goes first and gets scammed — or nobody moves and the deal dies. Middlemen charge fees, require identity checks, and add a third party you still have to trust.

I kept thinking: this is exactly the kind of problem cryptography was supposed to solve. Two parties, one locked outcome, no human in the middle. When I discovered SimplicityHL and the Liquid Network, I realized the tools to build this properly finally existed. DuoPay is the result.


What It Does

DuoPay is a 2-of-2 co-buyer escrow contract written in SimplicityHL on the Liquid Network.

Both buyers lock their share of funds into the contract on-chain. The funds have exactly two possible outcomes — no more, no less:

Release path: Both buyers sign with their BIP-340 keys confirming delivery. The contract verifies both signatures via jet::bip_0340_verify and releases the full combined amount to the seller in Output[0].

Timeout path: If the deal collapses, both buyers wait for param::EXPIRY_TIME block height to pass. The contract enforces the timelock via jet::check_lock_time, then splits funds back — Output[0] to Buyer A, Output[1] to Buyer B.

No arbitrator. No admin key. No middleman. The chain enforces it.


How I Built It

I wrote duopay.simf in real SimplicityHL syntax, following the patterns from the Blockstream simplicity-contracts repository — specifically option_offer.simf.

The contract uses:

  • witness::PATH + Either<Left, Right> + match for two-path branching
  • param:: namespace for compile-time parameters (public keys, amounts, expiry)
  • witness::SIG_A and witness::SIG_B passed at spend time via .wit file
  • jet::bip_0340_verify for BIP-340 signature verification
  • jet::check_lock_time for timelock enforcement
  • jet::sig_all_hash for the transaction sighash
  • jet::add_64 and jet::eq_64 for amount arithmetic and verification

The Rust witness builder in main.rs handles constructing the .wit files for both spend paths using the LWK SDK.


Challenges I Ran Into

SimplicityHL is a new language with limited documentation and almost no Stack Overflow to fall back on. Every pattern had to be worked out from reading the existing contracts in the simplicity-contracts repo directly.

Getting the amount verification right was the trickiest part — unwrapping the explicit Liquid asset amount from jet::output_amount requires understanding the (Asset1, Amount1) pair type and using unwrap_right to extract the u64 value cleanly.

The two-path match structure also required careful thinking about what each witness carries and how param:: values interact with spend-time witnesses. There is no runtime debugging — you reason through it statically or it does not compile.


Accomplishments I'm Proud Of

The contract is written in real SimplicityHL syntax — not pseudocode, not a design sketch. Every jet, every namespace, every pattern matches what is actually in the Blockstream simplicity-contracts repo.

More importantly: the logic is correct. Exactly two terminal states. No unauthorized spend path. No runtime surprises. Both buyers can verify this before depositing a single satoshi — which is the entire point.

This is also the first co-buyer escrow design built natively for Simplicity's UTXO model. Most escrow solutions on-chain are single buyer ↔ single seller, rely on a human arbitrator, or inherit Ethereum's reentrancy risks. DuoPay has none of those problems.


What I Learned

Simplicity's "no loops, no recursion" constraint is not a limitation — it is a feature. Being forced to think in finite, bounded execution paths makes the contract logic cleaner and easier to reason about than any Solidity escrow I have seen.

I also learned how Liquid's explicit asset and amount model works at the jet level — how jet::output_amount returns a typed pair, and how to correctly verify that the right amount of L-BTC is going to the right output index.

Most of all: formal verifiability is not just a talking point. When you cannot rely on a debugger or gas estimation, you think much more carefully about what the code actually guarantees. That discipline is what makes Simplicity contracts worth building.


What's Next

  • Compile duopay.simf via the Simplex toolchain and deploy to Liquid Testnet
  • Build and test .wit witness files for both spend paths end-to-end
  • Add a Deal Link web interface using LWK SDK — create and fund contracts without touching the CLI
  • Client/Freelancer mode: single funder, single approver, same contract architecture
  • M-of-N variants: 3+ co-buyers using Simplicity's multi-input UTXO support
  • Optional dispute oracle: a third agreed-upon arbitrator signature as a third spend path

Built With

  • liquid-network
  • lwk-sdk
  • rust
  • simplex
  • simplicityhl
Share this project:

Updates