Twine: Buy and Sell CKB Without Depositing on an Exchange (Fiber P2P PoC)

Update: sell-only book, and JIT inbound at release

1. The book is sell-only

A post offers CKB. The poster is the seller. The taker is the buyer. There is no buy post.

The last build had both sides: sell (offering CKB) and buy (bidding for CKB). A buy post made the taker the seller. take had no invoice. Both sides got need-invoice, and the buyer sent payout-invoice before any hold existed.

That path is gone.

  • new-order with side other than sell is refused.
  • Taking a leftover buy post is refused.
  • take always carries the buyer’s payout invoice. The hold is created there. No need-invoice round trip on take.
  • The app no longer has Buy / Sell tabs. Posts is one list of sell offers. A buy post on the relay is hidden.
  • New post has no side picker. It always sells. Confirm is Sell CKB?

You buy CKB by taking someone else’s sell. You sell by posting.

sequenceDiagram
    autonumber
    participant BN as Buyer node
    participant B as Buyer
    participant D as Daemon
    participant O as Operator node
    participant S as Seller

    S->>D: new-order (sell)
    BN->>BN: mint S_b, invoice under H(S_b)
    B->>D: take with that invoice
    D->>O: hold under H(S_b), no preimage
    D-->>S: pay-invoice
    D-->>B: pay-invoice
    Note over O: no S_b, so no settle and no force-close claim
    S->>O: pays the hold
    O-->>D: Received
    D-->>B: waiting-fiat
    D-->>S: waiting-fiat
    B->>S: fiat, outside Twine
    B->>D: fiat-sent
    S->>D: release
    D->>D: record the hash before paying
    D->>O: send_payment
    O->>BN: pays H(S_b)
    O-->>D: payment_preimage is S_b
    D->>O: create_preimage, then settle
    D-->>B: settled
    D-->>S: settled

2. JIT inbound at release

A Fiber payment needs inbound on the receiver. Capacity is not inbound. A channel the buyer funds alone cannot receive. Asking every buyer to dual-fund a channel before they take a small trade is a bad first step, especially on a laptop with no public IP.

On release, if Twine cannot already pay the buyer, the daemon opens a private one-way channel to them, funded from the Twine Fiber node.

That is JIT inbound. It happens at payout, not at take.

sequenceDiagram
    autonumber
    participant S as Seller
    participant D as Daemon
    participant O as Twine Fiber
    participant B as Buyer node

    S->>D: release
    D->>O: list_channels toward buyer
    alt outbound already covers the trade
        D->>O: send_payment
    else no inbound
        D->>O: connect_peer if needed
        D->>O: open_channel one_way, private, trade + reserve, min 100 CKB
        O-->>B: channel confirms
        D->>O: send_payment
    end
    O-->>D: payment_preimage is S_b
    D->>O: create_preimage, then settle

If the buyer already has inbound, Twine pays and does not open. If they are already peered, it does not dial again. If the node is offline, it does not hang on the hold. It asks for a new invoice. The buyer reconnects, stays online, and retries. A pending open is not paid twice.

The hub now publishes its listen addresses on kind 31421. Take can include a public Fiber listen address. Loopback and LAN addresses are refused. A laptop connects outbound to the hub. It does not need a public IP.

What you do

Role Before the trade At payout
Seller Post the offer. Open and fund a channel to the Twine hub so you can lock the hold. Release, same as before.
Buyer Take that sell. Connect outbound to the hub. Leave auto-accept on. Keep about 99 CKB on-chain. Stay online. Twine opens inbound to you if you cannot receive yet.

The app says this on Post, Take, and Trade. Post and Take show a channel checklist. Seller has to confirm outbound before they can post or take. Buyer is told to stay online so Twine can open inbound at payout.

What this spends

The Twine node now spends on-chain CKB to fund those channels. Keep spare CKB on it. The RPC biscuit grew: read / write on channels and peers, on top of invoices, payments, node, and watchtower.

read("node");
read("invoices"); write("invoices");
read("payments"); write("payments");
read("channels"); write("channels");
read("peers"); write("peers");
write("watchtower");
node("<this fnn pubkey>");

A one-way channel only lets the opener send. Twine is the opener. A buyer-funded one-way still cannot receive.

3. Deploy

There is a VPS install path: DEPLOY.md. Fiber 0.9.1, port 8228 public, RPC on localhost, systemd for the daemon. The README covers the CKB key, the biscuit, and announced_addrs for wallets that are not on the same machine.

What this does not change

  • Fiat is still off the protocol.
  • The daemon still stores no preimage at take.
  • A failed payout still does not settle. The buyer can still send another invoice with the same payment hash.
  • The 36-hour hold, the 15-minute payment window, and the 30-minute safety margin are the same.
  • Disputes are the same.

Tests (cargo test in the daemon, mock Fiber)

  • a_buy_post_cannot_be_created_or_taken
  • a_post_must_be_sell
  • payout_skips_jit_when_the_buyer_already_has_inbound
  • payout_opens_a_one_way_channel_when_the_buyer_has_no_inbound
  • payout_skips_connect_when_the_buyer_already_peered
  • payout_asks_for_a_new_invoice_when_the_buyer_node_is_offline
  • a_buyer_funded_one_way_channel_cannot_receive
  • operator_funded_one_way_can_pay

The app tests that Buy / Sell tabs are gone, that a buy post is hidden, and that the buyer checklist is JIT inbound at payout.

Code

Ask for review

@ArthurZhang @quake — please look at this JIT path. The hold hash is still the buyer’s. The new piece is that the operator node now opens a private one-way channel at release when there is no route.

I want blunt notes on:

  1. Opening at release, not at take. The buyer has to stay online through an on-chain channel open while the hold is already Received.
  2. Funding with trade + 99 CKB reserve, floored at 100 CKB so a default Fiber node auto-accepts. one_way: true, public: false.
  3. The biscuit now includes write("channels") and write("peers").
  4. If the open is slow, the 30-minute safety margin can still refund the seller after fiat was marked sent.
  5. Anything I am missing on Fiber auto-accept, one-way channels, or a laptop that only connected outbound.
2 Likes