@@ -4,8 +4,11 @@ import (
4
4
"context"
5
5
"errors"
6
6
"fmt"
7
+ "os"
7
8
"strconv"
9
+ "time"
8
10
11
+ "github.com/lightningnetwork/lnd/lnrpc"
9
12
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
10
13
"github.com/lightningnetwork/lnd/routing/route"
11
14
"github.com/urfave/cli"
@@ -98,3 +101,173 @@ func importMissionControl(ctx *cli.Context) error {
98
101
_ , err = client .XImportMissionControl (rpcCtx , req )
99
102
return err
100
103
}
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