Skip to content

Instantly share code, notes, and snippets.

@ajtowns
Last active October 25, 2022 07:40
Show Gist options
  • Save ajtowns/53e0f735f4d5c06a681429d937200aa5 to your computer and use it in GitHub Desktop.
Save ajtowns/53e0f735f4d5c06a681429d937200aa5 to your computer and use it in GitHub Desktop.
anyprevout based eltoo design with only two participants, A and B
funding transaction is multisig A,B
for each channel state from 0..n, create transactions:
UA:
spends funding tx, A holds B's signatures, SIGHASH_ALL, no ANYPREVOUT
reveals state number in nlocktime/nsequence
all funds go to:
tapscript 1: "1 CHECKSIGVERIFY n CLTV" where n is state number+500M
tapscript 2: "sig_G(S,ANYPREVOUTANYSCRIPT|ALL) G CHECKSIG"
internal pubkey: musig(A,B)/1
has ephemeral output for CPFP fees
reveals "sig_G(S,ANYPREVOUTANYSCRIPT|ALL)" via the annex
UB:
same thing, but swapping A/B, and internal pubkey is musig(A,B)/2
S:
spends UA or UB via tapscript 2
nSequence set to shared delay
outputs go to normal balance/htlcs/etc
RA:
spends "UB", outputs go to normal balance/htlcs/etc
no nSequence delay
nlocktime set to state number+500M+1
A holds B's partial ANYPREVOUTANYSCRIPT signature
RB: likewise, swapping A/B
Logic for A is:
* every update, you calculate three signatures (F->UB, UA->RB and sig_G(S)) and send the first two
* when doing a unilateral close, you finish signing UA and publish it,
then either wait until you can post S, or observe B publishing RB,
at which point you claim your balance/htlcs
* if B does a unilateral close, you publish RA in order to access your funds immediately,
(and also prevent B from publishing an out of date S if their UB was out of date)
If B publishes UB before you have the signature for RA, then you can wait for the the shared delay to complete
and publish S yourself.
Griefing is prevented because after starting a unilateral close, you can't publish any more transactions
until either the shared delay timeout finishes, or the other party publishes a transaction.
Pinning is prevented because RA can be published on top of UA while it's still in the mempool
(and as RA only has an APOAS signature, RA can spend UA's ephemeral output).
To add penalties:
- change UA/UB to an SINGLE|ANYONECANPAY signature, skip ephemeral output
- change the output amount for UA/UB to be channel-capacity + penalty
- split S into SA/SB - UA sends to SA, UB sends to SB; SA adds penalty to A's balance, SB adds penalty to B's balance
- decrement RA/RB's nLockTime by one, so that they can only spend UB/UA from *prior* states
- RA adds penalty to A's balance, RB adds penalty to B's balance
To avoid the annex commitment:
- the annex commitment is needed in case B publishes an old UB -- in that case for A to spend it with the current
RA, A needs to derive a path to tapscript 1, which requires knowing S
- so instead we drop the commitment to S entirely and replace it with a scriptless scripts approach.
- we drop UA/UB's second tapscript
- we send our half of the musig to spend UB to S to B
- we no longer send a signature spending F to UB, but instead a partial signature, dependent on
revealing B's signature spending UB to RA.
- thus, as soon as we see UB on-chain, we can either spend with our latest agreed RA; or we can used B's signature
to finish calculating the newest RA and spend with that
Funding transaction is F.
Transactions for state n are:
UA.n CA.n SA.n RA.n (publishable by A)
UB.n CB.n SB.n RB.n (publishable by B)
The valid payment paths are:
F -> money (cooperative close)
F -> UA.n -(delay)-> SA.n -> money (unilateral close)
F -> UA.n -> CB.n -> money (semi-cooperative close)
F -> UA.k -> RB.n -> money (attempted cheating by A, k < n)
F -> UB.n -(delay)-> SB.n -> money (unilateral close)
F -> UB.n -> CA.n -> money (semi-cooperative close)
F -> UB.k -> RA.n -> money (attempted cheating by B, k < n)
Keys are A, B, P=musig(A,B), Pa = musig(A,B,1), Pb = musig(A,B,2)
F pays to P
We describe UA.n, CA.n, SA.n and RA.n only; the others just have A/B
(and Pa/Pb) swapped
F:
input: whatever
output:
* value = channel capacity
scriptPubKey =
internal key: P
tapscript: "apo(A) CHECKSIG NOTIF 1 ELSE apo(B) ENDIF CHECKSIG"
(apo(x) is just X as a bip118 pubkey, ie prefixed with 1,
script is "or(pk(1),and(pk(A),pk(B)))"
aka "c:andor(pk(A),pk_k(1),pk_k(B))")
* whatever
UA.n:
locktime: encodes lower 24 bits of "n"
input:
* F
- (keypath, musig, SINGLE|ANYONECANPAY)
- or (tapscript, musig, SINGLE|ANYPREVOUT) [DualFund]
- or (tapscript, sig(B, SINGLE or SINGLE|ANYPREVOUT))
nsequence encodes upper 24 bits of "n"
output:
* value = capacity + penalty
scriptPubKey =
internal key: Pa
tapscript: "IF CODESEP n OP_CLTV DROP ENDIF 1 CHECKSIG"
SA.n:
input:
* UA.n nSequence = to-self-delay
- (keypath, musig, ALL)
- or (tapscript, musig, codesep=FFFF, ALL|ANYPREVOUT) [DualFund]
output:
* distribute according to channel state, with A's balance increased
by "penalty"
CA.n:
input:
* UB.n nSequence = 0
- (keypath, musig, ALL)
- or (tapscript, musig, codesep=FFFF, ALL|ANYPREVOUT) [DualFund]
output:
* distribute according to channel state, with B's balance increased
by "penalty"
RA.n:
nlocktime=500,000,000 + n
input:
* UB.k nSequence = 0
- (tapscript, musig, codesep=1, ALL|ANYPREVOUTANYSCRIPT)
output:
* distribute according to channel state, with A's balance increased by
"penalty"
When A proposes a new state, she needs to provide the following signatures
to B:
- spending F to UB.n via tapscript. A creates an adaptor signature
that requires B to reveal their signature on CA.n to complete.
- musig2 spending UB.n to SB.n
- musig2 spending UA.n to CB.n
- musig2 spending UA.k to RB.n
After receiving a signature for UB.n to CA.n, should provide B with a
musig2 signature spending F to UB.n.
This requires pre-sharing 8 musig2 nonce pairs in advance of each state
update.
For the adaptor signature for F->UB.n, A calculates:
* musig2 public nonces for UB.n->CB.n, giving Ra and Rb
* B's signature target for UB.n->CB.n, ie:
T = Rb + H(Ra+Rb, muA*A + muB*B, sigmsg(CB.n))*muB*B
* an adaptor signature for F to UB.n via tapscript path:
s' = r + H(R+T, A, sigmsg(UB.n))*A
based on T above, with fresh nonce R, sends (s', R) to B,
and stores s'.
B can use this signature on chain, by first calculating t s.t t*G=T
(as B knows the discrete log of both Rb and B), then setting s=(s'+t)
with (s,R) being a valid signature by A to spend F to UA.x.
If A sees the signature on chain, she can calculate t=(s-s'), and combine
that with her own partial musig (u,Ra) to produce a valid signature
(t+u,Ra+Rb) spending UB.n to CB.n.
[DualFund] used if F is dual funded and not yet confirmed, meaning txid
may not be known
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment