Repository: lightningnetwork/lnd
Pull request #7576 (2023-04-05 03:17:23)

lnd: merge v0.17.0 staging branch into master
In this PR, we move to merge the `v0.17.0` staging branch into the master branch. Now that `v0.16.0` has been released, we no longer need this branch, and can merge everything for `v0.17.0` directly into the master branch. 


Patch:
diff --git a/build/version.go b/build/version.go
index 40b1719bb..223cf0708 100644
--- a/build/version.go
+++ b/build/version.go
@@ -43,11 +43,11 @@ const (
 	AppMinor uint = 16
 
 	// AppPatch defines the application patch for this binary.
-	AppPatch uint = 99
+	AppPatch uint = 0
 
 	// AppPreRelease MUST only contain characters from semanticAlphabet
 	// per the semantic versioning spec.
-	AppPreRelease = "beta"
+	AppPreRelease = "beta.rc3"
 )
 
 func init() {
diff --git a/channeldb/invoices.go b/channeldb/invoices.go
index b4ce3ba39..215c54609 100644
--- a/channeldb/invoices.go
+++ b/channeldb/invoices.go
@@ -1858,7 +1858,7 @@ func settleHtlcsAmp(invoice *invpkg.Invoice,
 
 // updateInvoice fetches the invoice, obtains the update descriptor from the
 // callback and applies the updates in a single db transaction.
-func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, //nolint:lll,funlen
+func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 	settleIndex, setIDIndex kvdb.RwBucket, invoiceNum []byte,
 	callback invpkg.InvoiceUpdateCallback) (*invpkg.Invoice, error) {
 
@@ -1893,27 +1893,128 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 		return &invoice, nil
 	}
 
-	var (
-		newState = invoice.State
-		setID    *[32]byte
-	)
+	switch update.UpdateType {
+	case invpkg.CancelHTLCsUpdate:
+		return d.cancelHTLCs(invoices, invoiceNum, &invoice, update)
 
-	// We can either get the set ID from the main state update (if the
-	// state is changing), or via the hint passed in returned by the update
-	// call back.
-	if update.State != nil {
-		setID = update.State.SetID
-		newState = update.State.NewState
-	} else if update.SetID != nil {
-		// When we go to cancel HTLCs, there's no new state, but the
-		// set of HTLCs to be cancelled along with the setID affected
-		// will be passed in.
-		setID = (*[32]byte)(update.SetID)
+	case invpkg.AddHTLCsUpdate:
+		return d.addHTLCs(
+			invoices, settleIndex, setIDIndex, invoiceNum, &invoice,
+			hash, update,
+		)
+
+	case invpkg.SettleHodlInvoiceUpdate:
+		return d.settleHodlInvoice(
+			invoices, settleIndex, invoiceNum, &invoice, hash,
+			update.State,
+		)
+
+	case invpkg.CancelInvoiceUpdate:
+		return d.cancelInvoice(
+			invoices, invoiceNum, &invoice, hash, update.State,
+		)
+
+	default:
+		return nil, fmt.Errorf("unknown update type: %s",
+			update.UpdateType)
+	}
+}
+
+// cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc.
+//
+// NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the
+// InvoiceUpdateDesc.
+func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte,
+	invoice *invpkg.Invoice,
+	update *invpkg.InvoiceUpdateDesc) (*invpkg.Invoice, error) {
+
+	timestamp := d.clock.Now()
+
+	// Process add actions from update descriptor.
+	htlcsAmpUpdate := make(map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) //nolint:lll
+
+	// Process cancel actions from update descriptor.
+	cancelHtlcs := update.CancelHtlcs
+	for key, htlc := range invoice.Htlcs {
+		htlc := htlc
+
+		// Check whether this htlc needs to be canceled. If it does,
+		// update the htlc state to Canceled.
+		_, cancel := cancelHtlcs[key]
+		if !cancel {
+			continue
+		}
+
+		err := cancelSingleHtlc(timestamp, htlc, invoice.State)
+		if err != nil {
+			return nil, err
+		}
+
+		// Delete processed cancel action, so that we can check later
+		// that there are no actions left.
+		delete(cancelHtlcs, key)
+
+		// Tally this into the set of HTLCs that need to be updated on
+		// disk, but once again, only if this is an AMP invoice.
+		if invoice.IsAMP() {
+			cancelHtlcsAmp(
+				invoice, htlcsAmpUpdate, htlc, key,
+			)
+		}
+	}
+
+	// Verify that we didn't get an action for htlcs that are not present on
+	// the invoice.
+	if len(cancelHtlcs) > 0 {
+		return nil, errors.New("cancel action on non-existent htlc(s)")
+	}
+
+	err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice)
+	if err != nil {
+		return nil, err
+	}
+
+	// If this is an AMP invoice, then we'll actually store the rest of the
+	// HTLCs in-line with the invoice, using the invoice ID as a prefix,
+	// and the AMP key as a suffix: invoiceNum || setID.
+	if invoice.IsAMP() {
+		err := updateAMPInvoices(invoices, invoiceNum, htlcsAmpUpdate)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return invoice, nil
+}
+
+// serializeAndStoreInvoice is a helper function used to store invoices.
+func (d *DB) serializeAndStoreInvoice(invoices kvdb.RwBucket, invoiceNum []byte,
+	invoice *invpkg.Invoice) error {
+
+	var buf bytes.Buffer
+	if err := serializeInvoice(&buf, invoice); err != nil {
+		return err
+	}
+
+	if err := invoices.Put(invoiceNum, buf.Bytes()); err != nil {
+		return err
 	}
 
-	now := d.clock.Now()
+	return nil
+}
 
