Payment channel implementation in Grin/MW leveraging No Such Kernel Recently (NSKR) relative time locks. The limitations of the NSKR lock semantics require some changes to how we think about relative lock heights as we cannot rely on nodes maintaining an index of full kernel history and cannot enforce locks between arbitrary kernels.
Elder Channels (5) is based heavily on Elder Channels (3) but loses the ability to reuse the shared (state independent) close outputs due to the NSKR lock limitations. Instead we introduce endpoint "attribution" based on different kernels while minimizing endpoint specific outputs and rangeproofs.
Elder Channnels (4) was a brief interlude exploring the revealing of kernel excess values to revoke state changes which turned out to be flawed. Don't do this.
The funds in the channel are held in a single multiparty channel output.
- Channel
The channel is initially funded by two parties Alice and Bob.
- [Funda, Fundb] -> Channel
The channel can be closed by spending this channel output to a new output. Each channel state update can be represented with a new close transaction, each resulting a new close output.
- Channel -> Close1
- Channel -> Close2
- ...
- Channel -> Closen
We want to invalidate each previous state, leaving only the current valid state. We can do this by providing a revoke transaction for each previous state.
- Close1 -> Channel
- Close2 -> Channel
- ...
- Closen-1 -> Channel
We also want to be able to settle the current closed state out to individual outputs for each party involved in the channel, distributing funds as appropriate.
- Close1 -> [Settle1a, Settle1b]
- Close2 -> [Settle2a, Settle2b]
- ...
- Closen -> [Settlena, Settlenb]
Each channel state update involves both parties building a series of transactions. A close and settle transaction pair for the new state and a revoke transaction to invalidate the previous channel state.
Close: Channel -> Closen
Settle: Closen -> [Settlena, Settlenb]
Revoke: Closen-1 -> Channel
Each previous closed state can be revoked. A revoke and close can be aggregated and broadcast as a single transaction to revoke and previous closed state and close to the current state, which itself cannot be revoked. The current state, once closed, can then be settled.
Revoke and Close:
Closem -> Channel
Channel -> Closen
=> Closem -> Closen
The problem here is any previous state can be closed and immediately settled. We want to enforce a delay between the close and settle transactions. This delay is critical in allowing the revoke transaction to be broadcast for an old invalid state, before the corresponding settle transaction can be broadcast.
We can use a No Such Kernel Recently (NSKR) lock to implement this delay. An NSKR lock prevents one transaction kernel from being accepted within a certain number of blocks of an earlier transaction kernel.
- Channel -> Close1, Kclose
- Closen -> [Settle1a, Settle1b], Ksettle(close, 1440)
In this example the settle kernel is not valid within 1440 blocks of the referenced close kernel. During this period the revoke transaction can be broadcast, preventing the old invalid state being settled.
[TODO - describe attribution, Alice vs Bob, same transaction but different kernel etc.]
Endpoint specific close transaction for each state
Both variants of each close transaction share the same input and output. The kernel excess commitment is endpoint specific, with kernel offset adjusted to compensate.
- State1
- Channel -> Close1, Kclose_1a
- Channel -> Close1, Kclose_1b
- State2
- Channel -> Close2, Kclose_2a
- Channel -> Close2, Kclose_2b
- ...
- Staten
- Channel -> Closen, Kclose_na
- Channel -> Closen, Kclose_nb
Each state can be closed by Alice or Bob, resulting in the same close output. The different kernel excess commitment variants provide endpoint "attribution", with each referenced individually by subsequent endpoint specific transactions.
Endpoint specific settle transaction for each state
Both variants of each settle transaction share the same input and output. The kernel excess commitment is also shared, but each variant references a different endpoint specific kernel via the NSKR lock.
Each party possesses a close transaction and settle transaction that are NSKR locked such that either party cannot close the channel and then immediately settle.
- Settle1
- Close1 -> [Settle1a, Settle1b], Ksettle_1(close_1a, 1440)
- Close1 -> [Settle1a, Settle1b], Ksettle_1(close_1b, 1440)
- Settle2
- Close2 -> [Settle2a, Settle2b], Ksettle_1(close_2a, 1440)
- Close2 -> [Settle2a, Settle2b], Ksettle_1(close_2b, 1440)
- ...
Alice cannot close and then immediately settle. Alice must wait for the associated NSKR lock to expire after closing. Bob has the opportunity to immediately revoke if Alice attempts to close an old invalid state.
Alice attempts to close old invalid state:
Channel -> Close1, Kclose_1A
Bob can immediately revoke and close current state:
Close1 -> Channel, Krevoke_1
Channel -> Closen, Kclose_nb
=> Close1 -> Closen, [Krevoke_1, Kclose_nb]
Endpoint specific revoke transaction for each previous state
There is a problem if either party can immediately revoke a previous channel state. Alice could close an old invalid state and then immediately revoke and close to another old invalid state and continue doing this indefinitely. Alice could effectively lock funds up, preventing Bob from ever closing to the current state.
We can use a similar NSKR lock construction used in the endpoint specific settle transactions, preventing either party "self-revoking", while still allowing the other party to revoke as necessary.
Both variants of each revoke transaction share the same input and output. The kernel excess commitment is also shared but the kernel NSKR lock references an endpoint specific close kernel.
- Revoke1
- Close1 -> Channel, Krevoke_1(close_1a, 1440)
- Close1 -> Channel, Krevoke_1(close_1b, 1440)
- Revoke2
- Close2 -> Channel, Krevoke_2(close_2a, 1440)
- Close2 -> Channel, Krevoke_2(close_2b, 1440)
- ...
Alice cannot close and then immediately revoke and re-close, potentially locking funds up indefinitely. Alice must wait for the associated NSKR lock to expire before revoking. This still allows Bob, who can immediately revoke, to respond by revoking and closing to current state.
Alice attempts to close old invalid state:
Channel -> Close1, Kclose_1a
Bob can immediately revoke and close current state:
Close1 -> Channel, Krevoke_1(close_1b, 1440)
Channel -> Close2, Kclose_2b
=> Close1 -> Close2, [Krevoke_1(close_1b, 1440), Kclose_2b]
Note: Bob has an NSKR lock on their endpoint specific revoke transaction kernel but this is trivially satisfied as the referenced close kernel has not been recently seen. An NSKR lock is not a strict dependency between two kernels and the lock semantics are satisfied (unlocked) if the referenced kernel is not seen within the specified period.
[TODO - describe per round data requirements etc.]
Each state update -
close transaction(s): 1x multiparty output 1x multiparty rangeproof 2x endpoint specific kernels
settle transaction(s): 2x individual outputs 2x individual rangeproofs 2x endpoint specific kernels
previous state revoke transaction(s): 2x endpoint specific kernels
Local storage requirements:
current close tx + current settle tx + revoke kernel for each previous state
Close current state: close tx settle tx
Close old invalid state: close tx revoke and close tx settle tx
Nice write-up, Antioch.
I'd add the word "index" after "We cannot rely on nodes maintaining full kernel history".