From fd0c0574e6079b55a4fcde6ae6a2084712190f9d Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 5 Jan 2017 20:54:39 -0800 Subject: [PATCH] lnwallet: add send/recv of HTLC cancellations to state machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the ability to send/recv HTLC cancellation to the commitment state machine. Previously this feature had been unimplemented within the state machine, with only adds/settles working. With this change, there’s now now no concept of “timing” out HTLC’s, only the cancellation of HTLC’s which may be triggered for various reasons. --- lnwallet/channel.go | 66 ++++++++++++++++++++++++++---- lnwallet/channel_test.go | 86 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 8 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index dc62f4d5..03ddff35 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -89,7 +89,7 @@ type updateType uint8 const ( Add updateType = iota - Timeout + Cancel Settle ) @@ -1100,9 +1100,9 @@ func processRemoveEntry(htlc *PaymentDescriptor, ourBalance, // the HTLC amount. case isIncoming && htlc.EntryType == Settle: *ourBalance += htlc.Amount - // Otherwise, this HTLC is being timed out, therefore the value of the + // Otherwise, this HTLC is being cancelled, therefore the value of the // HTLC should return to the remote party. - case isIncoming && htlc.EntryType == Timeout: + case isIncoming && htlc.EntryType == Cancel: *theirBalance += htlc.Amount // If an outgoing HTLC is being settled, then this means that the // downstream party resented the preimage or learned of it via a @@ -1110,9 +1110,9 @@ func processRemoveEntry(htlc *PaymentDescriptor, ourBalance, // the value of the HTLC. case !isIncoming && htlc.EntryType == Settle: *theirBalance += htlc.Amount - // Otherwise, one of our outgoing HTLC's has timed out, so the value of - // the HTLC should be returned to our settled balance. - case !isIncoming && htlc.EntryType == Timeout: + // Otherwise, one of our outgoing HTLC's has been cancelled, so the + // value of the HTLC should be returned to our settled balance. + case !isIncoming && htlc.EntryType == Cancel: *ourBalance += htlc.Amount } @@ -1715,8 +1715,58 @@ func (lc *LightningChannel) ReceiveHTLCSettle(preimage [32]byte, logIndex uint32 return nil } -// TimeoutHTLC... -func (lc *LightningChannel) TimeoutHTLC() error { +// CancelHTLC attempts to cancel a targeted HTLC by its log index, inserting an +// entry which will remove the target log entry within the next commitment +// update. This method is intended to be called in order to cancel in +// _incoming_ HTLC. +func (lc *LightningChannel) CancelHTLC(logIndex uint32) error { + lc.Lock() + defer lc.Unlock() + + addEntry, ok := lc.theirLogIndex[logIndex] + if !ok { + return fmt.Errorf("unable to find HTLC to cancel") + } + + htlc := addEntry.Value.(*PaymentDescriptor) + + pd := &PaymentDescriptor{ + Amount: htlc.Amount, + Index: lc.ourLogCounter, + ParentIndex: htlc.Index, + EntryType: Cancel, + } + + lc.ourUpdateLog.PushBack(pd) + lc.ourLogCounter++ + + return nil +} + +// ReceiveCancelHTLC attempts to cancel a targeted HTLC by its log index, +// inserting an entry which will remove the target log entry within the next +// commitment update. This method should be called in response to the upstream +// party cancelling an outgoing HTLC. +func (lc *LightningChannel) ReceiveCancelHTLC(logIndex uint32) error { + lc.Lock() + defer lc.Unlock() + + addEntry, ok := lc.ourLogIndex[logIndex] + if !ok { + return fmt.Errorf("unable to find HTLC to cancel") + } + + htlc := addEntry.Value.(*PaymentDescriptor) + pd := &PaymentDescriptor{ + Amount: htlc.Amount, + ParentIndex: htlc.Index, + Index: lc.theirLogCounter, + EntryType: Cancel, + } + + lc.theirUpdateLog.PushBack(pd) + lc.theirLogCounter++ + return nil } diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index fca83d85..d10a89bd 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -1158,3 +1158,89 @@ func TestStateUpdatePersistence(t *testing.T) { 3000, bobChannel.channelState.TotalSatoshisSent) } } + +func TestCancelHTLC(t *testing.T) { + // Create a test channel which will be used for the duration of this + // unittest. The channel will be funded evenly with Alice having 5 BTC, + // and Bob having 5 BTC. + aliceChannel, bobChannel, cleanUp, err := createTestChannels(5) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + // Add a new HTLC from Alice to Bob, then trigger a new state + // transition in order to include it in the latest state. + const htlcAmt = btcutil.SatoshiPerBitcoin + + var preImage [32]byte + copy(preImage[:], bytes.Repeat([]byte{0xaa}, 32)) + htlc := &lnwire.HTLCAddRequest{ + RedemptionHashes: [][32]byte{fastsha256.Sum256(preImage[:])}, + Amount: htlcAmt, + Expiry: 10, + } + + if _, err := aliceChannel.AddHTLC(htlc); err != nil { + t.Fatalf("unable to add alice htlc: %v", err) + } + bobHtlcIndex, err := bobChannel.ReceiveHTLC(htlc) + if err != nil { + t.Fatalf("unable to add bob htlc: %v", err) + } + if err := forceStateTransition(aliceChannel, bobChannel); err != nil { + t.Fatalf("unable to create new commitment state: %v", err) + } + + // With the HTLC committed, Alice's balance should reflect the clearing + // of the new HTLC. + aliceExpectedBalance := btcutil.Amount(btcutil.SatoshiPerBitcoin * 4) + if aliceChannel.channelState.OurBalance != aliceExpectedBalance { + t.Fatalf("Alice's balance is wrong: expected %v, got %v", + aliceExpectedBalance, aliceChannel.channelState.OurBalance) + } + + // Now, with the HTLC committed on both sides, trigger a cancellation + // from Bob to Alice, removing the HTLC. + if err := bobChannel.CancelHTLC(bobHtlcIndex); err != nil { + t.Fatalf("unable to cancel HTLC: %v", err) + } + if err := aliceChannel.ReceiveCancelHTLC(bobHtlcIndex); err != nil { + t.Fatalf("unable to recv htlc cancel: %v", err) + } + + // Now trigger another state transition, the HTLC should now be removed + // from both sides, with balances reflected. + if err := forceStateTransition(bobChannel, aliceChannel); err != nil { + t.Fatalf("unable to create new commitment: %v", err) + } + + // Now HTLC's should be present on the commitment transaction for + // either side. + if len(aliceChannel.localCommitChain.tip().outgoingHTLCs) != 0 || + len(aliceChannel.remoteCommitChain.tip().outgoingHTLCs) != 0 { + t.Fatalf("htlc's still active from alice's POV") + } + if len(bobChannel.localCommitChain.tip().outgoingHTLCs) != 0 || + len(bobChannel.remoteCommitChain.tip().outgoingHTLCs) != 0 { + t.Fatalf("htlc's still active from bob's POV") + } + + expectedBalance := btcutil.Amount(btcutil.SatoshiPerBitcoin * 5) + if aliceChannel.channelState.OurBalance != expectedBalance { + t.Fatalf("balance is wrong: expected %v, got %v", + aliceChannel.channelState.OurBalance, expectedBalance) + } + if aliceChannel.channelState.TheirBalance != expectedBalance { + t.Fatalf("balance is wrong: expected %v, got %v", + aliceChannel.channelState.TheirBalance, expectedBalance) + } + if bobChannel.channelState.OurBalance != expectedBalance { + t.Fatalf("balance is wrong: expected %v, got %v", + bobChannel.channelState.OurBalance, expectedBalance) + } + if bobChannel.channelState.TheirBalance != expectedBalance { + t.Fatalf("balance is wrong: expected %v, got %v", + bobChannel.channelState.TheirBalance, expectedBalance) + } +}