-	invoiceIsAMP := invoiceCopy.IsAMP()
+// addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc.
+func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen
+	setIDIndex kvdb.RwBucket, invoiceNum []byte, invoice *invpkg.Invoice,
+	hash *lntypes.Hash, update *invpkg.InvoiceUpdateDesc) (*invpkg.Invoice,
+	error) {
+
+	var setID *[32]byte
+	invoiceIsAMP := invoice.IsAMP()
+	if invoiceIsAMP && update.State != nil {
+		setID = update.State.SetID
+	}
+	timestamp := d.clock.Now()
 
 	// Process add actions from update descriptor.
 	htlcsAmpUpdate := make(map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) //nolint:lll
@@ -1928,21 +2029,26 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 			return nil, errors.New("nil custom records map")
 		}
 
-		// If a newly added HTLC has an associated set id, use it to
-		// index this invoice in the set id index. An error is returned
-		// if we find the index already points to a different invoice.
-		var setID [32]byte
-		if htlcUpdate.AMP != nil {
-			setID = htlcUpdate.AMP.Record.SetID()
-			setIDInvNum := setIDIndex.Get(setID[:])
+		if invoiceIsAMP {
+			if htlcUpdate.AMP == nil {
+				return nil, fmt.Errorf("unable to add htlc "+
+					"without AMP data to AMP invoice(%v)",
+					invoice.AddIndex)
+			}
+
+			// Check if this SetID already exist.
+			htlcSetID := htlcUpdate.AMP.Record.SetID()
+			setIDInvNum := setIDIndex.Get(htlcSetID[:])
+
 			if setIDInvNum == nil {
-				err = setIDIndex.Put(setID[:], invoiceNum)
+				err := setIDIndex.Put(htlcSetID[:], invoiceNum)
 				if err != nil {
 					return nil, err
 				}
 			} else if !bytes.Equal(setIDInvNum, invoiceNum) {
-				err = invpkg.ErrDuplicateSetID{SetID: setID}
-				return nil, err
+				return nil, invpkg.ErrDuplicateSetID{
+					SetID: htlcSetID,
+				}
 			}
 		}
 
@@ -1951,7 +2057,7 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 			MppTotalAmt:   htlcUpdate.MppTotalAmt,
 			Expiry:        htlcUpdate.Expiry,
 			AcceptHeight:  uint32(htlcUpdate.AcceptHeight),
-			AcceptTime:    now,
+			AcceptTime:    timestamp,
 			State:         invpkg.HtlcStateAccepted,
 			CustomRecords: htlcUpdate.CustomRecords,
 			AMP:           htlcUpdate.AMP.Copy(),
@@ -1963,53 +2069,12 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 		// below, but only if this is an AMP invoice.
 		if invoiceIsAMP {
 			updateHtlcsAmp(
-				&invoice, htlcsAmpUpdate, htlc, setID, key,
+				invoice, htlcsAmpUpdate, htlc,
+				htlcUpdate.AMP.Record.SetID(), key,
 			)
 		}
 	}
 
-	// Process cancel actions from update descriptor.
-	cancelHtlcs := update.CancelHtlcs
-	for key, htlc := range invoice.Htlcs {
-		htlc := htlc
-
-		// Check whether this htlc needs to be canceled. If it does,
-		// update the htlc state to Canceled.
-		_, cancel := cancelHtlcs[key]
-		if !cancel {
-			continue
-		}
-
-		// Consistency check to verify that there is no overlap between
-		// the add and cancel sets.
-		if _, added := update.AddHtlcs[key]; added {
-			return nil, fmt.Errorf("added htlc %v canceled", key)
-		}
-
-		err := cancelSingleHtlc(now, htlc, newState)
-		if err != nil {
-			return nil, err
-		}
-
-		// Delete processed cancel action, so that we can check later
-		// that there are no actions left.
-		delete(cancelHtlcs, key)
-
-		// Tally this into the set of HTLCs that need to be updated on
-		// disk, but once again, only if this is an AMP invoice.
-		if invoiceIsAMP {
-			cancelHtlcsAmp(
-				&invoice, htlcsAmpUpdate, htlc, key,
-			)
-		}
-	}
-
-	// Verify that we didn't get an action for htlcs that are not present on
-	// the invoice.
-	if len(cancelHtlcs) > 0 {
-		return nil, errors.New("cancel action on non-existent htlc(s)")
-	}
-
 	// At this point, the set of accepted HTLCs should be fully
 	// populated with added HTLCs or removed of canceled ones. Update
 	// invoice state if the update descriptor indicates an invoice state
