From 076f7073029b3b2f88e6997e37b43eeba072942c Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Mon, 9 Oct 2023 01:53:11 -0300 Subject: [PATCH 01/20] wip: add HTLCs to ledger channel update protocol --- .../consensus_channel/consensus_channel.go | 136 ++++++++++++++++-- 1 file changed, 128 insertions(+), 8 deletions(-) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index 23f693c64..7fe97bcbb 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -6,6 +6,7 @@ import ( "fmt" "math/big" "sort" + "time" "github.com/ethereum/go-ethereum/common" "github.com/statechannels/go-nitro/channel/state" @@ -25,6 +26,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 ( @@ -365,12 +369,31 @@ 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 expiration cutoff for the HTLC. If the preimage is not revealed before this time, + // the HTLC expires and funds are returned to the sender + expiration time.Time + // the address that will receive the payment if the preimage is revealed + releaseTo types.Destination + // the address making the payment + paidFrom types.Destination } // Clone returns a deep copy of the receiver. @@ -379,11 +402,16 @@ 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 + } return LedgerOutcome{ assetAddress: lo.assetAddress, leader: lo.leader.Clone(), follower: lo.follower.Clone(), guarantees: clonedGuarantees, + htlcs: clonedHTLCs, } } @@ -564,9 +592,11 @@ 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. // @@ -574,6 +604,11 @@ type Proposal 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. @@ -582,12 +617,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 @@ -597,8 +636,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 } } @@ -622,14 +665,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)) @@ -841,6 +892,75 @@ 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 + } + + if p.expiration.Before(time.Now()) { + return ErrExpiredHTLC + } + + leaderPaying := p.paidFrom == o.leader.destination + if leaderPaying { + // CHECKS + if types.Gt(p.amount, o.leader.amount) { + return ErrInsufficientFunds + } + + // Include HTLC + o.htlcs[p.hash] = p + } + + followerPaying := p.paidFrom == o.follower.destination + if followerPaying { + // CHECKS + if types.Gt(p.amount, o.follower.amount) { + return ErrInsufficientFunds + } + + // EFFECTS + o.follower.amount.Sub(o.follower.amount, p.amount) + } + + if !leaderPaying && !followerPaying { + return ErrInvalidPayer + } + + // EFFECTS + vars.TurnNum += 1 + o.htlcs[p.hash] = p + + return nil +} + +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 { + if time.Now().After(htlc.expiration) { + delete(o.htlcs, hash) + } + } + } else { + // Clear the HTLC with the given preimage + + // TODO: implement + + // hash the preimage with Keccak256 (evm-native payment), SHA256 (ln-compatible payment) + + // check result against keys in o.htlcs + + // if found, delete the HTLC and credit the receiver + } +} + // 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 From 26075467ae36cee4a93ef3cff15bbc443d05880c Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 10:35:44 -0300 Subject: [PATCH 02/20] replace expiration time w/ expirationBlock --- channel/consensus_channel/consensus_channel.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index 7fe97bcbb..f2a96d248 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -387,9 +387,9 @@ type HTLC struct { amount *big.Int // 32 byte hash of the preimage, either keccak256 (evm-only) or SHA256 (LN compatible). hash common.Hash - // the expiration cutoff for the HTLC. If the preimage is not revealed before this time, - // the HTLC expires and funds are returned to the sender - expiration time.Time + // 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 @@ -900,7 +900,8 @@ func (vars *Vars) AddHTLC(p HTLC) error { return ErrDuplicateHTLC } - if p.expiration.Before(time.Now()) { + currentBlock := uint64(time.Now().Unix()) // todo: use a real block number + if p.expirationBlock < currentBlock { return ErrExpiredHTLC } From e39402b90c9bdc72100bf1fdc425c37680992cf3 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 10:36:46 -0300 Subject: [PATCH 03/20] refactor / fix for adding HTLC... now deducts funds from leader when leader pays --- channel/consensus_channel/consensus_channel.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index f2a96d248..2bf6d56cb 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -906,17 +906,21 @@ func (vars *Vars) AddHTLC(p HTLC) error { } 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 } - - // Include HTLC - o.htlcs[p.hash] = p + // EFFECTS + o.leader.amount.Sub(o.leader.amount, p.amount) } - followerPaying := p.paidFrom == o.follower.destination if followerPaying { // CHECKS if types.Gt(p.amount, o.follower.amount) { @@ -927,10 +931,6 @@ func (vars *Vars) AddHTLC(p HTLC) error { o.follower.amount.Sub(o.follower.amount, p.amount) } - if !leaderPaying && !followerPaying { - return ErrInvalidPayer - } - // EFFECTS vars.TurnNum += 1 o.htlcs[p.hash] = p From b207e48ac094a375d56e575eee1064030ae5640e Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 10:39:07 -0300 Subject: [PATCH 04/20] add payer refunds to HTLC timeout clearing fcn --- channel/consensus_channel/consensus_channel.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index 2bf6d56cb..de1390118 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -945,7 +945,19 @@ func (vars *Vars) RemoveHTLC(b []byte) { if len(b) == 0 { // Clear all expired HTLCs for hash, htlc := range o.htlcs { - if time.Now().After(htlc.expiration) { + 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) } } From 1b9a9b6881089a6441dae8442398c8fa27a8e0dd Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 11:18:23 -0300 Subject: [PATCH 05/20] add HTLC allocationType --- channel/state/outcome/HTLC.go | 61 +++++++++++++++++++++++++++++ channel/state/outcome/allocation.go | 1 + 2 files changed, 62 insertions(+) create mode 100644 channel/state/outcome/HTLC.go diff --git a/channel/state/outcome/HTLC.go b/channel/state/outcome/HTLC.go new file mode 100644 index 000000000..f17df3beb --- /dev/null +++ b/channel/state/outcome/HTLC.go @@ -0,0 +1,61 @@ +package outcome + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/statechannels/go-nitro/types" +) + +// An HTLC is an Allocation with AllocationType == HTLCAllocationType and Metadata = encode(HTLCMetaData) +type HTLCMetadata struct { + // The peer who has made the payment + Payer types.Address + // The peer who will receive the payment + Payee types.Address + + // The block number at which the HTLC will expire + ExpirationBlock uint64 + + // The hash whose preimage will unlock the HTLC + Hash types.Bytes32 +} + +// guaranteeMetadataTy describes the shape of HTLCMetadata, so that the abi encoder knows how to encode it +var htlcMetadataTy, _ = abi.NewType("tuple", "struct GuaranteeMetadata", []abi.ArgumentMarshaling{ + {Name: "Payer", Type: "address"}, + {Name: "Right", Type: "address"}, + {Name: "ExpirationBlock", Type: "uint64"}, + {Name: "Hash", Type: "bytes32"}, +}) + +// Encode returns the abi.encoded HTLCMetadata (suitable for packing in an Allocation.Metadata field) +func (h HTLCMetadata) Encode() (types.Bytes, error) { + return abi.Arguments{{Type: htlcMetadataTy}}.Pack(h) +} + +// rawHTLCMetadataType is an alias to the type returned when using the github.com/ethereum/go-ethereum/accounts/abi Unpack method with guaranteeMetadataTy +type rawHTLCMetadataType = struct { + Payer [20]uint8 + Payee [20]uint8 + ExpirationBlock uint64 + Hash [32]uint8 +} + +// convertToHTLCMetadata converts a rawGuaranteeMetadataType to a GuaranteeMetadata +func convertToHTLCMetadata(r rawHTLCMetadataType) HTLCMetadata { + htlcMetadata := HTLCMetadata{ + Payer: r.Payer, + Payee: r.Payee, + ExpirationBlock: r.ExpirationBlock, + Hash: r.Hash, + } + return htlcMetadata +} + +// Decode returns a GuaranteeMetaData from an abi encoding +func DecodeIntoHTLCMetadata(m []byte) (HTLCMetadata, error) { + unpacked, err := abi.Arguments{{Type: htlcMetadataTy}}.Unpack(m) + if err != nil { + return HTLCMetadata{}, err + } + return convertToHTLCMetadata(unpacked[0].(rawHTLCMetadataType)), nil +} diff --git a/channel/state/outcome/allocation.go b/channel/state/outcome/allocation.go index 6b90dd7a6..4625532cc 100644 --- a/channel/state/outcome/allocation.go +++ b/channel/state/outcome/allocation.go @@ -14,6 +14,7 @@ type AllocationType uint8 const ( NormalAllocationType AllocationType = iota GuaranteeAllocationType + HTLCAllocationType ) // Allocation declares an Amount to be paid to a Destination. From 61c121e13e22e3b3b8b054ca00dc0b5ab5959751 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 12:19:19 -0300 Subject: [PATCH 06/20] fix typo --- channel/state/outcome/allocation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/channel/state/outcome/allocation.go b/channel/state/outcome/allocation.go index 4625532cc..9bd07523b 100644 --- a/channel/state/outcome/allocation.go +++ b/channel/state/outcome/allocation.go @@ -66,7 +66,7 @@ func (a Allocations) Clone() Allocations { return clone } -// Total returns the toal amount allocated, summed across all destinations (regardless of AllocationType) +// Total returns the total amount allocated, summed across all destinations (regardless of AllocationType) func (a Allocations) Total() *big.Int { total := big.NewInt(0) for _, allocation := range a { From 11d9c6872a2fbc7db9d7115834bf4a07215866b3 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 12:39:20 -0300 Subject: [PATCH 07/20] add converter for LedgerOutcome HTLC -> Allocation --- .../consensus_channel/consensus_channel.go | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index de1390118..1fc39e6e2 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -396,6 +396,28 @@ type HTLC struct { 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. func (lo *LedgerOutcome) Clone() LedgerOutcome { clonedGuarantees := make(map[types.Destination]Guarantee) From 7a9683f6c4b728e37879f3c861365f0c5ea11fb0 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 12:40:14 -0300 Subject: [PATCH 08/20] add mutator for `Allocations` to add HTLCs --- channel/state/outcome/allocation.go | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/channel/state/outcome/allocation.go b/channel/state/outcome/allocation.go index 9bd07523b..3dd056fa8 100644 --- a/channel/state/outcome/allocation.go +++ b/channel/state/outcome/allocation.go @@ -130,6 +130,49 @@ var allocationsTy = abi.ArgumentMarshaling{ }, } +// DivertToHTLC returns a new Allocations, identical to the receiver but with +// the payer's amount reduced by amount and an encoded HTLC appended +func (a Allocations) DivertToHTLC( + payer types.Address, + payee types.Address, + amount *big.Int, + hash types.Bytes32, + expirationBlock uint64, +) (Allocations, error) { + newAllocations := make([]Allocation, 0, len(a)+1) + for i, allocation := range a { + newAllocations = append(newAllocations, allocation.Clone()) + switch newAllocations[i].Destination { + case types.AddressToDestination(payer): + newAllocations[i].Amount.Sub(newAllocations[i].Amount, amount) + case types.AddressToDestination(payee): + newAllocations[i].Amount.Add(newAllocations[i].Amount, amount) + } + if types.Gt(big.NewInt(0), newAllocations[i].Amount) { + return Allocations{}, errors.New(`insufficient funds`) + } + } + + encodedHTLCMetadata, err := HTLCMetadata{ + Payer: payer, + Payee: payee, + ExpirationBlock: expirationBlock, + Hash: hash, + }.Encode() + if err != nil { + return Allocations{}, fmt.Errorf(`error encoding HTLC: %w`, err) + } + + newAllocations = append(newAllocations, Allocation{ + Destination: types.Destination{}, + Amount: amount, + AllocationType: HTLCAllocationType, + Metadata: encodedHTLCMetadata, + }) + + return newAllocations, nil +} + // DivertToGuarantee returns a new Allocations, identical to the receiver but with // the leftDestination's amount reduced by leftAmount, // the rightDestination's amount reduced by rightAmount, From 7a08c5c029f26073286272052ac3bd33b7bd0fb7 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 12:40:53 -0300 Subject: [PATCH 09/20] add missing import --- channel/state/outcome/allocation.go | 1 + 1 file changed, 1 insertion(+) diff --git a/channel/state/outcome/allocation.go b/channel/state/outcome/allocation.go index 3dd056fa8..f4b671cb2 100644 --- a/channel/state/outcome/allocation.go +++ b/channel/state/outcome/allocation.go @@ -3,6 +3,7 @@ package outcome import ( "bytes" "errors" + "fmt" "math/big" "github.com/ethereum/go-ethereum/accounts/abi" From c773007d9682493a5323ed8814ffc572d5ad5290 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 12:41:36 -0300 Subject: [PATCH 10/20] add HTLCs to ledgerOutcome > exit-format converter --- channel/consensus_channel/consensus_channel.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index 1fc39e6e2..865d486f4 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -534,6 +534,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, From 8336a7874abde9c915e5649ccb022e4f8b810673 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Tue, 10 Oct 2023 23:33:19 -0300 Subject: [PATCH 11/20] make HTLC fields public... to permit good default behavior for marshaling --- .../consensus_channel/consensus_channel.go | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index 865d486f4..e9511a857 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -383,35 +383,35 @@ type LedgerOutcome struct { // // 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 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 + ExpirationBlock uint64 // the address that will receive the payment if the preimage is revealed - releaseTo types.Destination + ReleaseTo types.Destination // the address making the payment - paidFrom types.Destination + PaidFrom types.Destination } func (h HTLC) AsAllocation() outcome.Allocation { - payerAddr, _ := h.paidFrom.ToAddress() - payeeAddr, _ := h.releaseTo.ToAddress() + payerAddr, _ := h.PaidFrom.ToAddress() + payeeAddr, _ := h.ReleaseTo.ToAddress() htlcMetadata, err := outcome.HTLCMetadata{ Payer: payerAddr, Payee: payeeAddr, - Hash: h.hash, - ExpirationBlock: h.expirationBlock, + Hash: h.Hash, + ExpirationBlock: h.ExpirationBlock, }.Encode() if err != nil { panic(err) // todo: handle this error } return outcome.Allocation{ - Amount: h.amount, + Amount: h.Amount, Destination: types.Destination{}, AllocationType: outcome.HTLCAllocationType, Metadata: htlcMetadata, @@ -930,18 +930,18 @@ func (vars *Vars) Remove(p Remove) error { func (vars *Vars) AddHTLC(p HTLC) error { o := vars.Outcome // CHECKS - _, found := o.htlcs[p.hash] + _, found := o.htlcs[p.Hash] if found { return ErrDuplicateHTLC } currentBlock := uint64(time.Now().Unix()) // todo: use a real block number - if p.expirationBlock < currentBlock { + if p.ExpirationBlock < currentBlock { return ErrExpiredHTLC } - leaderPaying := p.paidFrom == o.leader.destination - followerPaying := p.paidFrom == o.follower.destination + leaderPaying := p.PaidFrom == o.leader.destination + followerPaying := p.PaidFrom == o.follower.destination if !leaderPaying && !followerPaying { return ErrInvalidPayer @@ -949,26 +949,26 @@ func (vars *Vars) AddHTLC(p HTLC) error { if leaderPaying { // CHECKS - if types.Gt(p.amount, o.leader.amount) { + if types.Gt(p.Amount, o.leader.amount) { return ErrInsufficientFunds } // EFFECTS - o.leader.amount.Sub(o.leader.amount, p.amount) + o.leader.amount.Sub(o.leader.amount, p.Amount) } if followerPaying { // CHECKS - if types.Gt(p.amount, o.follower.amount) { + if types.Gt(p.Amount, o.follower.amount) { return ErrInsufficientFunds } // EFFECTS - o.follower.amount.Sub(o.follower.amount, p.amount) + o.follower.amount.Sub(o.follower.amount, p.Amount) } // EFFECTS vars.TurnNum += 1 - o.htlcs[p.hash] = p + o.htlcs[p.Hash] = p return nil } @@ -981,16 +981,16 @@ func (vars *Vars) RemoveHTLC(b []byte) { // 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 { + if blockNumber >= htlc.ExpirationBlock { - leaderRefund := htlc.paidFrom == o.leader.destination - followerRefund := htlc.paidFrom == o.follower.destination + leaderRefund := htlc.PaidFrom == o.leader.destination + followerRefund := htlc.PaidFrom == o.follower.destination if leaderRefund { - o.leader.amount.Add(o.leader.amount, htlc.amount) + o.leader.amount.Add(o.leader.amount, htlc.Amount) } if followerRefund { - o.follower.amount.Add(o.follower.amount, htlc.amount) + o.follower.amount.Add(o.follower.amount, htlc.Amount) } delete(o.htlcs, hash) From b478780a22d52ab256b171ba79f8b30d70d7a388 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Wed, 11 Oct 2023 09:28:10 -0300 Subject: [PATCH 12/20] improve error message to identify broken test case --- channel/consensus_channel/serde_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/channel/consensus_channel/serde_test.go b/channel/consensus_channel/serde_test.go index 52d0fff8c..6f9acf73e 100644 --- a/channel/consensus_channel/serde_test.go +++ b/channel/consensus_channel/serde_test.go @@ -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)) } }) From 7f14a4e7d74509d6551e9d6dac0505d2d2dca5a6 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Wed, 11 Oct 2023 09:53:52 -0300 Subject: [PATCH 13/20] specify a literal proposal in msg service test... Previous test was relying on a default switch fallthrough returning a specific "blank" proposal type. --- .../messageservice/test-messageservice_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/node/engine/messageservice/test-messageservice_test.go b/node/engine/messageservice/test-messageservice_test.go index a907789e4..9dc9dfc49 100644 --- a/node/engine/messageservice/test-messageservice_test.go +++ b/node/engine/messageservice/test-messageservice_test.go @@ -15,13 +15,19 @@ var ( bobMS = NewTestMessageService(types.Address{'b'}, broker, 0) ) -var testId protocols.ObjectiveId = "VirtualDefund-0x0000000000000000000000000000000000000000000000000000000000000000" +var testId protocols.ObjectiveId = "VirtualDefund-0x0200000000000000000000000000000000000000000000000000000000000000" var aToB protocols.Message = protocols.CreateSignedProposalMessage( bobMS.address, consensus_channel.SignedProposal{ - Proposal: consensus_channel.Proposal{LedgerID: types.Destination{1}}, - TurnNum: 1, + Proposal: consensus_channel.Proposal{ + LedgerID: types.Destination{1}, + ToRemove: consensus_channel.Remove{ + Target: types.Destination{2}, + LeftAmount: big.NewInt(0), + }, + }, + TurnNum: 1, }, ) From 581c76fb27e95ce68a64817c217bd59af439314b Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Wed, 11 Oct 2023 09:56:46 -0300 Subject: [PATCH 14/20] add a nil check for copying htlc map... avoids clashes in equality checks between a nil map and an empty map --- channel/consensus_channel/consensus_channel.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index e9511a857..0dd3d3b71 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -428,6 +428,9 @@ func (lo *LedgerOutcome) Clone() LedgerOutcome { 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(), From ec103d4e38aab4d88993f918d9c765dc6c038eda Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Wed, 11 Oct 2023 09:57:00 -0300 Subject: [PATCH 15/20] add missing import --- node/engine/messageservice/test-messageservice_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/node/engine/messageservice/test-messageservice_test.go b/node/engine/messageservice/test-messageservice_test.go index 9dc9dfc49..63299bdc8 100644 --- a/node/engine/messageservice/test-messageservice_test.go +++ b/node/engine/messageservice/test-messageservice_test.go @@ -1,6 +1,7 @@ package messageservice import ( + "math/big" "testing" "github.com/statechannels/go-nitro/channel/consensus_channel" From dbc0cd655a713c35588605b943e140ca3e262cf5 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Wed, 11 Oct 2023 09:59:15 -0300 Subject: [PATCH 16/20] add HTLC proposal -> ObjectiveID mapping... requires further thought - these returned IDs only take the ledger channel ID as identifier. Multiple simultaneous objectives would collide. --- protocols/messages.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/protocols/messages.go b/protocols/messages.go index 4ee3a3b88..c7f706596 100644 --- a/protocols/messages.go +++ b/protocols/messages.go @@ -67,20 +67,32 @@ func (se *SideEffects) Merge(other SideEffects) { // GetProposalObjectiveId returns the objectiveId for a proposal. func GetProposalObjectiveId(p consensus_channel.Proposal) (ObjectiveId, error) { switch p.Type() { - case "AddProposal": + case consensus_channel.AddProposal: { const prefix = "VirtualFund-" channelId := p.ToAdd.Guarantee.Target().String() return ObjectiveId(prefix + channelId), nil } - case "RemoveProposal": + case consensus_channel.RemoveProposal: { const prefix = "VirtualDefund-" channelId := p.ToRemove.Target.String() return ObjectiveId(prefix + channelId), nil } + case consensus_channel.AddHTLCProposal: + { + const prefix = "AddHTLC-" + channelId := p.LedgerID.String() + return ObjectiveId(prefix + channelId), nil + } + case consensus_channel.RemoveHTLCProposal: + { + const prefix = "RemoveHTLC-" + channelId := p.LedgerID.String() + return ObjectiveId(prefix + channelId), nil + } default: { return "", errors.New("invalid proposal type") From 505fe23d39bd4f39be76abc39413843d090b5641 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Wed, 11 Oct 2023 10:00:21 -0300 Subject: [PATCH 17/20] update string literals for serde tests... and update jsonProposal helper struct with added fields --- channel/consensus_channel/serde.go | 8 +++++--- channel/consensus_channel/serde_test.go | 2 +- protocols/messages_test.go | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/channel/consensus_channel/serde.go b/channel/consensus_channel/serde.go index e50aa34e5..22abc0fdb 100644 --- a/channel/consensus_channel/serde.go +++ b/channel/consensus_channel/serde.go @@ -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 diff --git a/channel/consensus_channel/serde_test.go b/channel/consensus_channel/serde_test.go index 6f9acf73e..a0551560d 100644 --- a/channel/consensus_channel/serde_test.go +++ b/channel/consensus_channel/serde_test.go @@ -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 diff --git a/protocols/messages_test.go b/protocols/messages_test.go index 1690eeac0..82251afa8 100644 --- a/protocols/messages_test.go +++ b/protocols/messages_test.go @@ -76,7 +76,7 @@ func TestMessage(t *testing.T) { RejectedObjectives: []ObjectiveId{"say-hello-to-my-little-friend2"}, } - msgString := `{"To":"0x6100000000000000000000000000000000000000","From":"0x0000000000000000000000000000000000000000","ObjectivePayloads":[{"PayloadData":"eyJTdGF0ZSI6eyJQYXJ0aWNpcGFudHMiOlsiMHhmNWExYmI1NjA3YzlkMDc5ZTQ2ZDFiM2RjMzNmMjU3ZDkzN2I0M2JkIiwiMHg3NjBiZjI3Y2Q0NTAzNmE2YzQ4NjgwMmQzMGI1ZDkwY2ZmYmUzMWZlIl0sIkNoYW5uZWxOb25jZSI6MzcxNDA2NzY1ODAsIkFwcERlZmluaXRpb24iOiIweDVlMjllNWFiOGVmMzNmMDUwYzdjYzEwYjVhMDQ1NmQ5NzVjNWY4OGQiLCJDaGFsbGVuZ2VEdXJhdGlvbiI6NjAsIkFwcERhdGEiOiIiLCJPdXRjb21lIjpbeyJBc3NldCI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsIkFzc2V0TWV0YWRhdGEiOnsiQXNzZXRUeXBlIjowLCJNZXRhZGF0YSI6IiJ9LCJBbGxvY2F0aW9ucyI6W3siRGVzdGluYXRpb24iOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGY1YTFiYjU2MDdjOWQwNzllNDZkMWIzZGMzM2YyNTdkOTM3YjQzYmQiLCJBbW91bnQiOjUsIkFsbG9jYXRpb25UeXBlIjowLCJNZXRhZGF0YSI6bnVsbH0seyJEZXN0aW5hdGlvbiI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwZWUxOGZmMTU3NTA1NTY5MTAwOWFhMjQ2YWU2MDgxMzJjNTdhNDIyYyIsIkFtb3VudCI6NSwiQWxsb2NhdGlvblR5cGUiOjAsIk1ldGFkYXRhIjpudWxsfV19XSwiVHVybk51bSI6NSwiSXNGaW5hbCI6ZmFsc2V9LCJTaWdzIjp7fX0=","ObjectiveId":"say-hello-to-my-little-friend","Type":""}],"LedgerProposals":[{"Signature":"0x00","Proposal":{"LedgerID":"0x6c00000000000000000000000000000000000000000000000000000000000000","ToAdd":{"Guarantee":{"Amount":1,"Target":"0x6100000000000000000000000000000000000000000000000000000000000000","Left":"0x6200000000000000000000000000000000000000000000000000000000000000","Right":"0x6300000000000000000000000000000000000000000000000000000000000000"},"LeftDeposit":1},"ToRemove":{"Target":"0x0000000000000000000000000000000000000000000000000000000000000000","LeftAmount":null}},"TurnNum":0},{"Signature":"0x00","Proposal":{"LedgerID":"0x6c00000000000000000000000000000000000000000000000000000000000000","ToAdd":{"Guarantee":{"Amount":null,"Target":"0x0000000000000000000000000000000000000000000000000000000000000000","Left":"0x0000000000000000000000000000000000000000000000000000000000000000","Right":"0x0000000000000000000000000000000000000000000000000000000000000000"},"LeftDeposit":null},"ToRemove":{"Target":"0x6100000000000000000000000000000000000000000000000000000000000000","LeftAmount":1}},"TurnNum":0}],"Payments":[{"ChannelId":"0x6400000000000000000000000000000000000000000000000000000000000000","Amount":123,"Signature":"0x00"}],"RejectedObjectives":["say-hello-to-my-little-friend2"]}` + msgString := `{"To":"0x6100000000000000000000000000000000000000","From":"0x0000000000000000000000000000000000000000","ObjectivePayloads":[{"PayloadData":"eyJTdGF0ZSI6eyJQYXJ0aWNpcGFudHMiOlsiMHhmNWExYmI1NjA3YzlkMDc5ZTQ2ZDFiM2RjMzNmMjU3ZDkzN2I0M2JkIiwiMHg3NjBiZjI3Y2Q0NTAzNmE2YzQ4NjgwMmQzMGI1ZDkwY2ZmYmUzMWZlIl0sIkNoYW5uZWxOb25jZSI6MzcxNDA2NzY1ODAsIkFwcERlZmluaXRpb24iOiIweDVlMjllNWFiOGVmMzNmMDUwYzdjYzEwYjVhMDQ1NmQ5NzVjNWY4OGQiLCJDaGFsbGVuZ2VEdXJhdGlvbiI6NjAsIkFwcERhdGEiOiIiLCJPdXRjb21lIjpbeyJBc3NldCI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsIkFzc2V0TWV0YWRhdGEiOnsiQXNzZXRUeXBlIjowLCJNZXRhZGF0YSI6IiJ9LCJBbGxvY2F0aW9ucyI6W3siRGVzdGluYXRpb24iOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGY1YTFiYjU2MDdjOWQwNzllNDZkMWIzZGMzM2YyNTdkOTM3YjQzYmQiLCJBbW91bnQiOjUsIkFsbG9jYXRpb25UeXBlIjowLCJNZXRhZGF0YSI6bnVsbH0seyJEZXN0aW5hdGlvbiI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwZWUxOGZmMTU3NTA1NTY5MTAwOWFhMjQ2YWU2MDgxMzJjNTdhNDIyYyIsIkFtb3VudCI6NSwiQWxsb2NhdGlvblR5cGUiOjAsIk1ldGFkYXRhIjpudWxsfV19XSwiVHVybk51bSI6NSwiSXNGaW5hbCI6ZmFsc2V9LCJTaWdzIjp7fX0=","ObjectiveId":"say-hello-to-my-little-friend","Type":""}],"LedgerProposals":[{"Signature":"0x00","Proposal":{"LedgerID":"0x6c00000000000000000000000000000000000000000000000000000000000000","ToAdd":{"Guarantee":{"Amount":1,"Target":"0x6100000000000000000000000000000000000000000000000000000000000000","Left":"0x6200000000000000000000000000000000000000000000000000000000000000","Right":"0x6300000000000000000000000000000000000000000000000000000000000000"},"LeftDeposit":1},"ToRemove":{"Target":"0x0000000000000000000000000000000000000000000000000000000000000000","LeftAmount":null},"AddHTLC":{"Amount":null,"Hash":"0x0000000000000000000000000000000000000000000000000000000000000000","ExpirationBlock":0,"ReleaseTo":"0x0000000000000000000000000000000000000000000000000000000000000000","PaidFrom":"0x0000000000000000000000000000000000000000000000000000000000000000"},"RemoveHTLC":null},"TurnNum":0},{"Signature":"0x00","Proposal":{"LedgerID":"0x6c00000000000000000000000000000000000000000000000000000000000000","ToAdd":{"Guarantee":{"Amount":null,"Target":"0x0000000000000000000000000000000000000000000000000000000000000000","Left":"0x0000000000000000000000000000000000000000000000000000000000000000","Right":"0x0000000000000000000000000000000000000000000000000000000000000000"},"LeftDeposit":null},"ToRemove":{"Target":"0x6100000000000000000000000000000000000000000000000000000000000000","LeftAmount":1},"AddHTLC":{"Amount":null,"Hash":"0x0000000000000000000000000000000000000000000000000000000000000000","ExpirationBlock":0,"ReleaseTo":"0x0000000000000000000000000000000000000000000000000000000000000000","PaidFrom":"0x0000000000000000000000000000000000000000000000000000000000000000"},"RemoveHTLC":null},"TurnNum":0}],"Payments":[{"ChannelId":"0x6400000000000000000000000000000000000000000000000000000000000000","Amount":123,"Signature":"0x00"}],"RejectedObjectives":["say-hello-to-my-little-friend2"]}` t.Run(`serialize`, func(t *testing.T) { got, err := msg.Serialize() if err != nil { From 053caf2814be20e21dc266bdecad9255bcec7c8c Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Wed, 11 Oct 2023 10:15:36 -0300 Subject: [PATCH 18/20] comment out empty branch (for staticcheck) --- channel/consensus_channel/consensus_channel.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index 0dd3d3b71..4869e834f 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -999,17 +999,18 @@ func (vars *Vars) RemoveHTLC(b []byte) { delete(o.htlcs, hash) } } - } else { - // Clear the HTLC with the given preimage + } + // else { + // Clear the HTLC with the given preimage - // TODO: implement + // TODO: implement - // hash the preimage with Keccak256 (evm-native payment), SHA256 (ln-compatible payment) + // hash the preimage with Keccak256 (evm-native payment), SHA256 (ln-compatible payment) - // check result against keys in o.htlcs + // check result against keys in o.htlcs - // if found, delete the HTLC and credit the receiver - } + // if found, delete the HTLC and credit the receiver + // } } // Remove is a proposal to remove a guarantee for the given virtual channel. From 73d2fbdec278dedc56d82985951c47c94538ba4e Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Wed, 11 Oct 2023 12:21:53 -0300 Subject: [PATCH 19/20] add RemoveHTLC implementation --- .../consensus_channel/consensus_channel.go | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index 4869e834f..89662cfbe 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -12,6 +12,7 @@ import ( "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" ) @@ -976,6 +977,8 @@ func (vars *Vars) AddHTLC(p HTLC) error { 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 @@ -999,18 +1002,53 @@ func (vars *Vars) RemoveHTLC(b []byte) { delete(o.htlcs, hash) } } - } - // else { - // Clear the HTLC with the given preimage + } 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)) - // TODO: implement + if _, ok := o.htlcs[kecHash]; ok { + toRemove = kecHash + } - // hash the preimage with Keccak256 (evm-native payment), SHA256 (ln-compatible payment) + 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. - // check result against keys in o.htlcs + // todo: handle this via logging, circling back to a clearExpiredHTLC operation, etc. + return + } - // if found, delete the HTLC and credit the receiver - // } + 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. From a2f9a68fb4634a841981560508ff91e417e07547 Mon Sep 17 00:00:00 2001 From: Colin Kennedy Date: Wed, 11 Oct 2023 12:44:00 -0300 Subject: [PATCH 20/20] go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2e91a2c1a..c5ddce33c 100644 --- a/go.mod +++ b/go.mod @@ -176,7 +176,7 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tklauser/go-sysconf v0.3.5 // indirect github.com/tklauser/numcpus v0.2.2 // indirect - golang.org/x/crypto v0.12.0 // indirect + golang.org/x/crypto v0.12.0 golang.org/x/sys v0.11.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect )