Blockstream Turin Simplicity Hackathon · 2026

DuoPay

Trustless Co-Buyer Escrow on Liquid Network

Two buyers. One seller. Zero middlemen.
A SimplicityHL smart contract that holds funds until both parties confirm — then releases automatically. No trust required.

NetworkLiquid Testnet
LanguageSimplicityHL
Pattern2-of-2 Escrow
Contractduopay.simf

01 — The Problem

Two people.
One purchase.
No safe way to pay.

Buyer A goes first

Sends their share and waits. If Buyer B or the seller disappears — money is gone. No recourse, no reversal.

Buyer B goes first

Same problem, reversed. Someone always absorbs all the risk. A trust deadlock with no exit and no on-chain resolution.

Use a middleman

Escrow services charge fees, require identity checks, and add a third party you still have to trust. That's the whole problem.

02 — How It Works

The DuoPay
contract flow

Co-Buyer A
Alice
Deposits share
Co-Buyer B
Bob
Deposits share
duopay.simf · SimplicityHL
DuoPay Escrow
Funds locked on Liquid Network
witness::PATH = Left
Both sign via jet::bip_0340_verify → full amount to seller

witness::PATH = Right
jet::check_lock_time(param::EXPIRY_TIME) passes → deposits returned
Left(()) — release
Outcome A
Seller Paid
Output[0] → seller
Right(()) — timeout
Outcome B
Both Refunded
Output[0]+[1] → buyers

03 — The Contract

duopay.simf

/*
 * DuoPay — 2-of-2 Co-Buyer Escrow
 * Left  → release: both buyers sign → seller paid
 * Right → timeout: expiry passed + both sign → refund
 *
 * Parameters set at compile time via param:: namespace:
 *   BUYER_A_PUBKEY, BUYER_B_PUBKEY — BIP-340 public keys
 *   EXPIRY_TIME                    — block height for refund unlock
 *   BUYER_A_AMOUNT, BUYER_B_AMOUNT — deposits in satoshis
 */

fn get_output_explicit_amount(index: u32) -> u64 {
    let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index));
    let (_, amount): (Asset1, Amount1) = pair;
    let amount: u64 = unwrap_right::<(u1, u256)>(amount);
    amount
}

fn check_signature(pubkey: Pubkey, sig: Signature) {
    let msg: u256 = jet::sig_all_hash();
    jet::bip_0340_verify((pubkey, msg), sig);
}

fn release_path(sig_a: Signature, sig_b: Signature) {
    check_signature(param::BUYER_A_PUBKEY, sig_a);
    check_signature(param::BUYER_B_PUBKEY, sig_b);
    let (carry, total): (bool, u64) =
        jet::add_64(param::BUYER_A_AMOUNT, param::BUYER_B_AMOUNT);
    assert!(jet::eq_1(<bool>::into(carry), 0));
    assert!(jet::eq_64(get_output_explicit_amount(0), total));
}

fn timeout_path(sig_a: Signature, sig_b: Signature) {
    jet::check_lock_time(param::EXPIRY_TIME);
    check_signature(param::BUYER_A_PUBKEY, sig_a);
    check_signature(param::BUYER_B_PUBKEY, sig_b);
    assert!(jet::eq_64(get_output_explicit_amount(0), param::BUYER_A_AMOUNT));
    assert!(jet::eq_64(get_output_explicit_amount(1), param::BUYER_B_AMOUNT));
}

fn main() {
    let sig_a: Signature = witness::SIG_A;
    let sig_b: Signature = witness::SIG_B;
    match witness::PATH {
        Left(params: ())  => release_path(sig_a, sig_b),
        Right(params: ()) => timeout_path(sig_a, sig_b),
    }
}

Written in real SimplicityHL syntax — same patterns as option_offer.simf in the Blockstream simplicity-contracts repo. Uses jet::bip_0340_verify, jet::check_lock_time, jet::sig_all_hash, jet::add_64, jet::eq_64 — all documented jets. witness::PATH, param:: namespaces match official contract structure exactly.

04 — Why Simplicity

Not just any chain.
Specifically this one.

01

No Surprise Failures

No loops, no recursion. Exactly two outcomes: release or refund. Both buyers can verify the complete execution path before depositing a single satoshi.