@@ -2017,7 +2082,7 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 	// HTLCs.
 	if update.State != nil {
 		newState, err := updateInvoiceState(
-			&invoice, hash, *update.State,
+			invoice, hash, *update.State,
 		)
 		if err != nil {
 			return nil, err
@@ -2039,7 +2104,8 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 		isSettled := update.State.NewState == invpkg.ContractSettled
 		if !invoiceIsAMP && isSettled {
 			err := setSettleMetaFields(
-				settleIndex, invoiceNum, &invoice, now, nil,
+				settleIndex, invoiceNum, invoice, timestamp,
+				nil,
 			)
 			if err != nil {
 				return nil, err
@@ -2093,7 +2159,7 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 			htlcContextState = invpkg.ContractSettled
 		}
 		htlcSettled, err := updateHtlc(
-			now, htlc, htlcContextState, setID,
+			timestamp, htlc, htlcContextState, setID,
 		)
 		if err != nil {
 			return nil, err
@@ -2104,7 +2170,7 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 		// meta data state.
 		if htlcSettled && invoiceIsAMP {
 			settleHtlcsAmp(
-				&invoice, settledSetIDs, htlcsAmpUpdate, htlc,
+				invoice, settledSetIDs, htlcsAmpUpdate, htlc,
 				key,
 			)
 		}
@@ -2156,20 +2222,16 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 	for settledSetID := range settledSetIDs {
 		settledSetID := settledSetID
 		err := setSettleMetaFields(
-			settleIndex, invoiceNum, &invoice, now, &settledSetID,
+			settleIndex, invoiceNum, invoice, timestamp,
+			&settledSetID,
 		)
 		if err != nil {
 			return nil, err
 		}
 	}
 
-	// Reserialize and update invoice.
-	var buf bytes.Buffer
-	if err := serializeInvoice(&buf, &invoice); err != nil {
-		return nil, err
-	}
-
-	if err := invoices.Put(invoiceNum[:], buf.Bytes()); err != nil {
+	err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice)
+	if err != nil {
 		return nil, err
 	}
 
@@ -2183,7 +2245,137 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices,
 		}
 	}
 
-	return &invoice, nil
+	return invoice, nil
+}
+
+// settleHodlInvoice marks a hodl invoice as settled.
+//
+// NOTE: Currently it is not possible to have HODL AMP invoices.
+func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket,
+	invoiceNum []byte, invoice *invpkg.Invoice, hash *lntypes.Hash,
+	update *invpkg.InvoiceStateUpdateDesc) (*invpkg.Invoice, error) {
+
+	if !invoice.HodlInvoice {
+		return nil, fmt.Errorf("unable to settle hodl invoice: %v is "+
+			"not a hodl invoice", invoice.AddIndex)
+	}
+
+	// TODO(positiveblue): because NewState can only be ContractSettled we
+	// can remove it from the API and set it here directly.
+	switch {
+	case update == nil:
+		fallthrough
+
+	case update.NewState != invpkg.ContractSettled:
+		return nil, fmt.Errorf("unable to settle hodl invoice: "+
+			"not valid InvoiceUpdateDesc.State: %v", update)
+
+	case update.Preimage == nil:
+		return nil, fmt.Errorf("unable to settle hodl invoice: " +
+			"preimage is nil")
+	}
+
+	// TODO(positiveblue): create a invoice.CanSettleHodlInvoice func.
+	newState, err := updateInvoiceState(invoice, hash, *update)
+	if err != nil {
+		return nil, err
+	}
+
+	if newState == nil || *newState != invpkg.ContractSettled {
+		return nil, fmt.Errorf("unable to settle hodl invoice: "+
+			"new computed state is not settled: %s", newState)
+	}
+
+	invoice.State = invpkg.ContractSettled
+	timestamp := d.clock.Now()
+
+	err = setSettleMetaFields(
+		settleIndex, invoiceNum, invoice, timestamp, nil,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO(positiveblue): this logic can be further simplified.
+	var amtPaid lnwire.MilliSatoshi
+	for _, htlc := range invoice.Htlcs {
+		_, err := updateHtlc(
+			timestamp, htlc, invpkg.ContractSettled, nil,
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		if htlc.State == invpkg.HtlcStateSettled {
+			amtPaid += htlc.Amt
+		}
+	}
+
+	invoice.AmtPaid = amtPaid
+
+	err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice)
+	if err != nil {
+		return nil, err
+	}
+
+	return invoice, nil
+}
+
+// cancelInvoice attempts to cancel the given invoice. That includes changing
+// the invoice state and the state of any relevant HTLC.
+func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte,
+	invoice *invpkg.Invoice, hash *lntypes.Hash,
+	update *invpkg.InvoiceStateUpdateDesc) (*invpkg.Invoice, error) {
+
+	switch {
+	case update == nil:
+		fallthrough
+
+	case update.NewState != invpkg.ContractCanceled:
+		return nil, fmt.Errorf("unable to cancel invoice: "+
+			"InvoiceUpdateDesc.State not valid: %v", update)
+	}
+
+	var (
+		setID        *[32]byte
+		invoiceIsAMP bool
+	)
+
+	invoiceIsAMP = invoice.IsAMP()
+	if invoiceIsAMP {
+		setID = update.SetID
+	}
+
+	newState, err := updateInvoiceState(invoice, hash, *update)
+	if err != nil {
+		return nil, err
+	}
+
+	if newState == nil || *newState != invpkg.ContractCanceled {
+		return nil, fmt.Errorf("unable to cancel invoice(%v): new "+
+			"computed state is not canceled: %s", invoice.AddIndex,
+			newState)
+	}
+
+	invoice.State = invpkg.ContractCanceled
+	timestamp := d.clock.Now()
+
+	// TODO(positiveblue): this logic can be simplified.
+	for _, htlc := range invoice.Htlcs {
+		_, err := updateHtlc(
+			timestamp, htlc, invpkg.ContractCanceled, setID,
+		)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice)
+	if err != nil {
+		return nil, err
+	}
+
+	return invoice, nil
 }
 
 // updateInvoiceState validates and processes an invoice state update. The new
