Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add HTLCs to ledger channel update protocol #1815

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 218 additions & 8 deletions channel/consensus_channel/consensus_channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"fmt"
"math/big"
"sort"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/statechannels/go-nitro/channel/state"
"github.com/statechannels/go-nitro/channel/state/outcome"
"github.com/statechannels/go-nitro/crypto"
"golang.org/x/crypto/sha3"

"github.com/statechannels/go-nitro/types"
)
Expand All @@ -25,6 +27,9 @@ const (
ErrDuplicateGuarantee = types.ConstError("duplicate guarantee detected")
ErrGuaranteeNotFound = types.ConstError("guarantee not found")
ErrInvalidAmount = types.ConstError("left amount is greater than the guarantee amount")
ErrDuplicateHTLC = types.ConstError("duplicate HTLC detected")
ErrExpiredHTLC = types.ConstError("HTLC is expired")
ErrInvalidPayer = types.ConstError("HTLC payer is not a participant")
)

const (
Expand Down Expand Up @@ -365,12 +370,53 @@ func (g Guarantee) AsAllocation() outcome.Allocation {
// participant.
//
// This struct does not store items in sorted order. The conventional ordering of allocation items is:
// [leader, follower, ...guaranteesSortedByTargetDestination]
// [leader, follower, ...guaranteesSortedByTargetDestination, ...htlcsSortedByHash]
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
htlcs map[common.Hash]HTLC
}

// HTLC represents a hash time-locked payment. A payment can be sent by
// either channel participant, and is received by the other participant.
//
// The payment is claimed by the payee with the reveal of the preimage of hash.
type HTLC struct {
// the Amount of the payment. TODO: allow specified asset / multi-asset HTLCs
Amount *big.Int
// 32 byte Hash of the preimage, either keccak256 (evm-only) or SHA256 (LN compatible).
Hash common.Hash
// the block number at which the HTLC expires, after which the payer can claim the funds
// unilaterally.
ExpirationBlock uint64
// the address that will receive the payment if the preimage is revealed
ReleaseTo types.Destination
// the address making the payment
PaidFrom types.Destination
}

func (h HTLC) AsAllocation() outcome.Allocation {
payerAddr, _ := h.PaidFrom.ToAddress()
payeeAddr, _ := h.ReleaseTo.ToAddress()

htlcMetadata, err := outcome.HTLCMetadata{
Payer: payerAddr,
Payee: payeeAddr,
Hash: h.Hash,
ExpirationBlock: h.ExpirationBlock,
}.Encode()
if err != nil {
panic(err) // todo: handle this error
}

return outcome.Allocation{
Amount: h.Amount,
Destination: types.Destination{},
AllocationType: outcome.HTLCAllocationType,
Metadata: htlcMetadata,
}
}

// Clone returns a deep copy of the receiver.
Expand All @@ -379,11 +425,19 @@ func (lo *LedgerOutcome) Clone() LedgerOutcome {
for key, g := range lo.guarantees {
clonedGuarantees[key] = g.Clone()
}
clonedHTLCs := make(map[common.Hash]HTLC)
for key, h := range lo.htlcs {
clonedHTLCs[key] = h // TODO: define strategy for cloning HTLCs
}
if lo.htlcs == nil {
clonedHTLCs = nil
}
return LedgerOutcome{
assetAddress: lo.assetAddress,
leader: lo.leader.Clone(),
follower: lo.follower.Clone(),
guarantees: clonedGuarantees,
htlcs: clonedHTLCs,
}
}

Expand Down Expand Up @@ -484,6 +538,19 @@ func (o *LedgerOutcome) AsOutcome() outcome.Exit {
allocations = append(allocations, o.guarantees[target].AsAllocation())
}

// followed by HTLCs, _sorted by the hash_
hashes := make([]common.Hash, 0, len(o.htlcs))
for h := range o.htlcs {
hashes = append(hashes, h)
}
sort.Slice(hashes, func(i, j int) bool {
return hashes[i].String() < hashes[j].String()
})

for _, h := range hashes {
allocations = append(allocations, o.htlcs[h].AsAllocation())
}

return outcome.Exit{
outcome.SingleAssetExit{
Asset: o.assetAddress,
Expand Down Expand Up @@ -564,16 +631,23 @@ func (sv *SignedVars) clone() SignedVars {
}
}

// Proposal is a proposal either to add or to remove a guarantee.
// Proposal is a proposal either:
// - to add or to remove a guarantee, or
// - to add or to remove an HTLC.
//
// Exactly one of {toAdd, toRemove} should be non nil.
// Exactly one of {toAdd, toRemove, AddHTLC, RemoveHTLC} should be non nil.
type Proposal struct {
// LedgerID is the ChannelID of the ConsensusChannel which should receive the proposal.
//
// The target virtual channel ID is contained in the Add / Remove struct.
LedgerID types.Destination
ToAdd Add
ToRemove Remove
AddHTLC HTLC
// Either
// - the secret preimage of some currently running HTLC, or
// - an empty slice, indicating a proposal to clear all expired HTLCs
RemoveHTLC []byte
}

// Clone returns a deep copy of the receiver.
Expand All @@ -582,12 +656,16 @@ func (p *Proposal) Clone() Proposal {
p.LedgerID,
p.ToAdd.Clone(),
p.ToRemove.Clone(),
p.AddHTLC,
p.RemoveHTLC,
}
}

const (
AddProposal ProposalType = "AddProposal"
RemoveProposal ProposalType = "RemoveProposal"
AddProposal ProposalType = "AddProposal"
RemoveProposal ProposalType = "RemoveProposal"
AddHTLCProposal ProposalType = "AddHTLCProposal"
RemoveHTLCProposal ProposalType = "RemoveHTLCProposal"
)

type ProposalType string
Expand All @@ -597,8 +675,12 @@ func (p *Proposal) Type() ProposalType {
zeroAdd := Add{}
if p.ToAdd != zeroAdd {
return AddProposal
} else {
} else if p.ToRemove != (Remove{}) {
return RemoveProposal
} else if p.AddHTLC != (HTLC{}) {
return AddHTLCProposal
} else {
return RemoveHTLCProposal
}
}

Expand All @@ -622,14 +704,22 @@ func (p SignedProposal) SortInfo() (types.Destination, uint64) {
// Target returns the target channel of the proposal.
func (p *Proposal) Target() types.Destination {
switch p.Type() {
case "AddProposal":
case AddProposal:
{
return p.ToAdd.Target()
}
case "RemoveProposal":
case RemoveProposal:
{
return p.ToRemove.Target
}
case AddHTLCProposal:
{
return p.LedgerID
}
case RemoveHTLCProposal:
{
return p.LedgerID
}
default:
{
panic(fmt.Errorf("invalid proposal type %T", p))
Expand Down Expand Up @@ -841,6 +931,126 @@ func (vars *Vars) Remove(p Remove) error {
return nil
}

func (vars *Vars) AddHTLC(p HTLC) error {
o := vars.Outcome
// CHECKS
_, found := o.htlcs[p.Hash]
if found {
return ErrDuplicateHTLC
}

currentBlock := uint64(time.Now().Unix()) // todo: use a real block number
if p.ExpirationBlock < currentBlock {
return ErrExpiredHTLC
}

leaderPaying := p.PaidFrom == o.leader.destination
followerPaying := p.PaidFrom == o.follower.destination

if !leaderPaying && !followerPaying {
return ErrInvalidPayer
}

if leaderPaying {
// CHECKS
if types.Gt(p.Amount, o.leader.amount) {
return ErrInsufficientFunds
}
// EFFECTS
o.leader.amount.Sub(o.leader.amount, p.Amount)
}

if followerPaying {
// CHECKS
if types.Gt(p.Amount, o.follower.amount) {
return ErrInsufficientFunds
}

// EFFECTS
o.follower.amount.Sub(o.follower.amount, p.Amount)
}

// EFFECTS
vars.TurnNum += 1
o.htlcs[p.Hash] = p

return nil
}

// RemoveHTLC removes the HTLC whose key is the hash of preimage b.
// If b is empty, RemoveHTLC prunes out all of the expired HTLCs.
func (vars *Vars) RemoveHTLC(b []byte) {
vars.TurnNum += 1
o := vars.Outcome

if len(b) == 0 {
// Clear all expired HTLCs
for hash, htlc := range o.htlcs {
blockNumber := uint64(time.Now().Unix()) // todo: use a real block number
if blockNumber >= htlc.ExpirationBlock {

leaderRefund := htlc.PaidFrom == o.leader.destination
followerRefund := htlc.PaidFrom == o.follower.destination

if leaderRefund {
o.leader.amount.Add(o.leader.amount, htlc.Amount)
}
if followerRefund {
o.follower.amount.Add(o.follower.amount, htlc.Amount)
}

delete(o.htlcs, hash)
}
}
} else {
toRemove := common.Hash{}

// Check for a LN compatible preimage
sha := sha3.New256()
sha.Write(b)
shaHash := common.Hash(sha.Sum(nil))

if _, ok := o.htlcs[shaHash]; ok {
toRemove = shaHash
}

// Check for an EVM compatible preimage
kec := sha3.NewLegacyKeccak256()
kec.Write(b)
kecHash := common.Hash(kec.Sum(nil))

if _, ok := o.htlcs[kecHash]; ok {
toRemove = kecHash
}

if toRemove != (common.Hash{}) {
currentBlock := uint64(time.Now().Unix()) // todo: use a real block number
if currentBlock > o.htlcs[toRemove].ExpirationBlock {
// if the htlc has expired, it should probably have already been removed.
// its presence should be considered a bug, and the funds should not
// propagate to the payee.

// todo: handle this via logging, circling back to a clearExpiredHTLC operation, etc.
return
}

htlc := o.htlcs[toRemove]

leaderClaim := htlc.ReleaseTo == o.leader.destination
followerClaim := htlc.ReleaseTo == o.follower.destination

if leaderClaim {
o.leader.amount.Add(o.leader.amount, htlc.Amount)
}
if followerClaim {
o.follower.amount.Add(o.follower.amount, htlc.Amount)
}

delete(o.htlcs, toRemove)
}
}
}

// Remove is a proposal to remove a guarantee for the given virtual channel.
type Remove struct {
// Target is the address of the virtual channel being defunded
Expand Down
8 changes: 5 additions & 3 deletions channel/consensus_channel/serde.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ func (r *Remove) UnmarshalJSON(data []byte) error {
// jsonProposal replaces Proposal's private fields with public ones,
// making it suitable for serialization
type jsonProposal struct {
LedgerID types.Destination
ToAdd Add
ToRemove Remove
LedgerID types.Destination
ToAdd Add
ToRemove Remove
AddHTLC HTLC
RemoveHTLC []byte
}

// MarshalJSON returns a JSON representation of the Proposal
Expand Down
4 changes: 2 additions & 2 deletions channel/consensus_channel/serde_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestSerde(t *testing.T) {
},
},
}
someConsensusChannelJSON := `{"Id":"0x0100000000000000000000000000000000000000000000000000000000000000","OnChainFunding":{"0x0000000000000000000000000000000000000000":9},"MyIndex":0,"FP":{"Participants":["0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce","0xbbb676f9cff8d242e9eac39d063848807d3d1d94"],"ChannelNonce":9001,"AppDefinition":"0x0000000000000000000000000000000000000000","ChallengeDuration":100},"Current":{"TurnNum":0,"Outcome":{"AssetAddress":"0x0000000000000000000000000000000000000000","Leader":{"Destination":"0x000000000000000000000000aaa6628ec44a8a742987ef3a114ddfe2d4f7adce","Amount":2},"Follower":{"Destination":"0x000000000000000000000000bbb676f9cff8d242e9eac39d063848807d3d1d94","Amount":7},"Guarantees":{"0x6300000000000000000000000000000000000000000000000000000000000000":{"Amount":1,"Target":"0x6300000000000000000000000000000000000000000000000000000000000000","Left":"0x000000000000000000000000aaa6628ec44a8a742987ef3a114ddfe2d4f7adce","Right":"0x000000000000000000000000aaa6628ec44a8a742987ef3a114ddfe2d4f7adce"}}},"Signatures":["0x704b3afcc6e702102ca1af3f73cf3b37f3007f368c40e8b81ca823a65740a05314040ad4c598dbb055a50430142a13518e1330b79d24eed86fcbdff1a7a9558900","0x14040ad4c598dbb055a50430142a13518e1330b79d24eed86fcbdff1a7a95589704b3afcc6e702102ca1af3f73cf3b37f3007f368c40e8b81ca823a65740a05300"]},"ProposalQueue":[{"Signature":"0x14040ad4c598dbb055a50430142a13518e1330b79d24eed86fcbdff1a7a95589704b3afcc6e702102ca1af3f73cf3b37f3007f368c40e8b81ca823a65740a05300","Proposal":{"LedgerID":"0x0000000000000000000000000000000000000000000000000000000000000000","ToAdd":{"Guarantee":{"Amount":1,"Target":"0x0300000000000000000000000000000000000000000000000000000000000000","Left":"0x000000000000000000000000aaa6628ec44a8a742987ef3a114ddfe2d4f7adce","Right":"0x000000000000000000000000bbb676f9cff8d242e9eac39d063848807d3d1d94"},"LeftDeposit":1},"ToRemove":{"Target":"0x0000000000000000000000000000000000000000000000000000000000000000","LeftAmount":null}},"TurnNum":0},{"Signature":"0x14040ad4c598dbb055a50430142a13518e1330b79d24eed86fcbdff1a7a95589704b3afcc6e702102ca1af3f73cf3b37f3007f368c40e8b81ca823a65740a05300","Proposal":{"LedgerID":"0x0000000000000000000000000000000000000000000000000000000000000000","ToAdd":{"Guarantee":{"Amount":null,"Target":"0x0000000000000000000000000000000000000000000000000000000000000000","Left":"0x0000000000000000000000000000000000000000000000000000000000000000","Right":"0x0000000000000000000000000000000000000000000000000000000000000000"},"LeftDeposit":null},"ToRemove":{"Target":"0x0300000000000000000000000000000000000000000000000000000000000000","LeftAmount":1}},"TurnNum":0}]}`
someConsensusChannelJSON := `{"Id":"0x0100000000000000000000000000000000000000000000000000000000000000","OnChainFunding":{"0x0000000000000000000000000000000000000000":9},"MyIndex":0,"FP":{"Participants":["0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce","0xbbb676f9cff8d242e9eac39d063848807d3d1d94"],"ChannelNonce":9001,"AppDefinition":"0x0000000000000000000000000000000000000000","ChallengeDuration":100},"Current":{"TurnNum":0,"Outcome":{"AssetAddress":"0x0000000000000000000000000000000000000000","Leader":{"Destination":"0x000000000000000000000000aaa6628ec44a8a742987ef3a114ddfe2d4f7adce","Amount":2},"Follower":{"Destination":"0x000000000000000000000000bbb676f9cff8d242e9eac39d063848807d3d1d94","Amount":7},"Guarantees":{"0x6300000000000000000000000000000000000000000000000000000000000000":{"Amount":1,"Target":"0x6300000000000000000000000000000000000000000000000000000000000000","Left":"0x000000000000000000000000aaa6628ec44a8a742987ef3a114ddfe2d4f7adce","Right":"0x000000000000000000000000aaa6628ec44a8a742987ef3a114ddfe2d4f7adce"}}},"Signatures":["0x704b3afcc6e702102ca1af3f73cf3b37f3007f368c40e8b81ca823a65740a05314040ad4c598dbb055a50430142a13518e1330b79d24eed86fcbdff1a7a9558900","0x14040ad4c598dbb055a50430142a13518e1330b79d24eed86fcbdff1a7a95589704b3afcc6e702102ca1af3f73cf3b37f3007f368c40e8b81ca823a65740a05300"]},"ProposalQueue":[{"Signature":"0x14040ad4c598dbb055a50430142a13518e1330b79d24eed86fcbdff1a7a95589704b3afcc6e702102ca1af3f73cf3b37f3007f368c40e8b81ca823a65740a05300","Proposal":{"LedgerID":"0x0000000000000000000000000000000000000000000000000000000000000000","ToAdd":{"Guarantee":{"Amount":1,"Target":"0x0300000000000000000000000000000000000000000000000000000000000000","Left":"0x000000000000000000000000aaa6628ec44a8a742987ef3a114ddfe2d4f7adce","Right":"0x000000000000000000000000bbb676f9cff8d242e9eac39d063848807d3d1d94"},"LeftDeposit":1},"ToRemove":{"Target":"0x0000000000000000000000000000000000000000000000000000000000000000","LeftAmount":null},"AddHTLC":{"Amount":null,"Hash":"0x0000000000000000000000000000000000000000000000000000000000000000","ExpirationBlock":0,"ReleaseTo":"0x0000000000000000000000000000000000000000000000000000000000000000","PaidFrom":"0x0000000000000000000000000000000000000000000000000000000000000000"},"RemoveHTLC":null},"TurnNum":0},{"Signature":"0x14040ad4c598dbb055a50430142a13518e1330b79d24eed86fcbdff1a7a95589704b3afcc6e702102ca1af3f73cf3b37f3007f368c40e8b81ca823a65740a05300","Proposal":{"LedgerID":"0x0000000000000000000000000000000000000000000000000000000000000000","ToAdd":{"Guarantee":{"Amount":null,"Target":"0x0000000000000000000000000000000000000000000000000000000000000000","Left":"0x0000000000000000000000000000000000000000000000000000000000000000","Right":"0x0000000000000000000000000000000000000000000000000000000000000000"},"LeftDeposit":null},"ToRemove":{"Target":"0x0300000000000000000000000000000000000000000000000000000000000000","LeftAmount":1},"AddHTLC":{"Amount":null,"Hash":"0x0000000000000000000000000000000000000000000000000000000000000000","ExpirationBlock":0,"ReleaseTo":"0x0000000000000000000000000000000000000000000000000000000000000000","PaidFrom":"0x0000000000000000000000000000000000000000000000000000000000000000"},"RemoveHTLC":null},"TurnNum":0}]}`

type testCase struct {
name string
Expand Down Expand Up @@ -113,7 +113,7 @@ func TestSerde(t *testing.T) {
}
want := c.json
if string(got) != want {
t.Fatalf("incorrect json marshaling, expected %v got \n%v", want, string(got))
t.Fatalf("incorrect json marshaling of test %s, expected %v got \n%v", c.name, want, string(got))
}
})

Expand Down
Loading
Loading