02

Formally Verifiable

Both parties can prove no unauthorized spend path exists. The code is the guarantee — not a company promise, not a terms of service, not a human arbitrator.

03

UTXO Isolation

Each DuoPay contract is an isolated UTXO. Bugs in other contracts cannot drain yours. Reentrancy attacks are structurally impossible — not just prevented by code.

04

Predictable Fees

Statically bounded execution cost. Both buyers see the exact fee before committing. No gas estimation games. No failed transactions from insufficient gas.

Why not Ethereum / Solidity?

Reentrancy Attacks
Ethereum

Global state allows malicious contracts to call back mid-execution and drain an escrow before the transfer completes. A well-known, recurring attack vector.

Simplicity

UTXO model means each contract is an isolated coin. There is no shared state to re-enter. The attack is structurally impossible, not just guarded against.

Gas Costs
Ethereum

Gas can spike unexpectedly. Transactions can fail out-of-gas at runtime. Buyers cannot know exact costs before committing funds.

Simplicity

Execution cost is statically bounded at compile time. Both buyers see the exact fee before signing anything. No surprises.

Arbitrator Dependence
Ethereum

Most production escrows on Ethereum still include an admin key or arbitrator address — a human who can be bribed, hacked, or simply go offline.

Simplicity

DuoPay has no admin key. No arbitrator. The timeout path is enforced by the chain itself via jet::check_lock_time. No third party exists to corrupt.

05 — The Vision

This is just
the beginning.

DuoPay is designed to become a full trustless payment platform — eliminating the security risks of WhatsApp, Telegram, and TikTok payments entirely. Every transfer verified on-chain. Every deal sealed with code, not promises.

Deal Links

Create a deal, share a link. The other party sees terms verified on-chain and deposits. No screenshots. No "trust me." The blockchain is the receipt.

Deal Dashboard

Create, track, and manage deals in a clean interface. Deposit status verified in real time on-chain — no edited screenshots, no "I sent it, check again."

Mode 1 — Co-Buy
Two buyers, one seller
Friends splitting a purchase. Neither goes first. Both deposit, both confirm, seller gets paid. Either backs out — both refunded after timeout.
A deposits B deposits Both confirm Seller paid
Mode 2 — Client / Freelancer
One client, one builder
Client locks payment upfront. Freelancer delivers work. Client approves and releases. No invoices ignored. No work stolen. No WhatsApp payment drama.
Client deposits Work delivered Client approves Builder paid

06 — Who Needs This

Real people.
Real problems.

Friends splitting a big purchase
Laptop, TV, appliances — neither wants to go first.
Consumer
Designers & developers getting paid
Client locks funds before work starts. No ghosting. No unpaid invoices.
Freelance
Online marketplace co-buys
Two collectors buying rare items from an unknown seller.
E-commerce
Cross-border purchases
Where traditional escrow doesn't exist or costs too much to access.
Global
Liquid asset co-investment
Two investors jointly funding a tokenized asset on Liquid Network.
DeFi
Business partners paying a vendor
Both partners fund a contract. Vendor is paid only when both approve delivery.
SME

07 — Roadmap

What's built.
What's next.

v1 — Hackathon Scope
Contract design & spend path logicFull 2-of-2 escrow with release and timeout paths, written in real SimplicityHL syntax.
Rust witness builder scaffoldingmain.rs structure for building .wit files for both spend paths using LWK SDK.
Compile via Simplex toolchainduopay.simf → compiled and tested against Liquid Testnet using Simplex.
Witness files for both pathsrelease.wit and timeout.wit — covering the happy path and the refund path end-to-end.
v2 — Post-Hackathon
Deal Link web interfaceLightweight JS frontend using LWK SDK — create and fund DuoPay contracts without touching the CLI.
Client / Freelancer modeSingle funder, single approver — the second operating mode using the same contract architecture.
M-of-N co-buyer variants3+ co-buyers using Simplicity's multi-input UTXO support. Group buys, community purchases, multi-partner deals.
Optional dispute oracleA third agreed-upon arbitrator signature unlocks a third spend path — for cases where both parties genuinely disagree.
Partial settlement pathIf one buyer confirms but the other goes silent, a secondary timeout releases proportional funds after an extended window.