diff --git a/channel/consensus_channel/consensus_channel.go b/channel/consensus_channel/consensus_channel.go index 23f693c64..89662cfbe 100644 --- a/channel/consensus_channel/consensus_channel.go +++ b/channel/consensus_channel/consensus_channel.go @@ -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" ) @@ -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 ( @@ -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. @@ -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, } } @@ -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, @@ -564,9 +631,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 +643,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 +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 @@ -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 } } @@ -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)) @@ -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 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 52d0fff8c..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 @@ -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)) } }) 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..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" @@ -14,6 +15,7 @@ type AllocationType uint8 const ( NormalAllocationType AllocationType = iota GuaranteeAllocationType + HTLCAllocationType ) // Allocation declares an Amount to be paid to a Destination. @@ -65,7 +67,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 { @@ -129,6 +131,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, 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 ) diff --git a/node/engine/messageservice/test-messageservice_test.go b/node/engine/messageservice/test-messageservice_test.go index a907789e4..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" @@ -15,13 +16,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, }, ) 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") 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 {