diff --git a/cmd/lncli/cmd_mission_control.go b/cmd/lncli/cmd_mission_control.go
index 9749bcf2f..44e87c377 100644
--- a/cmd/lncli/cmd_mission_control.go
+++ b/cmd/lncli/cmd_mission_control.go
@@ -65,8 +65,7 @@ var setCfgCommand = cli.Command{
 		cli.StringFlag{
 			Name: "estimator",
 			Usage: "the probability estimator to use, choose " +
-				"between 'apriori' or 'bimodal' (bimodal is " +
-				"experimental)",
+				"between 'apriori' or 'bimodal'",
 		},
 		// Apriori config.
 		cli.DurationFlag{
diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md
index 3c97d9c0d..4b033687c 100644
--- a/docs/release-notes/release-notes-0.16.0.md
+++ b/docs/release-notes/release-notes-0.16.0.md
@@ -459,10 +459,6 @@ in the lnwire package](https://github.com/lightningnetwork/lnd/pull/7303)
   https://github.com/lightningnetwork/lnd/pull/6815)
 * [The a priori capacity factor is made configurable and its effect is 
   limited.](https://github.com/lightningnetwork/lnd/pull/7444)
-* Local edges and hop hints [are extended with a
-  capacity](https://github.com/lightningnetwork/lnd/pull/7520) to avoid channel
-  white listing in probability calculations. The influence of the node
-  probability is reduced.
 
 ## Configuration
 * Note that [this pathfinding change](https://github.com/lightningnetwork/lnd/pull/6815)
diff --git a/docs/release-notes/release-notes-0.17.0.md b/docs/release-notes/release-notes-0.17.0.md
new file mode 100644
index 000000000..e32b93157
--- /dev/null
+++ b/docs/release-notes/release-notes-0.17.0.md
@@ -0,0 +1,12 @@
+# Release Notes
+
+## DB
+
+* Split channeldb [`UpdateInvoice`
+  implementation](https://github.com/lightningnetwork/lnd/pull/7377) logic in 
+  different update types.
+
+# Contributors (Alphabetical Order)
+
+* Jordi Montes
+
diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go
index a80a85c8d..a638f46b1 100644
--- a/invoices/invoiceregistry.go
+++ b/invoices/invoiceregistry.go
@@ -711,6 +711,7 @@ func (i *InvoiceRegistry) cancelSingleHtlc(invoiceRef InvoiceRef,
 		}
 
 		return &InvoiceUpdateDesc{
+			UpdateType:  CancelHTLCsUpdate,
 			CancelHtlcs: canceledHtlcs,
 			SetID:       setID,
 		}, nil
@@ -1230,9 +1231,7 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
 	i.Lock()
 	defer i.Unlock()
 
-	updateInvoice := func(invoice *Invoice) (
-		*InvoiceUpdateDesc, error) {
-
+	updateInvoice := func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
 		switch invoice.State {
 		case ContractOpen:
 			return nil, ErrInvoiceStillOpen
@@ -1245,6 +1244,7 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
 		}
 
 		return &InvoiceUpdateDesc{
+			UpdateType: SettleHodlInvoiceUpdate,
 			State: &InvoiceStateUpdateDesc{
 				NewState: ContractSettled,
 				Preimage: &preimage,
@@ -1329,6 +1329,7 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(payHash lntypes.Hash,
 		// channeldb to return an error if the invoice is already
 		// settled or canceled.
 		return &InvoiceUpdateDesc{
+			UpdateType: CancelInvoiceUpdate,
 			State: &InvoiceStateUpdateDesc{
 				NewState: ContractCanceled,
 			},
diff --git a/invoices/invoices.go b/invoices/invoices.go
index 7a99a46c6..24447223d 100644
--- a/invoices/invoices.go
+++ b/invoices/invoices.go
@@ -656,6 +656,51 @@ type HtlcAcceptDesc struct {
 	AMP *InvoiceHtlcAMPData
 }
 
+// UpdateType is an enum that describes the type of update that was applied to
+// an invoice.
+type UpdateType uint8
+
+const (
+	// UnknownUpdate indicates that the UpdateType has not been set for a
+	// given udpate. This kind of updates are not allowed.
+	UnknownUpdate UpdateType = iota
+
+	// CancelHTLCsUpdate indicates that this update cancels one or more
+	// HTLCs.
+	CancelHTLCsUpdate
+
+	// AddHTLCsUpdate indicates that this update adds one or more HTLCs.
+	AddHTLCsUpdate
+
+	// SettleHodlInvoiceUpdate indicates that this update settles one or
+	// more HTLCs from a hodl invoice.
+	SettleHodlInvoiceUpdate
+
+	// CancelInvoiceUpdate indicates that this update is trying to cancel
+	// an invoice.
+	CancelInvoiceUpdate
+)
+
+// String returns a human readable string for the UpdateType.
+func (u UpdateType) String() string {
+	switch u {
+	case CancelHTLCsUpdate:
+		return "CancelHTLCsUpdate"
+
+	case AddHTLCsUpdate:
+		return "AddHTLCsUpdate"
+
+	case SettleHodlInvoiceUpdate:
+		return "SettleHodlInvoiceUpdate"
+
+	case CancelInvoiceUpdate:
+		return "CancelInvoiceUpdate"
+
+	default:
+		return fmt.Sprintf("unknown invoice update type: %d", u)
+	}
+}
+
 // InvoiceUpdateDesc describes the changes that should be applied to the
 // invoice.
 type InvoiceUpdateDesc struct {
@@ -674,6 +719,9 @@ type InvoiceUpdateDesc struct {
 	// to be more efficient by ensuring we don't need to read out the
 	// entire HTLC set each timee an HTLC is to be cancelled.
 	SetID *SetID
+
+	// UpdateType indicates what type of update is being applied.
+	UpdateType UpdateType
 }
 
 // InvoiceStateUpdateDesc describes an invoice-level state transition.
diff --git a/invoices/update.go b/invoices/update.go
index e9e7be6ed..390f325a7 100644
--- a/invoices/update.go
+++ b/invoices/update.go
@@ -238,7 +238,8 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
 	}
 
 	update := InvoiceUpdateDesc{
-		AddHtlcs: newHtlcs,
+		UpdateType: AddHTLCsUpdate,
+		AddHtlcs:   newHtlcs,
 	}
 
 	// If the invoice cannot be settled yet, only record the htlc.
@@ -252,7 +253,6 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
 	if inv.HodlInvoice {
 		update.State = &InvoiceStateUpdateDesc{
 			NewState: ContractAccepted,
-			SetID:    setID,
 		}
 		return &update, ctx.acceptRes(resultAccepted), nil
 	}
@@ -265,6 +265,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
 		var failRes *HtlcFailResolution
 		htlcPreimages, failRes = reconstructAMPPreimages(ctx, htlcSet)
 		if failRes != nil {
+			update.UpdateType = CancelInvoiceUpdate
 			update.State = &InvoiceStateUpdateDesc{
 				NewState: ContractCanceled,
 				SetID:    setID,
@@ -427,7 +428,8 @@ func updateLegacy(ctx *invoiceUpdateCtx,
 	}
 
 	update := InvoiceUpdateDesc{
-		AddHtlcs: newHtlcs,
+		AddHtlcs:   newHtlcs,
+		UpdateType: AddHTLCsUpdate,
 	}
 
 	// Don't update invoice state if we are accepting a duplicate payment.
diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go
index 514f9f8e3..3bfabfa6c 100644
--- a/lnrpc/routerrpc/router.pb.go
+++ b/lnrpc/routerrpc/router.pb.go
@@ -1667,8 +1667,8 @@ type MissionControlConfig struct {
 	// before we raise the failure amount.
 	MinimumFailureRelaxInterval uint64 `protobuf:"varint,5,opt,name=minimum_failure_relax_interval,json=minimumFailureRelaxInterval,proto3" json:"minimum_failure_relax_interval,omitempty"`
 	// ProbabilityModel defines which probability estimator should be used in
-	// pathfinding. Note that the bimodal estimator is experimental.
-	Model MissionControlConfig_ProbabilityModel `protobuf:"varint,6,opt,name=model,proto3,enum=routerrpc.MissionControlConfig_ProbabilityModel" json:"model,omitempty"`
+	// pathfinding.
+	Model MissionControlConfig_ProbabilityModel `protobuf:"varint,6,opt,name=Model,proto3,enum=routerrpc.MissionControlConfig_ProbabilityModel" json:"Model,omitempty"`
 	// EstimatorConfig is populated dependent on the estimator type.
 	//
 	// Types that are assignable to EstimatorConfig:
@@ -3468,11 +3468,11 @@ var file_routerrpc_router_proto_rawDesc = []byte{
 	0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x78, 0x5f, 0x69, 0x6e, 0x74, 0x65,
 	0x72, 0x76, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1b, 0x6d, 0x69, 0x6e, 0x69,
 	0x6d, 0x75, 0x6d, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x6c, 0x61, 0x78, 0x49,
-	0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x46, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c,
+	0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x46, 0x0a, 0x05, 0x4d, 0x6f, 0x64, 0x65, 0x6c,
 	0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72,
 	0x70, 0x63, 0x2e, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
 	0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c,
-	0x69, 0x74, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12,
+	0x69, 0x74, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x05, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12,
 	0x38, 0x0a, 0x07, 0x61, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b,
 	0x32, 0x1c, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x70, 0x72,
 	0x69, 0x6f, 0x72, 0x69, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x48, 0x00,
@@ -3943,7 +3943,7 @@ var file_routerrpc_router_proto_depIdxs = []int32{
 	20, // 7: routerrpc.PairHistory.history:type_name -> routerrpc.PairData
 	25, // 8: routerrpc.GetMissionControlConfigResponse.config:type_name -> routerrpc.MissionControlConfig
 	25, // 9: routerrpc.SetMissionControlConfigRequest.config:type_name -> routerrpc.MissionControlConfig
-	4,  // 10: routerrpc.MissionControlConfig.model:type_name -> routerrpc.MissionControlConfig.ProbabilityModel
+	4,  // 10: routerrpc.MissionControlConfig.Model:type_name -> routerrpc.MissionControlConfig.ProbabilityModel
 	27, // 11: routerrpc.MissionControlConfig.apriori:type_name -> routerrpc.AprioriParameters
 	26, // 12: routerrpc.MissionControlConfig.bimodal:type_name -> routerrpc.BimodalParameters
 	20, // 13: routerrpc.QueryProbabilityResponse.history:type_name -> routerrpc.PairData
diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto
index d591cc485..8f30d8edd 100644
--- a/lnrpc/routerrpc/router.proto
+++ b/lnrpc/routerrpc/router.proto
@@ -513,9 +513,9 @@ message MissionControlConfig {
 
     /*
     ProbabilityModel defines which probability estimator should be used in
-    pathfinding. Note that the bimodal estimator is experimental.
+    pathfinding.
     */
-    ProbabilityModel model = 6;
+    ProbabilityModel Model = 6;
 
     /*
     EstimatorConfig is populated dependent on the estimator type.
diff --git a/lnrpc/routerrpc/router.swagger.json b/lnrpc/routerrpc/router.swagger.json
index 5858ff24f..fc4de7678 100644
--- a/lnrpc/routerrpc/router.swagger.json
+++ b/lnrpc/routerrpc/router.swagger.json
@@ -1475,9 +1475,9 @@
           "format": "uint64",
           "description": "The minimum time that must have passed since the previously recorded failure\nbefore we raise the failure amount."
         },
-        "model": {
+        "Model": {
           "$ref": "#/definitions/MissionControlConfigProbabilityModel",
-          "description": "ProbabilityModel defines which probability estimator should be used in\npathfinding. Note that the bimodal estimator is experimental."
+          "description": "ProbabilityModel defines which probability estimator should be used in\npathfinding."
         },
         "apriori": {
           "$ref": "#/definitions/routerrpcAprioriParameters"
diff --git a/routing/pathfind.go b/routing/pathfind.go
index e293d320d..3c5397224 100644
--- a/routing/pathfind.go
+++ b/routing/pathfind.go
@@ -37,11 +37,6 @@ const (
 	// to avoid resizing and copies. It should be number on the same order as
 	// the number of active nodes in the network.
 	estimatedNodeCount = 10000
-
-	// fakeHopHintCapacity is the capacity we assume for hop hint channels.
-	// This is a high number, which expresses that a hop hint channel should
-	// be able to route payments.
-	fakeHopHintCapacity = btcutil.Amount(10 * btcutil.SatoshiPerBitcoin)
 )
 
 // pathFinder defines the interface of a path finding algorithm.
@@ -868,17 +863,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 			return nil, 0, err
 		}
 
-		// We add hop hints that were supplied externally.
 		for _, reverseEdge := range additionalEdgesWithSrc[pivot] {
-			// Hop hints don't contain a capacity. We set one here,
-			// since a capacity is needed for probability
-			// calculations. We set a high capacity to act as if
-			// there is enough liquidity, otherwise the hint would
-			// not have been added by a wallet.
-			u.addPolicy(
-				reverseEdge.sourceNode, reverseEdge.edge,
-				fakeHopHintCapacity,
-			)
+			u.addPolicy(reverseEdge.sourceNode, reverseEdge.edge, 0)
 		}
 
 		amtToSend := partialPath.amountToReceive
diff --git a/routing/probability_bimodal.go b/routing/probability_bimodal.go
index d23b96666..daa086fa9 100644
--- a/routing/probability_bimodal.go
+++ b/routing/probability_bimodal.go
@@ -28,12 +28,9 @@ const (
 	// the timescale of about a week.
 	DefaultBimodalDecayTime = 7 * 24 * time.Hour
 
-	// BimodalScaleMsatMax is the maximum value for BimodalScaleMsat. We
-	// limit it here to the fakeHopHintCapacity to avoid issues with hop
-	// hint probability calculations.
-	BimodalScaleMsatMax = lnwire.MilliSatoshi(
-		1000 * fakeHopHintCapacity / 4,
-	)
+	// BimodalScaleMsatLimit is the maximum value for BimodalScaleMsat to
+	// avoid numerical issues.
+	BimodalScaleMsatMax = lnwire.MilliSatoshi(21e17)
 
 	// BimodalEstimatorName is used to identify the bimodal estimator.
 	BimodalEstimatorName = "bimodal"
@@ -49,10 +46,6 @@ var (
 
 	// ErrInvalidDecayTime is returned when we get a decay time below zero.
 	ErrInvalidDecayTime = errors.New("decay time must be larger than zero")
-
-	// ErrZeroCapacity is returned when we encounter a channel with zero
-	// capacity in probability estimation.
-	ErrZeroCapacity = errors.New("capacity must be larger than zero")
 )
 
 // BimodalConfig contains configuration for our probability estimator.
@@ -230,9 +223,7 @@ func (p *BimodalEstimator) directProbability(now time.Time,
 		capacity, successAmount, failAmount, amt,
 	)
 	if err != nil {
-		log.Errorf("error computing probability to node: %v "+
-			"(node: %v, results: %v, amt: %v, capacity: %v)",
-			err, toNode, results, amt, capacity)
+		log.Errorf("error computing probability: %v", err)
 
 		return 0.0
 	}
@@ -308,32 +299,6 @@ func (p *BimodalEstimator) calculateProbability(directProbability float64,
 		return directProbability
 	}
 
-	// If we have up-to-date information about the channel we want to use,
-	// i.e. the info stems from results not longer ago than the decay time,
-	// we will only use the direct probability. This is needed in order to
-	// avoid that other previous results (on all other channels of the same
-	// routing node) will distort and pin the calculated probability even if
-	// we have accurate direct information. This helps to dip the
-	// probability below the min probability in case of failures, to start
-	// the splitting process.
-	directResult, ok := results[toNode]
-	if ok {
-		latest := directResult.SuccessTime
-		if directResult.FailTime.After(latest) {
-			latest = directResult.FailTime
-		}
-
-		// We use BimonodalDecayTime to judge the currentness of the
-		// data. It is the time scale on which we assume to have lost
-		// information.
-		if now.Sub(latest) < p.BimodalDecayTime {
-			log.Tracef("Using direct probability for node %v: %v",
-				toNode, directResult)
-
-			return directProbability
-		}
-	}
-
 	// w is a parameter which determines how strongly the other channels of
 	// a node should be incorporated, the higher the stronger.
 	w := p.BimodalNodeWeight
@@ -473,10 +438,10 @@ func (p *BimodalEstimator) probabilityFormula(capacityMsat, successAmountMsat,
 	failAmount := float64(failAmountMsat)
 	amount := float64(amountMsat)
 
-	// In order for this formula to give reasonable results, we need to have
-	// an estimate of the capacity of a channel (or edge between nodes).
+	// Capacity being zero is a sentinel value to ignore the probability
+	// estimation, we'll return the full probability here.
 	if capacity == 0.0 {
-		return 0, ErrZeroCapacity
+		return 1.0, nil
 	}
 
 	// We cannot send more than the capacity.
@@ -490,17 +455,11 @@ func (p *BimodalEstimator) probabilityFormula(capacityMsat, successAmountMsat,
 
 	// failAmount should be capacity at max.
 	if failAmount > capacity {
-		log.Debugf("Correcting failAmount %v to capacity %v",
-			failAmount, capacity)
-
 		failAmount = capacity
 	}
 
 	// successAmount should be capacity at max.
 	if successAmount > capacity {
-		log.Debugf("Correcting successAmount %v to capacity %v",
-			successAmount, capacity)
-
 		successAmount = capacity
 	}
 
@@ -509,7 +468,7 @@ func (p *BimodalEstimator) probabilityFormula(capacityMsat, successAmountMsat,
 	// happen if a large channel gets closed and smaller ones remain, but
 	// it should recover with the time decay.
 	if failAmount <= successAmount {
-		log.Tracef("fail amount (%v) is smaller than or equal the "+
+		log.Tracef("fail amount (%v) is larger than or equal the "+
 			"success amount (%v) for capacity (%v)",
 			failAmountMsat, successAmountMsat, capacityMsat)
 
diff --git a/routing/router.go b/routing/router.go
index 39b47e792..eea5189d9 100644
--- a/routing/router.go
+++ b/routing/router.go
@@ -1689,7 +1689,7 @@ func (r *ChannelRouter) processUpdate(msg interface{},
 			return err
 		}
 
-		log.Tracef("New channel update applied: %v",
+		log.Debugf("New channel update applied: %v",
 			newLogClosure(func() string { return spew.Sdump(msg) }))
 		r.stats.incNumChannelUpdates()
 
diff --git a/routing/unified_edges.go b/routing/unified_edges.go
index e8c2a9270..89aaf94ac 100644
--- a/routing/unified_edges.go
+++ b/routing/unified_edges.go
@@ -218,10 +218,7 @@ func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
 		maxBandwidth = bandwidth
 
 		// Update best edge.
-		bestEdge = &unifiedEdge{
-			policy:   edge.policy,
-			capacity: edge.capacity,
-		}
+		bestEdge = &unifiedEdge{policy: edge.policy}
 	}
 
 	return bestEdge
diff --git a/sample-lnd.conf b/sample-lnd.conf
index 23a1606da..3dfc76da8 100644
--- a/sample-lnd.conf
+++ b/sample-lnd.conf
@@ -1106,7 +1106,6 @@ litecoin.node=ltcd
 [routerrpc]
 
 ; Probability estimator used for pathfinding. (default: apriori)
-; Note that the bimodal estimator is experimental.
 ; routerrpc.estimator=[apriori|bimodal]
 
 ; Minimum required route success probability to attempt the payment (default:
@@ -1157,7 +1156,7 @@ litecoin.node=ltcd
 ; Defines how strongly non-routed channels of forwarders should be taken into
 ; account for probability estimation. A weight of zero disables this feature.
 ; Valid values are in [0, 1]. (default: 0.2)
-; routerrpc.bimodal.nodeweight=0.1
+; routerrpc.bimodal.nodeweight=0.3
 
 ; Defines the information decay of knowledge about previous successes and
 ; failures in channels. (default: 168h0m0s)


Test patch:
diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go
index 38674b6be..ce2d960b6 100644
--- a/channeldb/invoice_test.go
+++ b/channeldb/invoice_test.go
@@ -461,7 +461,8 @@ func TestInvoiceCancelSingleHtlc(t *testing.T) {
 		}
 
 		return &invpkg.InvoiceUpdateDesc{
-			AddHtlcs: htlcs,
+			UpdateType: invpkg.AddHTLCsUpdate,
+			AddHtlcs:   htlcs,
 		}, nil
 	}
 
@@ -479,6 +480,7 @@ func TestInvoiceCancelSingleHtlc(t *testing.T) {
 		invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) {
 
 		return &invpkg.InvoiceUpdateDesc{
+			UpdateType: invpkg.CancelHTLCsUpdate,
 			CancelHtlcs: map[models.CircuitKey]struct{}{
 				key: {},
 			},
@@ -554,6 +556,7 @@ func TestInvoiceCancelSingleHtlcAMP(t *testing.T) {
 		invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) {
 
 		return &invpkg.InvoiceUpdateDesc{
+			UpdateType: invpkg.CancelHTLCsUpdate,
 			CancelHtlcs: map[models.CircuitKey]struct{}{
 				{HtlcID: 0}: {},
 			},
@@ -616,6 +619,7 @@ func TestInvoiceCancelSingleHtlcAMP(t *testing.T) {
 			error) {
 
 			return &invpkg.InvoiceUpdateDesc{
+				UpdateType: invpkg.CancelHTLCsUpdate,
 				CancelHtlcs: map[models.CircuitKey]struct{}{
 					{HtlcID: 1}: {},
 				},
@@ -643,6 +647,7 @@ func TestInvoiceCancelSingleHtlcAMP(t *testing.T) {
 		invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) {
 
 		return &invpkg.InvoiceUpdateDesc{
+			UpdateType: invpkg.CancelHTLCsUpdate,
 			CancelHtlcs: map[models.CircuitKey]struct{}{
 				{HtlcID: 2}: {},
 			},
@@ -1529,6 +1534,7 @@ func getUpdateInvoice(amt lnwire.MilliSatoshi) invpkg.InvoiceUpdateCallback {
 			},
 		}
 		update := &invpkg.InvoiceUpdateDesc{
+			UpdateType: invpkg.AddHTLCsUpdate,
 			State: &invpkg.InvoiceStateUpdateDesc{
 				Preimage: invoice.Terms.PaymentPreimage,
 				NewState: invpkg.ContractSettled,
@@ -1586,7 +1592,10 @@ func TestCustomRecords(t *testing.T) {
 			},
 		}
 
-		return &invpkg.InvoiceUpdateDesc{AddHtlcs: htlcs}, nil
+		return &invpkg.InvoiceUpdateDesc{
+			AddHtlcs:   htlcs,
+			UpdateType: invpkg.AddHTLCsUpdate,
+		}, nil
 	}
 
 	_, err = db.UpdateInvoice(ref, nil, callback)
@@ -1663,7 +1672,10 @@ func testInvoiceHtlcAMPFields(t *testing.T, isAMP bool) {
 			},
 		}
 
-		return &invpkg.InvoiceUpdateDesc{AddHtlcs: htlcs}, nil
+		return &invpkg.InvoiceUpdateDesc{
+			AddHtlcs:   htlcs,
+			UpdateType: invpkg.AddHTLCsUpdate,
+		}, nil
 	}
 
 	ref := invpkg.InvoiceRefByHash(payHash)
@@ -2128,8 +2140,9 @@ func updateAcceptAMPHtlc(id uint64, amt lnwire.MilliSatoshi,
 		}
 
 		update := &invpkg.InvoiceUpdateDesc{
-			State:    state,
-			AddHtlcs: htlcs,
+			State:      state,
+			AddHtlcs:   htlcs,
+			UpdateType: invpkg.AddHTLCsUpdate,
 		}
 
 		return update, nil
@@ -2152,6 +2165,10 @@ func getUpdateInvoiceAMPSettle(setID *[32]byte, preimage [32]byte,
 		}
 
 		update := &invpkg.InvoiceUpdateDesc{
+			// TODO(positiveblue): this would be an invalid update
+			// because tires to settle an AMP invoice without adding
+			// any new htlc.
+			UpdateType: invpkg.AddHTLCsUpdate,
 			State: &invpkg.InvoiceStateUpdateDesc{
 				Preimage:      nil,
 				NewState:      invpkg.ContractSettled,
@@ -2272,12 +2289,16 @@ func testUpdateHTLCPreimages(t *testing.T, test updateHTLCPreimageTestCase) {
 		invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) {
 
 		update := &invpkg.InvoiceUpdateDesc{
+			// TODO(positiveblue): this would be an invalid update
+			// because tires to settle an AMP invoice without adding
+			// any new htlc.
 			State: &invpkg.InvoiceStateUpdateDesc{
 				Preimage:      nil,
 				NewState:      invpkg.ContractSettled,
 				HTLCPreimages: htlcPreimages,
 				SetID:         setID,
 			},
+			UpdateType: invpkg.AddHTLCsUpdate,
 		}
 
 		return update, nil
diff --git a/routing/probability_bimodal_test.go b/routing/probability_bimodal_test.go
index 676fe348f..53f8829c4 100644
--- a/routing/probability_bimodal_test.go
+++ b/routing/probability_bimodal_test.go
@@ -232,16 +232,6 @@ func TestSuccessProbability(t *testing.T) {
 			require.NoError(t, err)
 		})
 	}
-
-	// We expect an error when the capacity is zero.
-	t.Run("zero capacity", func(t *testing.T) {
-		t.Parallel()
-
-		_, err := estimator.probabilityFormula(
-			0, 0, 0, 0,
-		)
-		require.ErrorIs(t, err, ErrZeroCapacity)
-	})
 }
 
 // TestIntegral tests certain limits of the probability distribution integral.
@@ -379,44 +369,35 @@ func TestComputeProbability(t *testing.T) {
 	nodeWeight := 1 / 5.
 	toNode := route.Vertex{10}
 	tolerance := 0.01
-	now := time.Unix(0, 0)
 	decayTime := time.Duration(1) * time.Hour * 24
 
 	// makeNodeResults prepares forwarding data for the other channels of
 	// the node.
-	makeNodeResults := func(successes []bool, resultsTime time.Time,
-		directResultTime time.Time) NodeResults {
-
+	makeNodeResults := func(successes []bool, now time.Time) NodeResults {
 		results := make(NodeResults, len(successes))
 
 		for i, s := range successes {
 			vertex := route.Vertex{byte(i)}
 
 			results[vertex] = TimedPairResult{
-				FailTime: resultsTime, FailAmt: 1,
+				FailTime: now, FailAmt: 1,
 			}
 			if s {
 				results[vertex] = TimedPairResult{
-					SuccessTime: resultsTime, SuccessAmt: 1,
+					SuccessTime: now, SuccessAmt: 1,
 				}
 			}
 		}
 
-		// Include a direct result.
-		results[toNode] = TimedPairResult{
-			SuccessTime: directResultTime, SuccessAmt: 1,
-		}
-
 		return results
 	}
 
 	tests := []struct {
 		name                string
 		directProbability   float64
-		recentDirectResult  bool
 		otherResults        []bool
 		expectedProbability float64
-		resultsTimeAgo      time.Duration
+		delay               time.Duration
 	}{
 		// If no other information is available, use the direct
 		// probability.
@@ -535,7 +516,7 @@ func TestComputeProbability(t *testing.T) {
 				"time",
 			directProbability:   1.0,
 			otherResults:        []bool{true},
-			resultsTimeAgo:      decayTime,
+			delay:               decayTime,
 			expectedProbability: 1.00,
 		},
 		// A failure that was experienced some time ago won't influence
@@ -544,7 +525,7 @@ func TestComputeProbability(t *testing.T) {
 			name:                "success, single fail, decay time",
 			directProbability:   1.0,
 			otherResults:        []bool{false},
-			resultsTimeAgo:      decayTime,
+			delay:               decayTime,
 			expectedProbability: 0.9314,
 		},
 		// Information from a long time ago doesn't have any effect.
@@ -552,7 +533,7 @@ func TestComputeProbability(t *testing.T) {
 			name:                "success, single fail, long ago",
 			directProbability:   1.0,
 			otherResults:        []bool{false},
-			resultsTimeAgo:      10 * decayTime,
+			delay:               10 * decayTime,
 			expectedProbability: 1.0,
 		},
 		{
@@ -561,7 +542,7 @@ func TestComputeProbability(t *testing.T) {
 			otherResults: []bool{
 				true, true, true, true, true,
 			},
-			resultsTimeAgo:      decayTime,
+			delay:               decayTime,
 			expectedProbability: 0.269,
 		},
 		// Very recent info approaches the case with no time decay.
@@ -571,22 +552,9 @@ func TestComputeProbability(t *testing.T) {
 			otherResults: []bool{
 				true, true, true, true, true,
 			},
-			resultsTimeAgo:      decayTime / 10,
+			delay:               decayTime / 10,
 			expectedProbability: 0.741,
 		},
-		// If we have recent info on the direct probability, we don't
-		// include node-wide info. Here we check that a recent failure
-		// is not pinned to a high probability by many successes on the
-		// node.
-		{
-			name:               "recent direct result",
-			directProbability:  0.1,
-			recentDirectResult: true,
-			otherResults: []bool{
-				true, true, true, true, true,
-			},
-			expectedProbability: 0.1,
-		},
 	}
 
 	estimator := BimodalEstimator{
@@ -602,19 +570,9 @@ func TestComputeProbability(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			t.Parallel()
 
-			var directResultTime time.Time
-			if test.recentDirectResult {
-				directResultTime = now.Add(-decayTime / 2)
-			} else {
-				directResultTime = now.Add(-2 * decayTime)
-			}
-
-			resultsTime := now.Add(-test.resultsTimeAgo)
-
-			results := makeNodeResults(
-				test.otherResults, resultsTime,
-				directResultTime,
-			)
+			then := time.Unix(0, 0)
+			results := makeNodeResults(test.otherResults, then)
+			now := then.Add(test.delay)
 
 			p := estimator.calculateProbability(
 				test.directProbability, now, results, toNode,
@@ -691,9 +649,7 @@ func FuzzProbability(f *testing.F) {
 	estimator := BimodalEstimator{
 		BimodalConfig: BimodalConfig{BimodalScaleMsat: scale},
 	}
-
-	// We don't start fuzzing at zero, because that would cause an error.
-	f.Add(uint64(1), uint64(0), uint64(0), uint64(0))
+	f.Add(uint64(0), uint64(0), uint64(0), uint64(0))
 
 	f.Fuzz(func(t *testing.T, capacity, successAmt, failAmt, amt uint64) {
 		_, err := estimator.probabilityFormula(