Skip to content

Instantly share code, notes, and snippets.

@antiochp
Last active May 23, 2019 10:43
Show Gist options
  • Save antiochp/d490280fa0b87e0cd84f961b0911119f to your computer and use it in GitHub Desktop.
Save antiochp/d490280fa0b87e0cd84f961b0911119f to your computer and use it in GitHub Desktop.

Elder Channels (Redux)

Superseded by revised description here - https://gist.github.com/antiochp/e54fece52dc408d738bf434a14680988


A channel consists of a single multisig output. Alice and Bob agree to fund the channel.

Txfund ([InA, InB] -> Outchannel, Kernfund)

A pair of endpoint specific "close" and "settle" txs are negotiated for both Alice and Bob. Each "settle" tx has a relative lock height from the corresponding "close" tx kernel, introducing a delay between "close" and "settle" (24 hours for example).

The "close" and "settle" txs are negotiated between Alice and Bob before the channel is funded to prevent funds being locked up by either party.

Close and settle txs for Alice -

Txclose_0_A (Inchannel -> Outclose_0_A, Kernclose_0_A)

Txsettle_0_A (Inclose_0_A -> [OutA0, OutB0], Kernsettle_0_A,close_0_A,1440)

A matching pair of txs are created for Bob.

The channel can be closed cooperatively at any time by building a tx to spend from the multisig "channel" output, distributing funds back to Alice and Bob.

Alternatively either party can non-cooperatively close the channel by broadcasting their version of the current "close" tx (Txclose_0_A for Alice or Txclose_0_B for Bob).

To update the channel state a new "close" and "settle" pair of txs is negotiated, along with a "revoke" tx that revokes the previous state. The "revoke" tx simply spends the "close" funds back to the main "channel" output.

Txclose_1_A (Inchannel -> Outclose_1_A, Kernclose_1_A)

Txsettle_1_A (Inclose_1_A -> [OutA1, OutB1], Kernsettle_1_A,close_1_A,1440)

Txrevoke_0_B (Inclose_0_A -> Outchannel, Kernrevoke_0_B)

Note: Alice possesses the "close" and "settle" txs. Bob possesses the tx to revoke the previous state attributed to Alice. Bob can revoke any previous close broadcast by Alice.

Note: Alice cannot lock funds by repeatedly closing and revoking old states. Only Bob can revoke if Alice closes.

At this point Alice can non-cooperatively close the latest state by broadcasting Txclose_1_A, waiting for the delay and then broadcasting Txsettle_1_A.

Alternatively Alice could attempt to close a previous state by broadcasting Txclose_1_A.

Bob can "revoke and close" to immediately close the channel for the latest state as follows -

Txrevoke_0_B (Inclose_0_A -> Outchannel, Kernrevoke_0_B)

Txclose_1_B (Inchannel -> Outclose_1_B, Kernclose_1_B)

=> Txrevoke_close_B (Inclose_0_A -> Outclose_1_B, [Kernrevoke_0_B, Kernclose_1_B])

A "revoke and close" cut-through tx can be built for any previous "close" to the latest state with a single "revoke" kernel and the latest "close" kernel.

The example above used states 0 and 1 but the same thing applies for any arbitrary states. There is still only a single "revoke" tx with a single kernel and a single close tx for the latest state. An aggregate cut-through tx with 2 kernels is required to "revoke and close" any previous state.

On-Chain Activity

In the cooperative case there will be a single funding tx and a single close tx.

Txfund ([InA, InA] -> Outchannel, Kernfund)

Txclose (Inchannel -> [OutA', OutB'], Kernclose)

In the non-cooperative case there will be a close and settle pair. The relative lock height between close and settle will be visible.

Txfund ([InA, InA] -> Outchannel, Kernfund)

Txclose (Inchannel -> Outclose, Kernclose)

Txsettle (Inclose -> [OutA', OutB'], Kernsettle,close,1440)

In the revocation case we will see a close followed by a revoke and close and a final settle tx.

Txfund ([InA, InA] -> Outchannel, Kernfund)

Txclose (Inchannel -> Outclose, Kernclose)

Txrevoke_close (Inclose -> Outclose', [Kernrevoke, Kernclose'])

Txsettle (Inclose' -> [OutA', OutB'], Kernsettle,close',1440)

Local Storage Requirements

Each party must maintain a single "close" and "settle" tx pair for the latest channel state.

To allow revocation of any previous state they must also store the revocation kernel for each previous channel state.

