@@ -4,8 +4,11 @@ import (
44 "context"
55 "errors"
66 "fmt"
7+ "os"
78 "strconv"
9+ "time"
810
11+ "github.com/lightningnetwork/lnd/lnrpc"
912 "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1013 "github.com/lightningnetwork/lnd/routing/route"
1114 "github.com/urfave/cli"
@@ -98,3 +101,173 @@ func importMissionControl(ctx *cli.Context) error {
98101 _ , err = client .XImportMissionControl (rpcCtx , req )
99102 return err
100103}
104+
105+ var loadMissionControlCommand = cli.Command {
106+ Name : "loadmc" ,
107+ Category : "Mission Control" ,
108+ Usage : "Load mission control results to the internal mission " +
109+ "control state from a file produced by querymc with the " +
110+ "option to shift timestamps. Note that this data is not " +
111+ "persisted across restarts." ,
112+ Action : actionDecorator (loadMissionControl ),
113+ Flags : []cli.Flag {
114+ cli.StringFlag {
115+ Name : "mcdatapath" ,
116+ Usage : "The path to the querymc output file (json)." ,
117+ },
118+ cli.BoolFlag {
119+ Name : "discard" ,
120+ Usage : "Discards current mission control data." ,
121+ },
122+ cli.StringFlag {
123+ Name : "timeoffset" ,
124+ Usage : "Time offset to add to all timestamps. " +
125+ "Format: 1m for a minute, 1h for an hour, 1d " +
126+ "for one day. This can be used to let " +
127+ "mission control data appear to be more " +
128+ "recent, to trick pathfinding's in-built " +
129+ "information decay mechanism. Additionally " +
130+ "by setting 0m, this will report the most " +
131+ "recent result timestamp, which can be used " +
132+ "to find out how old this data is." ,
133+ },
134+ cli.BoolFlag {
135+ Name : "force" ,
136+ Usage : "Whether to force overiding more recent " +
137+ "results in the database with older results " +
138+ "from the file." ,
139+ },
140+ },
141+ }
142+
143+ // loadMissionControl loads mission control data into an LND instance.
144+ func loadMissionControl (ctx * cli.Context ) error {
145+ rpcCtx := context .Background ()
146+
147+ mcDataPath := ctx .String ("mcdatapath" )
148+ if mcDataPath == "" {
149+ return fmt .Errorf ("mcdatapath must be set" )
150+ }
151+
152+ if _ , err := os .Stat (mcDataPath ); os .IsNotExist (err ) {
153+ return fmt .Errorf ("%v does not exist" , mcDataPath )
154+ }
155+
156+ conn := getClientConn (ctx , false )
157+ defer conn .Close ()
158+
159+ client := routerrpc .NewRouterClient (conn )
160+
161+ // Load and unmarshal the querymc output file.
162+ mcRaw , err := os .ReadFile (mcDataPath )
163+ if err != nil {
164+ return fmt .Errorf ("could not read querymc output file: %w" , err )
165+ }
166+
167+ mc := & routerrpc.QueryMissionControlResponse {}
168+ err = lnrpc .ProtoJSONUnmarshalOpts .Unmarshal (mcRaw , mc )
169+ if err != nil {
170+ return fmt .Errorf ("could not unmarshal querymc output file: %w" ,
171+ err )
172+ }
173+
174+ // We discard mission control data if requested.
175+ if ctx .Bool ("discard" ) {
176+ if ! promptForConfirmation ("This will discard all current " +
177+ "mission control data in the database (yes/no): " ) {
178+
179+ return nil
180+ }
181+
182+ _ , err = client .ResetMissionControl (
183+ rpcCtx , & routerrpc.ResetMissionControlRequest {},
184+ )
185+ if err != nil {
186+ return err
187+ }
188+ }
189+
190+ // Add a time offset to all timestamps if requested.
191+ timeOffset := ctx .String ("timeoffset" )
192+ if timeOffset != "" {
193+ offset , err := time .ParseDuration (timeOffset )
194+ if err != nil {
195+ return fmt .Errorf ("could not parse time offset: %w" ,
196+ err )
197+ }
198+
199+ var maxTimestamp time.Time
200+
201+ for _ , pair := range mc .Pairs {
202+ if pair .History .SuccessTime != 0 {
203+ unix := time .Unix (pair .History .SuccessTime , 0 )
204+ unix = unix .Add (offset )
205+
206+ if unix .After (maxTimestamp ) {
207+ maxTimestamp = unix
208+ }
209+
210+ pair .History .SuccessTime = unix .Unix ()
211+ }
212+
213+ if pair .History .FailTime != 0 {
214+ unix := time .Unix (pair .History .FailTime , 0 )
215+ unix = unix .Add (offset )
216+
217+ if unix .After (maxTimestamp ) {
218+ maxTimestamp = unix
219+ }
220+
221+ pair .History .FailTime = unix .Unix ()
222+ }
223+ }
224+
225+ fmt .Printf ("Adding time offset %v to all timestamps. " +
226+ "New max timestamp: %v\n " , offset , maxTimestamp )
227+ }
228+
229+ sanitizeMCData (mc .Pairs )
230+
231+ fmt .Printf ("Mission control file contains %v pairs.\n " , len (mc .Pairs ))
232+ if ! promptForConfirmation ("Import mission control data (yes/no): " ) {
233+ return nil
234+ }
235+
236+ _ , err = client .XImportMissionControl (
237+ rpcCtx ,
238+ & routerrpc.XImportMissionControlRequest {
239+ Pairs : mc .Pairs , Force : ctx .Bool ("force" ),
240+ },
241+ )
242+ if err != nil {
243+ return fmt .Errorf ("could not import mission control data: %w" ,
244+ err )
245+ }
246+
247+ return nil
248+ }
249+
250+ // sanitizeMCData removes invalid data from the exported mission control data.
251+ func sanitizeMCData (mc []* routerrpc.PairHistory ) {
252+ for _ , pair := range mc {
253+ // It is not allowed to import a zero-amount success to mission
254+ // control if a timestamp is set. We unset it in this case.
255+ if pair .History .SuccessTime != 0 &&
256+ pair .History .SuccessAmtMsat == 0 &&
257+ pair .History .SuccessAmtSat == 0 {
258+
259+ pair .History .SuccessTime = 0
260+ }
261+
262+ // If we only deal with a failure, we need to set the failure
263+ // amount to a tiny value due to a limitation in the RPC. This
264+ // will lead to a similar penalization in pathfinding.
265+ if pair .History .SuccessTime == 0 &&
266+ pair .History .FailTime != 0 &&
267+ pair .History .FailAmtMsat == 0 &&
268+ pair .History .FailAmtSat == 0 {
269+
270+ pair .History .FailAmtMsat = 1
271+ }
272+ }
273+ }
0 commit comments