Skip to content

Instantly share code, notes, and snippets.

@RobinLinus
Created May 20, 2026 11:07
Show Gist options
  • Select an option

  • Save RobinLinus/7fbfcf4110d010222b1e0b8f972ec351 to your computer and use it in GitHub Desktop.

Select an option

Save RobinLinus/7fbfcf4110d010222b1e0b8f972ec351 to your computer and use it in GitHub Desktop.
BIP300-like withdrawals with BIP448-style covenants

BIP300-like withdrawals with BIP448-style covenants

This scheme emulates the slow miner-voting part of BIP300 using the covenant tools proposed in BIP448.

It is not full BIP300. It only supports fixed-denomination withdrawals to a fixed, permissioned set of recipients.

Assumptions

fixed withdrawal denomination D
fixed recipient set R = {recipient_0, recipient_1, ...}
one active withdrawal attempt per coin
one ACK or NACK transition per block
approval after T ACKs
rejection after T NACKs

For BIP300-like parameters:

T = 13,150

State

For each recipient r, the withdrawal attempt is represented by a two-dimensional counter state:

State(r, a, n)

where:

a = ACK count
n = NACK count
0 ≤ a < T
0 ≤ n < T

From each live state there are exactly two valid transitions:

ACK  -> State(r, a + 1, n)
NACK -> State(r, a, n + 1)

At the boundary:

if a + 1 = T:
    ACK -> payout D to recipient r

if n + 1 = T:
    NACK -> reset / redeposit back to idle covenant

The number of live states per recipient is:

T² = 13,150² = 172,922,500

For about 100 recipients this is about 2^34 possible states.

Finite vote graph with OP_TEMPLATEHASH

The ACK/NACK graph is finite and acyclic, so it can be expressed directly with OP_TEMPLATEHASH.

Each state script commits only to its two legal children:

State(r, a, n):

ACK branch:
    OP_TEMPLATEHASH == TH(tx creating State(r, a + 1, n))

NACK branch:
    OP_TEMPLATEHASH == TH(tx creating State(r, a, n + 1))

At the terminal boundary:

State(r, T-1, n):

ACK branch:
    OP_TEMPLATEHASH == TH(payout tx to recipient r)

and:

State(r, a, T-1):

NACK branch:
    reset / redeposit branch

OP_TEMPLATEHASH commits to the spending transaction template, including outputs and input sequences, but not prevouts or spent amounts. This makes the same abstract graph reusable for many fixed-denomination deposits.

One vote per block

Every ACK/NACK transition uses a one-block relative locktime.

Because OP_TEMPLATEHASH commits to the input sequence hash, every transition can enforce:

nSequence = 1 block

This gives the counter at most one ACK/NACK update per block. The relative-locktime semantics come from BIP68.

Recursive reset after rejection

The finite vote graph handles the voting path. But after rejection, funds should return to the idle covenant so another withdrawal can be attempted:

idle -> vote graph -> rejection -> idle

A direct OP_TEMPLATEHASH loop would create a hash cycle. The reset edge therefore uses the deleted-key recursive covenant trick described by AJ Towns.

The reset branch uses OP_CHECKSIGFROMSTACK to verify a signature over the reset template hash:

<TH(return_to_idle)> <Sig_dead> <P_dead> OP_CHECKSIGFROMSTACKVERIFY
OP_TEMPLATEHASH == TH(return_to_idle)

The key P_dead is generated by a large MPC. The MPC signs the reset template, then deletes its key shares. Security requires at least one participant to honestly delete their share.

The reset signature is only needed for the recursive loop. The huge ACK/NACK graph itself uses plain OP_TEMPLATEHASH.

Deposits

A deposit creates a fixed-denomination idle covenant output:

Deposit(D) -> Idle(D)

From Idle(D), a user starts a withdrawal attempt to one of the permissioned recipients:

Idle(D) -> State(r, 0, 0)

The recipient index r is chosen from the fixed recipient set. Arbitrary payout addresses are not supported unless they were included in the precomputed graph.

Payout

If the ACK counter reaches the threshold, the terminal ACK transaction pays the fixed denomination to the selected recipient:

State(r, T-1, n) --ACK--> payout D to recipient r

The payout transaction is committed by OP_TEMPLATEHASH, so the recipient and amount are fixed by the state graph.

Computation

The graph is deterministic. A state commitment is computed from its two children:

State(r, a, n)
    depends on State(r, a + 1, n)
    depends on State(r, a, n + 1)

So the graph can be computed backwards from the terminal payout and reset branches.

In practice, builders can cache likely paths or checkpoints and recompute uncommon branches on demand. Recomputing a state requires computing the Taproot output key, including the BIP341 tap tweak, so it is more expensive than a plain hash.

Relation to Spookchains

This is closely related to Jeremy Rubin’s Spookchains.

The shared idea is:

use a bounded covenant state graph
use one transition per block
use a fixed set of withdrawal destinations
reuse the setup across many deposits

The main difference is the primitive split:

Spookchains:
    APO-style signatures for state transitions

This scheme:
    OP_TEMPLATEHASH for the finite ACK/NACK graph
    deleted-key CSFS only for recursive reset / redeposit

Limitations

This is not a drop-in replacement for BIP300.

It does not support:

arbitrary withdrawal bundles
arbitrary payout addresses
dynamic denominations
dynamic recipient sets

The recipient set, denomination, payout templates, transaction shape, and reset path must be fixed in advance.

The ACK/NACK state space is genuinely two-dimensional. Collapsing it to a one-dimensional net score would change the semantics: “13,150 ACKs approves” is not the same rule as “ACKs minus NACKs reaches 13,150.”

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