Skip to content

Instantly share code, notes, and snippets.

Last active March 11, 2024 14:05
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save moonsettler/6a214f5d01148ea204e9131b86a35382 to your computer and use it in GitHub Desktop.
Save moonsettler/6a214f5d01148ea204e9131b86a35382 to your computer and use it in GitHub Desktop.
Darkpool (tarpit) concept (work in progress)

Darkpool (tarpit)

Darkpool: A chaumian blinded ecash like co-op mixing pool for self custodial savings accompanying Mints


Darkpool is a privacy preserving cooperative self-custody pool on bitcoin, utilizing taproot n-of-n musig on the key path and CTV (OP_CHECKTEMPLATEVERIFY) settlement tree on the script path. Tarpit is the name chosen for the proof of concept implementation project of darkpools.


  • Economic: Single on-chain UTXO, fee for state transitions is shared by all participants, small on-chain footprint.
  • Private: Fungible denominations and blind signatures provide ecash like privacy.
  • Byzantine proof: Preserves property rights with no offline risk, clients can't advance an invalid state.
  • Interactive: All participants have to be periodically online to sign a state transition via n-of-n schnorr musig.
  • Sovereign: All participants can initiate and drive forward withdrawal, an on-chain partial or final settlement.
  • Standalone recovery: Even in case of a disastrous crash and loss of state backups unaided recovery is possible.
  • Robust: The full CTV settlement tree is determinsitically generated from the current vTXO set.

Basic operation

The root UTXO, with the full balance in the pool, will be locked to a taproot output, where the n-of-n musig key path allows the pool to cooperatively advance with full keyset interaction. Alternatively a fallback mechanism exists on the script path that is the CTV settlement tree that ends up pushing the vTXOs on-chain if carried out in full. Each node will have a reduced m-of-m key path applicable to that sub-tree. The leaves would be taproot addresses that lock into a 2-of-2 musig on the key path for the aggregated key of the pool operator and the Xi key. This way the private key xi can be revealed to the pool operator in exchange for immediately spendable ecash. The leaf script path would be relatively time locked single sig spend with xi (where xiG = Xi). The pool operator may also cooperate with a musig spend before the time lock expires. Due to the time lock mechanism a pool operator, that provides immediate withdrawal, has offline risk in that case, in the time lock period of leaves he acquired the private key for.

State transition from pubkeys (X1..Xn) to pubkeys (Y1..Yn)

  1. Coordinator generates a new private-public key pair
  2. Authenticate with Xj, get hash(Yi) blind signed by coordinator
  3. Let the coordinator know to include hash(Yi) (show blind signature)
  4. Coordinator reveals hash committments to (Y1..Yn)
  5. Users reveal (Y1..Yn) (settlement tree)
  6. Users ensure their outputs are included in settlement
  7. (X1..Xn) commit to a new random nonce and publish commitment
  8. Publish random nonce when all commitments by (X1..Xn) are received
  9. Everyone publishes the signature part for the tx that spends the pool utxo to the new pool utxo...
  10. Signature parts are aggregated and if everything went well the transaction is broadcasted

