package routing import ( "bytes" "errors" "fmt" "io" "sync" "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btclog/v2" "github.com/btcsuite/btcwallet/walletdb" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" ) const ( // DefaultPenaltyHalfLife is the default half-life duration. The // half-life duration defines after how much time a penalized node or // channel is back at 50% probability. DefaultPenaltyHalfLife = time.Hour // minSecondChanceInterval is the minimum time required between // second-chance failures. // // If nodes return a channel policy related failure, they may get a // second chance to forward the payment. It could be that the channel // policy that we are aware of is not up to date. This is especially // important in case of mobile apps that are mostly offline. // // However, we don't want to give nodes the option to endlessly return // new channel updates so that we are kept busy trying to route through // that node until the payment loop times out. // // Therefore we only grant a second chance to a node if the previous // second chance is sufficiently long ago. This is what // minSecondChanceInterval defines. If a second policy failure comes in // within that interval, we will apply a penalty. // // Second chances granted are tracked on the level of node pairs. This // means that if a node has multiple channels to the same peer, they // will only get a single second chance to route to that peer again. // Nodes forward non-strict, so it isn't necessary to apply a less // restrictive channel level tracking scheme here. minSecondChanceInterval = time.Minute // DefaultMaxMcHistory is the default maximum history size. DefaultMaxMcHistory = 1000 // DefaultMcFlushInterval is the default interval we use to flush MC state // to the database. DefaultMcFlushInterval = time.Second // prevSuccessProbability is the assumed probability for node pairs that // successfully relayed the previous attempt. prevSuccessProbability = 0.95 // DefaultAprioriWeight is the default a priori weight. See // MissionControlConfig for further explanation. DefaultAprioriWeight = 0.5 // DefaultMinFailureRelaxInterval is the default minimum time that must // have passed since the previously recorded failure before the failure // amount may be raised. DefaultMinFailureRelaxInterval = time.Minute // DefaultFeeEstimationTimeout is the default value for // FeeEstimationTimeout. It defines the maximum duration that the // probing fee estimation is allowed to take. DefaultFeeEstimationTimeout = time.Minute // DefaultMissionControlNamespace is the name of the default mission // control name space. This is used as the sub-bucket key within the // top level DB bucket to store mission control results. DefaultMissionControlNamespace = "default" ) var ( // ErrInvalidMcHistory is returned if we get a negative mission control // history count. ErrInvalidMcHistory = errors.New("mission control history must be " + ">= 0") // ErrInvalidFailureInterval is returned if we get an invalid failure // interval. ErrInvalidFailureInterval = errors.New("failure interval must be >= 0") ) // NodeResults contains previous results from a node to its peers. type NodeResults map[route.Vertex]TimedPairResult // mcConfig holds various config members that will be required by all // MissionControl instances and will be the same regardless of namespace. type mcConfig struct { // clock is a time source used by mission control. clock clock.Clock // selfNode is our pubkey. selfNode route.Vertex } // MissionControl contains state which summarizes the past attempts of HTLC // routing by external callers when sending payments throughout the network. It // acts as a shared memory during routing attempts with the goal to optimize the // payment attempt success rate. // // Failed payment attempts are reported to mission control. These reports are // used to track the time of the last node or channel level failure. The time // since the last failure is used to estimate a success probability that is fed // into the path finding process for subsequent payment attempts. type MissionControl struct { cfg *mcConfig // state is the internal mission control state that is input for // probability estimation. state *missionControlState store *missionControlStore // estimator is the probability estimator that is used with the payment // results that mission control collects. estimator Estimator // onConfigUpdate is a function that is called whenever the // mission control state is updated. onConfigUpdate fn.Option[func(cfg *MissionControlConfig)] log btclog.Logger mu sync.Mutex } // MissionController manages MissionControl instances in various namespaces. type MissionController struct { db kvdb.Backend cfg *mcConfig defaultMCCfg *MissionControlConfig mc map[string]*MissionControl mu sync.Mutex // TODO(roasbeef): further counters, if vertex continually unavailable, // add to another generation // TODO(roasbeef): also add favorable metrics for nodes } // GetNamespacedStore returns the MissionControl in the given namespace. If one // does not yet exist, then it is initialised. func (m *MissionController) GetNamespacedStore(ns string) (*MissionControl, error) { m.mu.Lock() defer m.mu.Unlock() if mc, ok := m.mc[ns]; ok { return mc, nil } return m.initMissionControl(ns) } // ListNamespaces returns a list of the namespaces that the MissionController // is aware of. func (m *MissionController) ListNamespaces() []string { m.mu.Lock() defer m.mu.Unlock() namespaces := make([]string, 0, len(m.mc)) for ns := range m.mc { namespaces = append(namespaces, ns) } return namespaces } // MissionControlConfig defines parameters that control mission control // behaviour. type MissionControlConfig struct { // Estimator gives probability estimates for node pairs. Estimator Estimator // OnConfigUpdate is function that is called whenever the // mission control state is updated. OnConfigUpdate fn.Option[func(cfg *MissionControlConfig)] // MaxMcHistory defines the maximum number of payment results that are // held on disk. MaxMcHistory int // McFlushInterval defines the ticker interval when we flush the // accumulated state to the DB. McFlushInterval time.Duration // MinFailureRelaxInterval is the minimum time that must have passed // since the previously recorded failure before the failure amount may // be raised. MinFailureRelaxInterval time.Duration } func (c *MissionControlConfig) validate() error { if c.MaxMcHistory < 0 { return ErrInvalidMcHistory } if c.MinFailureRelaxInterval < 0 { return ErrInvalidFailureInterval } return nil } // String returns a string representation of a mission control config. func (c *MissionControlConfig) String() string { return fmt.Sprintf("maximum history: %v, minimum failure relax "+ "interval: %v", c.MaxMcHistory, c.MinFailureRelaxInterval) } // TimedPairResult describes a timestamped pair result. type TimedPairResult struct { // FailTime is the time of the last failure. FailTime time.Time // FailAmt is the amount of the last failure. This amount may be pushed // up if a later success is higher than the last failed amount. FailAmt lnwire.MilliSatoshi // SuccessTime is the time of the last success. SuccessTime time.Time // SuccessAmt is the highest amount that successfully forwarded. This // isn't necessarily the last success amount. The value of this field // may also be pushed down if a later failure is lower than the highest // success amount. Because of this, SuccessAmt may not match // SuccessTime. SuccessAmt lnwire.MilliSatoshi } // MissionControlSnapshot contains a snapshot of the current state of mission // control. type MissionControlSnapshot struct { // Pairs is a list of channels for which specific information is // logged. Pairs []MissionControlPairSnapshot } // MissionControlPairSnapshot contains a snapshot of the current node pair // state in mission control. type MissionControlPairSnapshot struct { // Pair is the node pair of which the state is described. Pair DirectedNodePair // TimedPairResult contains the data for this pair. TimedPairResult } // paymentResult is the information that becomes available when a payment // attempt completes. type paymentResult struct { id uint64 timeFwd tlv.RecordT[tlv.TlvType0, uint64] timeReply tlv.RecordT[tlv.TlvType1, uint64] route tlv.RecordT[tlv.TlvType2, mcRoute] // failure holds information related to the failure of a payment. The // presence of this record indicates a payment failure. The absence of // this record indicates a successful payment. failure tlv.OptionalRecordT[tlv.TlvType3, paymentFailure] } // newPaymentResult constructs a new paymentResult. func newPaymentResult(id uint64, rt *mcRoute, timeFwd, timeReply time.Time, failure *paymentFailure) *paymentResult { result := &paymentResult{ id: id, timeFwd: tlv.NewPrimitiveRecord[tlv.TlvType0]( uint64(timeFwd.UnixNano()), ), timeReply: tlv.NewPrimitiveRecord[tlv.TlvType1]( uint64(timeReply.UnixNano()), ), route: tlv.NewRecordT[tlv.TlvType2](*rt), } if failure != nil { result.failure = tlv.SomeRecordT( tlv.NewRecordT[tlv.TlvType3](*failure), ) } return result } // NewMissionController returns a new instance of MissionController. func NewMissionController(db kvdb.Backend, self route.Vertex, cfg *MissionControlConfig) (*MissionController, error) { log.Debugf("Instantiating mission control with config: %v, %v", cfg, cfg.Estimator) if err := cfg.validate(); err != nil { return nil, err } mcCfg := &mcConfig{ clock: clock.NewDefaultClock(), selfNode: self, } mgr := &MissionController{ db: db, defaultMCCfg: cfg, cfg: mcCfg, mc: make(map[string]*MissionControl), } if err := mgr.loadMissionControls(); err != nil { return nil, err } for _, mc := range mgr.mc { if err := mc.init(); err != nil { return nil, err } } return mgr, nil } // loadMissionControls initialises a MissionControl in the default namespace if // one does not yet exist. It then initialises a MissionControl for all other // namespaces found in the DB. // // NOTE: this should only be called once during MissionController construction. func (m *MissionController) loadMissionControls() error { m.mu.Lock() defer m.mu.Unlock() // Always initialise the default namespace. _, err := m.initMissionControl(DefaultMissionControlNamespace) if err != nil { return err } namespaces := make(map[string]struct{}) err = m.db.View(func(tx walletdb.ReadTx) error { mcStoreBkt := tx.ReadBucket(resultsKey) if mcStoreBkt == nil { return fmt.Errorf("top level mission control bucket " + "not found") } // Iterate through all the keys in the bucket and collect the // namespaces. return mcStoreBkt.ForEach(func(k, _ []byte) error { // We've already initialised the default namespace so // we can skip it. if string(k) == DefaultMissionControlNamespace { return nil } namespaces[string(k)] = struct{}{} return nil }) }, func() {}) if err != nil { return err } // Now, iterate through all the namespaces and initialise them. for ns := range namespaces { _, err = m.initMissionControl(ns) if err != nil { return err } } return nil } // initMissionControl creates a new MissionControl instance with the given // namespace if one does not yet exist. // // NOTE: the MissionController's mutex must be held before calling this method. func (m *MissionController) initMissionControl(namespace string) ( *MissionControl, error) { // If a mission control with this namespace has already been initialised // then there is nothing left to do. if mc, ok := m.mc[namespace]; ok { return mc, nil } cfg := m.defaultMCCfg store, err := newMissionControlStore( newNamespacedDB(m.db, namespace), cfg.MaxMcHistory, cfg.McFlushInterval, ) if err != nil { return nil, err } mc := &MissionControl{ cfg: m.cfg, state: newMissionControlState(cfg.MinFailureRelaxInterval), store: store, estimator: cfg.Estimator, log: build.NewPrefixLog( fmt.Sprintf("[%s]:", namespace), log, ), onConfigUpdate: cfg.OnConfigUpdate, } m.mc[namespace] = mc return mc, nil } // RunStoreTickers runs the mission controller store's tickers. func (m *MissionController) RunStoreTickers() { m.mu.Lock() defer m.mu.Unlock() for _, mc := range m.mc { mc.store.run() } } // StopStoreTickers stops the mission control store's tickers. func (m *MissionController) StopStoreTickers() { log.Debug("Stopping mission control store ticker") defer log.Debug("Mission control store ticker stopped") m.mu.Lock() defer m.mu.Unlock() for _, mc := range m.mc { mc.store.stop() } } // init initializes mission control with historical data. func (m *MissionControl) init() error { m.log.Debugf("Mission control state reconstruction started") m.mu.Lock() defer m.mu.Unlock() start := time.Now() results, err := m.store.fetchAll() if err != nil { return err } for _, result := range results { _ = m.applyPaymentResult(result) } m.log.Debugf("Mission control state reconstruction finished: "+ "n=%v, time=%v", len(results), time.Since(start)) return nil } // GetConfig returns the config that mission control is currently configured // with. All fields are copied by value, so we do not need to worry about // mutation. func (m *MissionControl) GetConfig() *MissionControlConfig { m.mu.Lock() defer m.mu.Unlock() return &MissionControlConfig{ Estimator: m.estimator, MaxMcHistory: m.store.maxRecords, McFlushInterval: m.store.flushInterval, MinFailureRelaxInterval: m.state.minFailureRelaxInterval, } } // SetConfig validates the config provided and updates mission control's config // if it is valid. func (m *MissionControl) SetConfig(cfg *MissionControlConfig) error { if cfg == nil { return errors.New("nil mission control config") } if err := cfg.validate(); err != nil { return err } m.mu.Lock() defer m.mu.Unlock() m.log.Infof("Active mission control cfg: %v, estimator: %v", cfg, cfg.Estimator) m.store.maxRecords = cfg.MaxMcHistory m.state.minFailureRelaxInterval = cfg.MinFailureRelaxInterval m.estimator = cfg.Estimator // Execute the callback function if it is set. m.onConfigUpdate.WhenSome(func(f func(cfg *MissionControlConfig)) { f(cfg) }) return nil } // ResetHistory resets the history of MissionControl returning it to a state as // if no payment attempts have been made. func (m *MissionControl) ResetHistory() error { m.mu.Lock() defer m.mu.Unlock() if err := m.store.clear(); err != nil { return err } m.state.resetHistory() m.log.Debugf("Mission control history cleared") return nil } // GetProbability is expected to return the success probability of a payment // from fromNode along edge. func (m *MissionControl) GetProbability(fromNode, toNode route.Vertex, amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { m.mu.Lock() defer m.mu.Unlock() now := m.cfg.clock.Now() results, _ := m.state.getLastPairResult(fromNode) // Use a distinct probability estimation function for local channels. if fromNode == m.cfg.selfNode { return m.estimator.LocalPairProbability(now, results, toNode) } return m.estimator.PairProbability( now, results, toNode, amt, capacity, ) } // GetHistorySnapshot takes a snapshot from the current mission control state // and actual probability estimates. func (m *MissionControl) GetHistorySnapshot() *MissionControlSnapshot { m.mu.Lock() defer m.mu.Unlock() m.log.Debugf("Requesting history snapshot from mission control") return m.state.getSnapshot() } // ImportHistory imports the set of mission control results provided to our // in-memory state. These results are not persisted, so will not survive // restarts. func (m *MissionControl) ImportHistory(history *MissionControlSnapshot, force bool) error { if history == nil { return errors.New("cannot import nil history") } m.mu.Lock() defer m.mu.Unlock() m.log.Infof("Importing history snapshot with %v pairs to mission "+ "control", len(history.Pairs)) imported := m.state.importSnapshot(history, force) m.log.Infof("Imported %v results to mission control", imported) return nil } // GetPairHistorySnapshot returns the stored history for a given node pair. func (m *MissionControl) GetPairHistorySnapshot( fromNode, toNode route.Vertex) TimedPairResult { m.mu.Lock() defer m.mu.Unlock() results, ok := m.state.getLastPairResult(fromNode) if !ok { return TimedPairResult{} } result, ok := results[toNode] if !ok { return TimedPairResult{} } return result } // ReportPaymentFail reports a failed payment to mission control as input for // future probability estimates. The failureSourceIdx argument indicates the // failure source. If it is nil, the failure source is unknown. This function // returns a reason if this failure is a final failure. In that case no further // payment attempts need to be made. func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route, failureSourceIdx *int, failure lnwire.FailureMessage) ( *channeldb.FailureReason, error) { timestamp := m.cfg.clock.Now() result := newPaymentResult( paymentID, extractMCRoute(rt), timestamp, timestamp, newPaymentFailure(failureSourceIdx, failure), ) return m.processPaymentResult(result) } // ReportPaymentSuccess reports a successful payment to mission control as input // for future probability estimates. func (m *MissionControl) ReportPaymentSuccess(paymentID uint64, rt *route.Route) error { timestamp := m.cfg.clock.Now() result := newPaymentResult( paymentID, extractMCRoute(rt), timestamp, timestamp, nil, ) _, err := m.processPaymentResult(result) return err } // processPaymentResult stores a payment result in the mission control store and // updates mission control's in-memory state. func (m *MissionControl) processPaymentResult(result *paymentResult) ( *channeldb.FailureReason, error) { // Store complete result in database. m.store.AddResult(result) m.mu.Lock() defer m.mu.Unlock() // Apply result to update mission control state. reason := m.applyPaymentResult(result) return reason, nil } // applyPaymentResult applies a payment result as input for future probability // estimates. It returns a bool indicating whether this error is a final error // and no further payment attempts need to be made. func (m *MissionControl) applyPaymentResult( result *paymentResult) *channeldb.FailureReason { // Interpret result. i := interpretResult(&result.route.Val, result.failure.ValOpt()) if i.policyFailure != nil { if m.state.requestSecondChance( time.Unix(0, int64(result.timeReply.Val)), i.policyFailure.From, i.policyFailure.To, ) { return nil } } // If there is a node-level failure, record a failure for every tried // connection of that node. A node-level failure can be considered as a // failure that would have occurred with any of the node's channels. // // Ideally we'd also record the failure for the untried connections of // the node. Unfortunately this would require access to the graph and // adding this dependency and db calls does not outweigh the benefits. // // Untried connections will fall back to the node probability. After the // call to setAllPairResult below, the node probability will be equal to // the probability of the tried channels except that the a priori // probability is mixed in too. This effect is controlled by the // aprioriWeight parameter. If that parameter isn't set to an extreme // and there are a few known connections, there shouldn't be much of a // difference. The largest difference occurs when aprioriWeight is 1. In // that case, a node-level failure would not be applied to untried // channels. if i.nodeFailure != nil { m.log.Debugf("Reporting node failure to Mission Control: "+ "node=%v", *i.nodeFailure) m.state.setAllFail( *i.nodeFailure, time.Unix(0, int64(result.timeReply.Val)), ) } for pair, pairResult := range i.pairResults { pairResult := pairResult if pairResult.success { m.log.Debugf("Reporting pair success to Mission "+ "Control: pair=%v, amt=%v", pair, pairResult.amt) } else { m.log.Debugf("Reporting pair failure to Mission "+ "Control: pair=%v, amt=%v", pair, pairResult.amt) } m.state.setLastPairResult( pair.From, pair.To, time.Unix(0, int64(result.timeReply.Val)), &pairResult, false, ) } return i.finalFailureReason } // namespacedDB is an implementation of the missionControlDB that gives a user // of the interface access to a namespaced bucket within the top level mission // control bucket. type namespacedDB struct { topLevelBucketKey []byte namespace []byte db kvdb.Backend } // A compile-time check to ensure that namespacedDB implements missionControlDB. var _ missionControlDB = (*namespacedDB)(nil) // newDefaultNamespacedStore creates an instance of namespaceDB that uses the // default namespace. func newDefaultNamespacedStore(db kvdb.Backend) missionControlDB { return newNamespacedDB(db, DefaultMissionControlNamespace) } // newNamespacedDB creates a new instance of missionControlDB where the DB will // have access to a namespaced bucket within the top level mission control // bucket. func newNamespacedDB(db kvdb.Backend, namespace string) missionControlDB { return &namespacedDB{ db: db, namespace: []byte(namespace), topLevelBucketKey: resultsKey, } } // update can be used to perform reads and writes on the given bucket. // // NOTE: this is part of the missionControlDB interface. func (n *namespacedDB) update(f func(bkt walletdb.ReadWriteBucket) error, reset func()) error { return n.db.Update(func(tx kvdb.RwTx) error { mcStoreBkt, err := tx.CreateTopLevelBucket(n.topLevelBucketKey) if err != nil { return fmt.Errorf("cannot create top level mission "+ "control bucket: %w", err) } namespacedBkt, err := mcStoreBkt.CreateBucketIfNotExists( n.namespace, ) if err != nil { return fmt.Errorf("cannot create namespaced bucket "+ "(%s) in mission control store: %w", n.namespace, err) } return f(namespacedBkt) }, reset) } // view can be used to perform reads on the given bucket. // // NOTE: this is part of the missionControlDB interface. func (n *namespacedDB) view(f func(bkt walletdb.ReadBucket) error, reset func()) error { return n.db.View(func(tx kvdb.RTx) error { mcStoreBkt := tx.ReadBucket(n.topLevelBucketKey) if mcStoreBkt == nil { return fmt.Errorf("top level mission control bucket " + "not found") } namespacedBkt := mcStoreBkt.NestedReadBucket(n.namespace) if namespacedBkt == nil { return fmt.Errorf("namespaced bucket (%s) not found "+ "in mission control store", n.namespace) } return f(namespacedBkt) }, reset) } // purge will delete all the contents in the namespace. // // NOTE: this is part of the missionControlDB interface. func (n *namespacedDB) purge() error { return n.db.Update(func(tx kvdb.RwTx) error { mcStoreBkt := tx.ReadWriteBucket(n.topLevelBucketKey) if mcStoreBkt == nil { return nil } err := mcStoreBkt.DeleteNestedBucket(n.namespace) if err != nil { return err } _, err = mcStoreBkt.CreateBucket(n.namespace) return err }, func() {}) } // paymentFailure represents the presence of a payment failure. It may or may // not include additional information about said failure. type paymentFailure struct { info tlv.OptionalRecordT[tlv.TlvType0, paymentFailureInfo] } // newPaymentFailure constructs a new paymentFailure struct. If the source // index is nil, then an empty paymentFailure is returned. This represents a // failure with unknown details. Otherwise, the index and failure message are // used to populate the info field of the paymentFailure. func newPaymentFailure(sourceIdx *int, failureMsg lnwire.FailureMessage) *paymentFailure { if sourceIdx == nil { return &paymentFailure{} } info := paymentFailureInfo{ sourceIdx: tlv.NewPrimitiveRecord[tlv.TlvType0]( uint8(*sourceIdx), ), msg: tlv.NewRecordT[tlv.TlvType1](failureMessage{failureMsg}), } return &paymentFailure{ info: tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType0](info)), } } // Record returns a TLV record that can be used to encode/decode a // paymentFailure to/from a TLV stream. func (r *paymentFailure) Record() tlv.Record { recordSize := func() uint64 { var ( b bytes.Buffer buf [8]byte ) if err := encodePaymentFailure(&b, r, &buf); err != nil { panic(err) } return uint64(len(b.Bytes())) } return tlv.MakeDynamicRecord( 0, r, recordSize, encodePaymentFailure, decodePaymentFailure, ) } func encodePaymentFailure(w io.Writer, val interface{}, _ *[8]byte) error { if v, ok := val.(*paymentFailure); ok { var recordProducers []tlv.RecordProducer v.info.WhenSome( func(r tlv.RecordT[tlv.TlvType0, paymentFailureInfo]) { recordProducers = append(recordProducers, &r) }, ) return lnwire.EncodeRecordsTo( w, lnwire.ProduceRecordsSorted(recordProducers...), ) } return tlv.NewTypeForEncodingErr(val, "routing.paymentFailure") } func decodePaymentFailure(r io.Reader, val interface{}, _ *[8]byte, l uint64) error { if v, ok := val.(*paymentFailure); ok { var h paymentFailure info := tlv.ZeroRecordT[tlv.TlvType0, paymentFailureInfo]() typeMap, err := lnwire.DecodeRecords( r, lnwire.ProduceRecordsSorted(&info)..., ) if err != nil { return err } if _, ok := typeMap[h.info.TlvType()]; ok { h.info = tlv.SomeRecordT(info) } *v = h return nil } return tlv.NewTypeForDecodingErr(val, "routing.paymentFailure", l, l) } // paymentFailureInfo holds additional information about a payment failure. type paymentFailureInfo struct { sourceIdx tlv.RecordT[tlv.TlvType0, uint8] msg tlv.RecordT[tlv.TlvType1, failureMessage] } // Record returns a TLV record that can be used to encode/decode a // paymentFailureInfo to/from a TLV stream. func (r *paymentFailureInfo) Record() tlv.Record { recordSize := func() uint64 { var ( b bytes.Buffer buf [8]byte ) if err := encodePaymentFailureInfo(&b, r, &buf); err != nil { panic(err) } return uint64(len(b.Bytes())) } return tlv.MakeDynamicRecord( 0, r, recordSize, encodePaymentFailureInfo, decodePaymentFailureInfo, ) } func encodePaymentFailureInfo(w io.Writer, val interface{}, _ *[8]byte) error { if v, ok := val.(*paymentFailureInfo); ok { return lnwire.EncodeRecordsTo( w, lnwire.ProduceRecordsSorted( &v.sourceIdx, &v.msg, ), ) } return tlv.NewTypeForEncodingErr(val, "routing.paymentFailureInfo") } func decodePaymentFailureInfo(r io.Reader, val interface{}, _ *[8]byte, l uint64) error { if v, ok := val.(*paymentFailureInfo); ok { var h paymentFailureInfo _, err := lnwire.DecodeRecords( r, lnwire.ProduceRecordsSorted(&h.sourceIdx, &h.msg)..., ) if err != nil { return err } *v = h return nil } return tlv.NewTypeForDecodingErr( val, "routing.paymentFailureInfo", l, l, ) }