@tromp
Copy link

tromp commented May 21, 2019

So this is like Poon-Dryja but with punish replaced by undo.

If you don't punish publication of old states, then you need not assign blame either.
So why do you have separate closes for A and B?

In principle this is a nice design for a "forgiving" payment channel,
with potential for needing only half the outputs/txs of the direct Poon-Dryja if you don't distinguish A/B closes.

But I see a big problem with designs that recycle outputs, in this case for the channel funds.
How do you pay the required fees?
To do a revoke+latest close seemingly requires that both the earlier close and revoke tx pay 0 fees?!

@antiochp
Copy link
Author

antiochp commented May 22, 2019

If you don't punish publication of old states, then you need not assign blame either.

I think you do need to assign blame here, otherwise A can potentially lock funds up.
A can "close" and then "revoke and close" on the next block and continue to do this repeatedly. If they can do this for long enough they get to expire the delay and close an old state.
The blame ensures only B can "close and revoke" after A "closes".

How do you pay the required fees?

I was ignoring fees for simplicity. One option may be to take fees out of the channel design entirely.
To "close" you could simply aggregate the channel close tx with another tx carrying sufficient fees for both.
Same for "revoke and close" and "settle" as necessary.

@tromp
Copy link

tromp commented May 22, 2019

Thanks for elaborating.

You make a good point about fees being possibly provided by separate aggregate-able transactions.
Those could even be single-input, zero-output transactions paying all of the input as fee.
Or more likely, single-input, single(change)-output transactions when no input of the right fee size is available.
That certainly simplifies the payment channel design.
And allays any fears that one wouldn't be able to settle due a sharp rise in fees.

This scheme has exactly the same number of required outputs (6) and transactions (6) per round as the punishing one.
We can call them the "punishing" and the "forgiving" scheme.

The blame ensures only B can "close and revoke" after A "closes".

The only remaining worry is that A watches out for B's revoke kernel appearing in a mempool,
and reusing it to close out to yet another old state (assuming miners resolve aggregation conflicts in favor of higher fees).
I suppose the kernel offsets would guard against this as well?!

@antiochp
Copy link
Author

antiochp commented May 22, 2019

The only remaining worry is that A watches out for B's revoke kernel appearing in a mempool,
and reusing it to close out to yet another old state (assuming miners resolve aggregation conflicts in favor of higher fees).
I suppose the kernel offsets would guard against this as well?!

Let me think about this a bit.

The kernel offset would prevent A from de-aggregating B's "revoke and close".
Each "revoke" may spend back to the reused channel output but each "revoke" tx must have a unique kernel offset (and both parties would need to verify this is the case). If a kernel offset was ever chosen that violated this then one party could potentially be in a position to lock the channel up and eventually close out an old invalid state.

B can build the aggregate [Txrevoke_B_0, Txclose_B_3].
Neither A nor any colluding miner can de-aggregate this and cannot retrieve Txrevoke_B_0 from the pool (assuming both "revoke" and "close" are endpoint specific).
B must never broadcast either of these individual "revoke" or "close" txs in this scenario.

There are several scenarios that come into play once a channel is closed (current or otherwise) and the channel must be closed ASAP by the other party in all these scenarios. I do not think it is ever safe to leave the channel open (or reopen it via a "revoke")
once any close has been attempted.

The reuse of the channel multisig output means its a one way process one a close has begun or "bad things" will happen.

@antiochp
Copy link
Author

antiochp commented May 22, 2019

I think we can make the "settle" tx symmetric if we make both A and B close txs spend to the same "close" output.
Rather than using distinct "close" outputs for A and B we can use a "relative lock height of 0" to constrain the "close" and "revoke" txs.

Txclose_0_A (Inchannel -> Outclose_0, Kernclose_0_A)

Txrevoke_0_B (Inclose_0 -> Outchannel, Kernrevoke_0_B,close_0_A,0)

Need to think this through a bit more but I think it works (again with the caveat that channels must be closed ASAP once any close is attempted).
This way we save an additional tx (settle still needs know if A or B closed it for the relative lock height) and an additional output for each channel state.

@antiochp
Copy link
Author

That certainly simplifies the payment channel design.

Definitely. I think it will be a lot easier to reason about the channels if we can safely assume fees are "out-sourced" to other txs and play no active role in the channel itself.

@tromp
Copy link

tromp commented May 22, 2019

make both A and B close txs spend to the same "close" output.