Simple pool-state transition with internal payments

  • (X1..Xn) -> (E1..En) committments to ephemeral keys (ownership can change here, internal payments)
  • (Y1..Yn) -> (H1..Hn) commitments to (Y1..Yn)
  • (E1..En) -> (B1..Bn) blinded (H1..Hn)!
  • (B1..Bn) -> [(H1..Hn)(C1..Cn)] coordinator signing + user unblinding!
  • (H1..Hn) revealed through the coordinator (coordinator checks authorizations).
  • (Y1..Yn) revealed (settlement tree) hash(Yi) inclusion in (H1..Hn) checked by all!
  • (E1..En) signs hash(Y1..Yn) to authenticate the new state (coordinator can't cheat)
  • Everyone generates CTV settlement tree for (Y1..Yn) independently.
  • Everyone creates a transaction tx0 (X1..Xn) -> tx1 (Y1..Yn) independently.
  • (X1..Xn) musig signs a state transition to the new root UTXO of the CTV settlement tree to (Y1..Yn)
  • If anyone signs the transaction to a different output, the musig session failes.

Note blind signing committments instead of public keys guard against coordinator attacking DKG.

Cooperative Recovery

Users that experience a node crash may rejoin by receiving the following information from a fellow user or the coordinator:

  • Pool Template (should have a backup)
  • Anchor UTXO (SPV proof may be provided to light clients)
  • Round number (increments with every mix round)
  • Keyset (X1..Xn)

Standalone (unaided) Recovery

Users can recover from their seed phrase that their keys are derived from, and the static Pool Template Backup. To aid this recovery, the CTV distribution script will contain an inscription in the witness script with the following content: 128 bit pool_id, 32 bit round. Keys (Xi) are derived along the following dervation path: m/86'/0'/<round>/<index>, for example for round 999 and the first key would be m/86'/0'/999/0. Clients in Standalone Recovery mode will scan the blockchain (or use an indexer service) for witnesses where the pool_id is inscribed to find their ejected leaves, that have a specific deterministic construction from:

  • Pubkey Xi
  • Pool pubkey P

Ways to spend the settlement tree leaves:

  • Key path: 2-of-2 musig (Xi + P)
  • Script path: relative time locked (default 24h) Xi, which can be used in recovery

The locking logic is analogous to the following pseudo-script:


TXID malleability prevents the 2-of-2 path from being useable before the vTXO hits the chain and becomes a UTXO.

WARNING: without having access to the static Pool Template Backup standalone recovery from seed is perilous. One would not be able to easily find the settlement transactions, and would have no idea if their leaves already hit onchain, or not. It would also be hard to tell what derivation paths to try under the assumption that the non cooperative leaves have been ejected.

Lightning interoperability and other immediate 2-of-2 exits

With CTV + CSFS, the following locking logic could be used to create a non-interactive channel (NIC), or to cooperatively spend to anywhere. Can be used to fund a standard lightning channel as well:

  # witness: template, signature
  OVER <Xi + P>
  # template, signature, template, pubkey
  # template

This signature on the CTV template as message is not subject to malleability, therefore the lightning channel opened off-chain could be used to spend even before the vTXO hits the chain. A part of the balance would be committed to fees in case the following transaction gets pushed on-chain, after spending the remaining balance, this fee may be credited back to the user in the form of ecash tokens, if he/she gives up the xi private key. At this point the user has nothing to lose as his remaining sats would go to fees. The coordinator may be able to substantially compact the off-chain state if he collects enough private keys.

Simple unidirectional NIC (non-interactive channel)

That is immune to TXID malleability and can be natural part of pool vTXOs (virtual UTXOs) from user Alice where you have a coordinator Bob that acts as an LSP:

  # witness: <sigP> <sigXi> <template>
  # witness: <sigXi>

You would have an alternative spending branch with a 24h timelock and Alice single sig. also have a 2-of-2 musig keyspend naturally.

Alice can keep giving Bob newer more favorable distributions. Bob gates them with his single sig.

What Alice would do is give Bob a new <sigAlice> <template> pairs and the new outputs that hash to the template to move funds to Alice. outputs can include HTLCs and then their updated settlement.


Bi-directional LN channels can also be grafted to vTXOs using CHECKTEMPLATEVERIFY (CTV) + CHECKSIGFROMSTACKVERIFY (CSFSV) + INTERNALKEY (IK) also known as the LNhance proposal.

# S = 500000000
# witness: <sig> <state-n-hash>

before funding sign first state template with the initial settlement!

# state-n-hash { nLockTime(S+n), out(contract, amount(Xi)+amount(P)) }
# settlement-hash { nSequence(2w), out(Xi, amount(Xi)), out(P, amount(P)) }

# contract for state n < m
  # witness: <sig> <state-m-hash>
  <settlement-hash> CTV

CLTV ensures only a larger nLockTime transaction can spend the current on-chain state, the relative timelock for the last co-signed state's CTV distribution is committed to in the settlement-hash

Without CSFS

With only CTV available, the following could be used to open a predetermined NIC back to the coordinator. The hashlock prevents the coordinator from executing it against the users will as the NIC would have a 2 week timeout:

  # witness: <signature> <preimage>
  SHA256 <hashlock> EQUALVERIFY

Alternatively without the hashlock, the user would trust the coordinator not to advance this output on the NIC path without his request. This action would cost the coordinator on-chain fees and the user would still have unilateral timelocked exit from the NIC. However this does make things simpler and the state smaller:

  # witness: <signature>

Paying fees in the non-cooperative case PPTC

The unpredictability of the future fee market makes it hard to precommit to the fees for the settlement transactions. Also someone could broadcast a CTV settlemenet transaction instead of further cooperation as a form of griefing for practically free. So instead we shall make the one that broadcasts a settlement transaction pay the full fee amount. The method of paying fees called PPTC (Parent Pays Through Child) wher ethe entire amount of the parent goes into fee for the child which technically pays the parent's fees for inclusion via CPFP (Child Pays For Parent).

Each node in the settlement tree will have two CTV templates, one each for the first and second input indexes and the same output distribution. The first input script is the target of the parent output, the second input is preserved for the fee. The outputs are predetermined full distribution of the first input funds.

An ephemeral UTXO is created when somone tries to move the CTV contract forward, sent to the second input address with enough fee for mempool inclusion, but not enough for immediate confirmation. The entire value of ephemeral the UTXO created can only go into fees for the CTV settlement transaction as the output amounts are predetermined and fulfilled by the first input funds. The UTXO is called ephemeral because ideally it only exists intra-block.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment