package channeldb import "fmt" // PaymentStatus represent current status of payment. type PaymentStatus byte const ( // NOTE: PaymentStatus = 0 was previously used for status unknown and // is now deprecated. // StatusInitiated is the status where a payment has just been // initiated. StatusInitiated PaymentStatus = 1 // StatusInFlight is the status where a payment has been initiated, but // a response has not been received. StatusInFlight PaymentStatus = 2 // StatusSucceeded is the status where a payment has been initiated and // the payment was completed successfully. StatusSucceeded PaymentStatus = 3 // StatusFailed is the status where a payment has been initiated and a // failure result has come back. StatusFailed PaymentStatus = 4 ) // errPaymentStatusUnknown is returned when a payment has an unknown status. var errPaymentStatusUnknown = fmt.Errorf("unknown payment status") // String returns readable representation of payment status. func (ps PaymentStatus) String() string { switch ps { case StatusInitiated: return "Initiated" case StatusInFlight: return "In Flight" case StatusSucceeded: return "Succeeded" case StatusFailed: return "Failed" default: return "Unknown" } } // initializable returns an error to specify whether initiating the payment // with its current status is allowed. A payment can only be initialized if it // hasn't been created yet or already failed. func (ps PaymentStatus) initializable() error { switch ps { // The payment has been created already. We will disallow creating it // again in case other goroutines have already been creating HTLCs for // it. case StatusInitiated: return ErrPaymentExists // We already have an InFlight payment on the network. We will disallow // any new payments. case StatusInFlight: return ErrPaymentInFlight // The payment has been attempted and is succeeded so we won't allow // creating it again. case StatusSucceeded: return ErrAlreadyPaid // We allow retrying failed payments. case StatusFailed: return nil default: return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus, ps) } } // removable returns an error to specify whether deleting the payment with its // current status is allowed. A payment cannot be safely deleted if it has // inflight HTLCs. func (ps PaymentStatus) removable() error { switch ps { // The payment has been created but has no HTLCs and can be removed. case StatusInitiated: return nil // There are still inflight HTLCs and the payment needs to wait for the // final outcomes. case StatusInFlight: return ErrPaymentInFlight // The payment has been attempted and is succeeded and is allowed to be // removed. case StatusSucceeded: return nil // Failed payments are allowed to be removed. case StatusFailed: return nil default: return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus, ps) } } // updatable returns an error to specify whether the payment's HTLCs can be // updated. A payment can update its HTLCs when it has inflight HTLCs. func (ps PaymentStatus) updatable() error { switch ps { // Newly created payments can be updated. case StatusInitiated: return nil // Inflight payments can be updated. case StatusInFlight: return nil // If the payment has a terminal condition, we won't allow any updates. case StatusSucceeded: return ErrPaymentAlreadySucceeded case StatusFailed: return ErrPaymentAlreadyFailed default: return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus, ps) } } // decidePaymentStatus uses the payment's DB state to determine a memory status // that's used by the payment router to decide following actions. // Together, we use four variables to determine the payment's status, // - inflight: whether there are any pending HTLCs. // - settled: whether any of the HTLCs has been settled. // - htlc failed: whether any of the HTLCs has been failed. // - payment failed: whether the payment has been marked as failed. // // Based on the above variables, we derive the status using the following // table, // | inflight | settled | htlc failed | payment failed | status | // |:--------:|:-------:|:-----------:|:--------------:|:--------------------:| // | true | true | true | true | StatusInFlight | // | true | true | true | false | StatusInFlight | // | true | true | false | true | StatusInFlight | // | true | true | false | false | StatusInFlight | // | true | false | true | true | StatusInFlight | // | true | false | true | false | StatusInFlight | // | true | false | false | true | StatusInFlight | // | true | false | false | false | StatusInFlight | // | false | true | true | true | StatusSucceeded | // | false | true | true | false | StatusSucceeded | // | false | true | false | true | StatusSucceeded | // | false | true | false | false | StatusSucceeded | // | false | false | true | true | StatusFailed | // | false | false | true | false | StatusInFlight | // | false | false | false | true | StatusFailed | // | false | false | false | false | StatusInitiated | // // When `inflight`, `settled`, `htlc failed`, and `payment failed` are false, // this indicates the payment is newly created and hasn't made any HTLCs yet. // When `inflight` and `settled` are false, `htlc failed` is true yet `payment // failed` is false, this indicates all the payment's HTLCs have occurred a // temporarily failure and the payment is still in-flight. func decidePaymentStatus(htlcs []HTLCAttempt, reason *FailureReason) (PaymentStatus, error) { var ( inflight bool htlcSettled bool htlcFailed bool paymentFailed bool ) // If we have a failure reason, the payment is failed. if reason != nil { paymentFailed = true } // Go through all HTLCs for this payment, check whether we have any // settled HTLC, and any still in-flight. for _, h := range htlcs { if h.Failure != nil { htlcFailed = true continue } if h.Settle != nil { htlcSettled = true continue } // If any of the HTLCs are not failed nor settled, we // still have inflight HTLCs. inflight = true } // Use the DB state to determine the status of the payment. switch { // If we have inflight HTLCs, no matter we have settled or failed // HTLCs, or the payment failed, we still consider it inflight so we // inform upper systems to wait for the results. case inflight: return StatusInFlight, nil // If we have no in-flight HTLCs, and at least one of the HTLCs is // settled, the payment succeeded. // // NOTE: when reaching this case, paymentFailed could be true, which // means we have a conflicting state for this payment. We choose to // mark the payment as succeeded because it's the receiver's // responsibility to only settle the payment iff all HTLCs are // received. case htlcSettled: return StatusSucceeded, nil // If we have no in-flight HTLCs, and the payment failure is set, the // payment is considered failed. // // NOTE: when reaching this case, settled must be false. case paymentFailed: return StatusFailed, nil // If we have no in-flight HTLCs, yet the payment is NOT failed, it // means all the HTLCs are failed. In this case we can attempt more // HTLCs. // // NOTE: when reaching this case, both settled and paymentFailed must // be false. case htlcFailed: return StatusInFlight, nil // If none of the HTLCs is either settled or failed, and we have no // inflight HTLCs, this means the payment has no HTLCs created yet. // // NOTE: when reaching this case, both settled and paymentFailed must // be false. case !htlcFailed: return StatusInitiated, nil // Otherwise an impossible state is reached. // // NOTE: we should never end up here. default: log.Error("Impossible payment state reached") return 0, fmt.Errorf("%w: payment is corrupted", errPaymentStatusUnknown) } }