go-nitro
go-nitro copied to clipboard
Create a `channel-top-up` protocol
There is currently no way for running ledger channels to be topped up. Topups are desirable because they are generally cheaper than finalizing a channel, disbursing its assets, and opening a new channel via a L1 deposit.
Protocol at a glance:
- with
- ledger channel
L(X, Y)between participants X and Y. Xas the topper-upper (ie, the person who executes the L1 deposit to add funds)- top-up amount
t
- ledger channel
- take the current channel outcome and append
X: tas the final outcome item- the channel outcome now probably resembles
{ X: x, Y: y, [someGuarantees]..., X: t} - exchange and co-sign this updated state
- the channel outcome now probably resembles
- await the
Deposited(L(X,Y).channelID, t)event - fold the
X: toutcome item back into the previously existingXoutcome item- result:
{ X: x+t, Y: y, [someGuarantees}... } - exchange and co-sign this updated state
- result:
Implementation Details
An ideal implementation of this protocol would leave the ledger channel usable for regular operations while the top-up is being run. Waiting on an on-chain event necessarily makes this a slow operation, but ledger channels are depended on for the faster, off-chain virtual fund and defund operations.
the consensus_channel implementation of ledger channels has customized representation of ledger outcomes:
type LedgerOutcome struct {
assetAddress types.Address // Address of the asset type
leader Balance // Balance of participants[0]
follower Balance // Balance of participants[1]
guarantees map[types.Destination]Guarantee
// add this?
topUp Balance // Balance of a running topup - usually empty
}
and conversions to and from canonical Exit-Format representation:
// AsOutcome converts a LedgerOutcome to an on-chain exit according to the following convention:
// - the "leader" balance is first
// - the "follower" balance is second
// - guarantees follow, sorted according to their target destinations
func (o *LedgerOutcome) AsOutcome() outcome.Exit {
// The first items are [leader, follower] balances
allocations := outcome.Allocations{o.leader.AsAllocation(), o.follower.AsAllocation()}
// Followed by guarantees, _sorted by the target destination_
keys := make([]types.Destination, 0, len(o.guarantees))
for k := range o.guarantees {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
for _, target := range keys {
allocations = append(allocations, o.guarantees[target].AsAllocation())
}
// add this?
// the last item is the running topUp balance
if (o.topUp != Balance{}) {
allocations = append(allocations, o.topUp.AsAllocation())
}
return outcome.Exit{
outcome.SingleAssetExit{
Asset: o.assetAddress,
Allocations: allocations,
},
}
}
// FromExit creates a new LedgerOutcome from the given SingleAssetExit.
//
// It makes the following assumptions about the exit:
// - The first allocation entry is for the ledger leader
// - The second allocation entry is for the ledger follower
// - The last allocation may be a running topup (added)
// - All other allocations are guarantees
func FromExit(sae outcome.SingleAssetExit) (LedgerOutcome, error) {
var (
leader = Balance{destination: sae.Allocations[0].Destination, amount: sae.Allocations[0].Amount}
follower = Balance{destination: sae.Allocations[1].Destination, amount: sae.Allocations[1].Amount}
guarantees = make(map[types.Destination]Guarantee)
)
for _, a := range sae.Allocations {
if a.AllocationType == outcome.GuaranteeAllocationType {
gM, err := outcome.DecodeIntoGuaranteeMetadata(a.Metadata)
if err != nil {
return LedgerOutcome{}, fmt.Errorf("failed to decode guarantee metadata: %w", err)
}
g := Guarantee{
amount: a.Amount,
target: a.Destination,
left: gM.Left,
right: gM.Right,
}
guarantees[a.Destination] = g
}
}
// add
if len(guarantees) != len(sae.Allocations)-2 {
last := sae.Allocations[len(sae.Allocations)-1]
var topUp = Balance{destination: last.Destination, amount: last.Amount}
}
return LedgerOutcome{leader: leader, follower: follower, guarantees: guarantees, assetAddress: sae.Asset}, nil
}