We can take this further by making all of close1, close2, ... spend from the unique channel output to a unique close output.
Then all spends from the latter are still distinguished by the earlier kernel that they are relative to.
So the per round requirements reduce to

2 outputs, for the Alice and Bob payouts
2 closing kernels
2 kernel relative settling tx
2 kernel relative revoke kernels for the previous round

@antiochp
Copy link
Author

Two states

  • Outchannel
  • Outclose

Alice can close the channel moving funds from Outchannel to Outclose -

Txclose_A_0 (Inchannel -> Outclose, Kernclose_A_0)

If this is an old state then Bob can "revoke and close" with -

Txrevoke_B_0 (Inclose -> Outchannel, Kernrevoke_B_0,close_A_0,0)
Txclose_B_n (Inchannel -> Outclose, Kernclose_B_n)
=> Txrevoke_close_B_n (Inclose -> Outclose, [Kernrevoke_B_0,close_A_0,0, Kernclose_A_n])

This "revoke and close" aggregate tx spends from Outclose back to Outclose ...

This doesn't work as the delay is still counting down from the first "close" (one kernel relative lock height does not supersede another).

We need a new "close" output per channel update to ensure it is only spent by the associated settle tx and not any previous one.

@tromp
Copy link

tromp commented May 22, 2019

This "revoke and close" aggregate tx spends from Outclose back to Outclose ...

Yes; in fact it would be a transaction with 0 inputs and 0 outputs after cut-through; just 2 kernels.

This doesn't work as the delay is still counting down from the first "close"

Yes, I should've realized it doesn't invalidate the earlier settle, and thus fails to work...

So the per round requirements are

2 kernel-relative 0-delay revoke kernels for the previous round
2 closing kernels
1 musig close output
1 kernel-relative k-day-delay settling tx
2 outputs, for the Alice and Bob payouts

@antiochp
Copy link
Author

antiochp commented May 22, 2019

In the above case we have the problem because A can start a timer (the relative lock height before "settle") and it will run down before B can settle the latest state.

But maybe we can solve this with 2 close states (one per endpoint) - each represented by a multisig output.

  • Outchannel
  • Outclose_A
  • Outclose_B

Alice can attempt to close an old state with -

Txclose_A_0 (Inchannel -> Outclose_A, Kernclose_A_0)

Bob can "revoke and close" with -

Txrevoke_B_0 (Inclose_A -> Outchannel, Kernrevoke_B_0,close_A_0,0)
Txclose_B_1 (Inchannel -> Outclose_B, Kernclose_B_1)
=> Txrevoke_close_B_1 (Inclose_A -> Outclose_B, [Kernrevoke_B_0,close_A_0,0, Kernclose_B_1])

This way we have two delay timers. One for Alice delaying Outclose_A from being settled and one for Bob delaying Outclose_B. Alice just got her (old) closed state revoked so the timer is no longer applicable. The timer is running for Bob to settle his closed state and this cannot be revoked as it is the latest state.
After the delay Bob can safely settle the channel and distribute funds back to Alice and Bob as per latest state.

Either party can attempt to close old state but this will be immediately revoked and the latest state will be closed by the other party.

So maybe we don't need a unique close output per channel state update, maybe we just need a unique close state per channel participant.

Per round requirements here would be -
2 revoke kernels + offsets
2 close kernels + offsets
2 settle kernels + offsets
2 settle outputs (shared across both variations of the settle tx)

Both parties would need to maintain latest state (close kernel + single settle tx) and all previous revoke kernels.

@tromp
Copy link

tromp commented May 22, 2019

we just need a unique close state per channel participant.

A yes, that would do the trick.
The revoke + latest close aggregates into a transfer from one close output to the other.

So in this final (famous last words:-) form, we need for each additional round

2 kernel-relative 0-delay revoke kernels for the previous round
2 "closing" kernels
2 kernel-relative k-day-delay "settling" tx
2 outputs, for the Alice and Bob payouts

Compared to the last one, we have one less output and one more kernel.
Which is a good trade-off, as outputs require expensive rangeproofs...

If the channel is likely to reuse previous balances, e.g. because Alice and Bob are playing a random game with 50-50-odds,
then they can decide to reuse the outputs (and corresponding rangeproofs) created earlier, for greater efficiency.

@antiochp
Copy link
Author

antiochp commented May 23, 2019

@mably
Copy link

mably commented May 23, 2019

So, does this mean that the Lightning Network could finally be implemented on top of Grin? That would be huge.

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