lnwallet: add send/recv of HTLC cancellations to state machine

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.
This commit is contained in:
Olaoluwa Osuntokun 2017-01-05 20:54:39 -08:00
parent 4e53cf8b49
commit fd0c0574e6
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
2 changed files with 144 additions and 8 deletions

@ -89,7 +89,7 @@ type updateType uint8
const ( const (
Add updateType = iota Add updateType = iota
Timeout Cancel
Settle Settle
) )
@ -1100,9 +1100,9 @@ func processRemoveEntry(htlc *PaymentDescriptor, ourBalance,
// the HTLC amount. // the HTLC amount.
case isIncoming && htlc.EntryType == Settle: case isIncoming && htlc.EntryType == Settle:
*ourBalance += htlc.Amount *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. // HTLC should return to the remote party.
case isIncoming && htlc.EntryType == Timeout: case isIncoming && htlc.EntryType == Cancel:
*theirBalance += htlc.Amount *theirBalance += htlc.Amount
// If an outgoing HTLC is being settled, then this means that the // If an outgoing HTLC is being settled, then this means that the
// downstream party resented the preimage or learned of it via a // 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. // the value of the HTLC.
case !isIncoming && htlc.EntryType == Settle: case !isIncoming && htlc.EntryType == Settle:
*theirBalance += htlc.Amount *theirBalance += htlc.Amount
// Otherwise, one of our outgoing HTLC's has timed out, so the value of // Otherwise, one of our outgoing HTLC's has been cancelled, so the
// the HTLC should be returned to our settled balance. // value of the HTLC should be returned to our settled balance.
case !isIncoming && htlc.EntryType == Timeout: case !isIncoming && htlc.EntryType == Cancel:
*ourBalance += htlc.Amount *ourBalance += htlc.Amount
} }
@ -1715,8 +1715,58 @@ func (lc *LightningChannel) ReceiveHTLCSettle(preimage [32]byte, logIndex uint32
return nil return nil
} }
// TimeoutHTLC... // CancelHTLC attempts to cancel a targeted HTLC by its log index, inserting an
func (lc *LightningChannel) TimeoutHTLC() error { // 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 return nil
} }

@ -1158,3 +1158,89 @@ func TestStateUpdatePersistence(t *testing.T) {
3000, bobChannel.channelState.TotalSatoshisSent) 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)
}
}