Skip to content
Open
1,205 changes: 1,205 additions & 0 deletions htlcswitch/fuzz_link_test.go

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions htlcswitch/htlc_commitment_state_machine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## States and Transitions
```mermaid
---
title: Channel Link State Machine
---

stateDiagram-v2

[*] --> Clean

Clean --> Pending : receive update_* (processRemoteUpdate*)
Pending --> Pending : more update_*

Pending --> TrySendCommitSig : BatchTicker / OweCommitment
TrySendCommitSig --> WaitingRevoke : SignNextCommitment ok + send CommitSig
TrySendCommitSig --> WindowExhausted : SignNextCommitment = ErrNoWindow

WaitingRevoke --> Pending : receive RevokeAndAck (ReceiveRevocation)
WaitingRevoke --> Clean : receive RevokeAndAck and channel clean

Pending --> RecvCommitSig : receive CommitSig (processRemoteCommitSig)
RecvCommitSig --> SendRevoke : ReceiveNewCommitment ok
SendRevoke --> Pending : RevokeCurrentCommitment + send RevokeAndAck

Pending --> TrySendCommitSig : after RevokeAndAck/RecvRevoke if OweCommitment

Clean --> Quiescent : STFU
Quiescent --> Clean : resume

state Failed <<terminal>>
Pending --> Failed : invalid sig/revocation / timeout
WaitingRevoke --> Failed : PendingCommitTicker timeout

```

## Legend

| Term | Meaning |
|------|---------|
| `OweCommitment` | Boolean flag set on the link when there are pending local updates that have not yet been covered by a `CommitSig`. Triggers sending the next commitment signature after a `RevokeAndAck` is received or when the batch ticker fires. |
| `WindowExhausted` | `SignNextCommitment` returned `ErrNoWindow`, meaning the in-flight HTLC limit was reached. The link waits for a `RevokeAndAck` to free a slot before retrying. |
| `BatchTicker` | Periodic timer that coalesces multiple downstream updates into a single `CommitSig` round. Replaced by `noopTicker` in fuzz/test harnesses. |
| `PendingCommitTicker` | Watchdog timer that fires if a `RevokeAndAck` is not received within the allowed window, transitioning the link to `Failed`. |
43 changes: 43 additions & 0 deletions htlcswitch/link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2135,6 +2135,7 @@ type singleLinkTestHarness struct {
aliceBatchTicker chan time.Time
start func() error
aliceRestore func() (*lnwallet.LightningChannel, error)
invoiceRegistry *mockInvoiceRegistry
}

func newSingleLinkTestHarness(t *testing.T, chanAmt,
Expand Down Expand Up @@ -2277,6 +2278,7 @@ func newSingleLinkTestHarness(t *testing.T, chanAmt,
aliceBatchTicker: bticker.Force,
start: start,
aliceRestore: aliceLc.restore,
invoiceRegistry: invoiceRegistry,
}

return harness, nil
Expand Down Expand Up @@ -5012,6 +5014,47 @@ func generateHtlcAndInvoice(t *testing.T,
return htlc, invoice
}

// generateSingleHopHtlc generate a single hop htlc to send to the receiver.
func generateSingleHopHtlc(t *testing.T, id uint64,
htlcAmt lnwire.MilliSatoshi) (*lnwire.UpdateAddHTLC, lntypes.Preimage,
error) {

t.Helper()

htlcExpiry := testStartingHeight + testInvoiceCltvExpiry
hops := []*hop.Payload{
hop.NewLegacyPayload(&sphinx.HopData{
Realm: [1]byte{}, // hop.BitcoinNetwork
NextAddress: [8]byte{}, // hop.Exit,
ForwardAmount: uint64(htlcAmt),
OutgoingCltv: uint32(htlcExpiry),
}),
}
blob, err := generateRoute(hops...)
if err != nil {
return nil, lntypes.Preimage{}, err
}

var preimage lntypes.Preimage
r, err := generateRandomBytes(sha256.Size)
if err != nil {
return nil, preimage, err
}
copy(preimage[:], r)

rhash := sha256.Sum256(preimage[:])

htlc := &lnwire.UpdateAddHTLC{
ID: id,
PaymentHash: rhash,
Amount: htlcAmt,
Expiry: uint32(htlcExpiry),
OnionBlob: blob,
}

return htlc, preimage, nil
}

// TestChannelLinkNoMoreUpdates tests that we won't send a new commitment
// when there are no new updates to sign.
func TestChannelLinkNoMoreUpdates(t *testing.T) {
Expand Down
83 changes: 83 additions & 0 deletions htlcswitch/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,11 @@ func newMockRegistry(t testing.TB) *mockInvoiceRegistry {
},
)
registry.Start()
t.Cleanup(func() {
if err := registry.Stop(); err != nil {
t.Errorf("registry.Stop: %v", err)
}
})

return &mockInvoiceRegistry{
registry: registry,
Expand Down Expand Up @@ -1179,3 +1184,81 @@ func (h *mockHTLCNotifier) NotifyFinalHtlcEvent(key models.CircuitKey,
info channeldb.FinalHtlcInfo) {

}

// mockMailBox is a no-op mailbox for testing.
type mockMailBox struct{}

// Compile-time assertion that mockMailBox implements MailBox.
var _ MailBox = (*mockMailBox)(nil)

func (m *mockMailBox) AddMessage(msg lnwire.Message) error {
return nil
}

func (m *mockMailBox) AddPacket(packet *htlcPacket) error {
return nil
}

func (m *mockMailBox) HasPacket(CircuitKey) bool {
return false
}

func (m *mockMailBox) AckPacket(CircuitKey) bool {
return false
}

func (m *mockMailBox) FailAdd(packet *htlcPacket) {

}

func (m *mockMailBox) MessageOutBox() chan lnwire.Message {
return make(chan lnwire.Message)
}

func (m *mockMailBox) PacketOutBox() chan *htlcPacket {
return make(chan *htlcPacket)
}

func (m *mockMailBox) ResetMessages() error {
return nil
}

func (m *mockMailBox) ResetPackets() error {
return nil
}

func (m *mockMailBox) SetDustClosure(isDust dustClosure) {

}

func (m *mockMailBox) SetFeeRate(feerate chainfee.SatPerKWeight) {

}

func (m *mockMailBox) DustPackets() (lnwire.MilliSatoshi, lnwire.MilliSatoshi) {
return 0, 0
}

func (m *mockMailBox) Start() {

}

func (m *mockMailBox) Stop() {

}

type noopTicker struct{}

func (n *noopTicker) Ticks() <-chan time.Time {
// Returning nil intentionally: a receive on a nil channel blocks
// forever, so the link's timer-driven paths never fire.
return nil
}

func (n *noopTicker) Stop() {}

func (n *noopTicker) Pause() {}

func (n *noopTicker) Resume() {}

func (n *noopTicker) ForceTick() {}
Loading
Loading