diff --git a/contrib/pylightning/lightning/lightning.py b/contrib/pylightning/lightning/lightning.py index da820b3a0..8da0ba269 100644 --- a/contrib/pylightning/lightning/lightning.py +++ b/contrib/pylightning/lightning/lightning.py @@ -428,6 +428,15 @@ class LightningRpc(UnixDomainSocketRpc): """ return self.call("listfunds") + def getroutestats(self, details=True): + """Get statistics about routed payments. + + If @details is True, this'll include the individual forwarded + payments. + + """ + return self.call("getroutestats", payload={'details': details}) + def dev_rescan_outputs(self): """ Synchronize the state of our funds with bitcoind diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 9315a3588..7b38a58fa 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -1842,3 +1842,82 @@ static const struct json_command dev_ignore_htlcs = { }; AUTODATA(json_command, &dev_ignore_htlcs); #endif /* DEVELOPER */ + +static void listforwardings_add_stats(struct json_result *response, struct wallet *wallet) +{ + const struct forwarding_stats *stats; + stats = wallet_forwarded_payments_stats(wallet, tmpctx); + + json_object_start(response, "stats"); + json_object_start(response, "settled"); + json_add_num(response, "fee_msatoshis", stats->fee[FORWARD_SETTLED]); + json_add_num(response, "count", stats->count[FORWARD_SETTLED]); + json_add_num(response, "msatoshi", stats->msatoshi[FORWARD_SETTLED]); + json_object_end(response); + + json_object_start(response, "failed"); + json_add_num(response, "fee_msatoshis", stats->fee[FORWARD_FAILED]); + json_add_num(response, "count", stats->count[FORWARD_FAILED]); + json_add_num(response, "msatoshi", stats->msatoshi[FORWARD_FAILED]); + json_object_end(response); + + json_object_start(response, "pending"); + json_add_num(response, "fee_msatoshis", stats->fee[FORWARD_OFFERED]); + json_add_num(response, "count", stats->count[FORWARD_FAILED]); + json_add_num(response, "msatoshi", stats->msatoshi[FORWARD_FAILED]); + json_object_end(response); + json_object_end(response); + + tal_free(stats); +} + +static void listforwardings_add_forwardings(struct json_result *response, struct wallet *wallet) +{ + const struct forwarding *forwardings; + forwardings = wallet_forwarded_payments_get(wallet, tmpctx); + + json_array_start(response, "forwards"); + for (size_t i=0; ichannel_in); + json_add_short_channel_id(response, "out_channel", &cur->channel_out); + json_add_num(response, "in_msatoshi", cur->msatoshi_in); + json_add_num(response, "out_msatoshi", cur->msatoshi_out); + json_add_num(response, "fee", cur->fee); + json_add_string(response, "status", forward_status_name(cur->status)); + json_object_end(response); + } + json_array_end(response); + + tal_free(forwardings); +} + +static void json_getroutestats(struct command *cmd, const char *buffer, + const jsmntok_t *params) +{ + struct json_result *response = new_json_result(cmd); + bool *details; + + if (!param(cmd, buffer, params, + p_opt_def("details", json_tok_bool, &details, true), + NULL)) + return; + + json_object_start(response, NULL); + listforwardings_add_stats(response, cmd->ld->wallet); + + if (*details) + listforwardings_add_forwardings(response, cmd->ld->wallet); + + json_object_end(response); + + command_success(cmd, response); +} + +static const struct json_command getroutestats_command = { + "getroutestats", json_getroutestats, + "Get statistics about routed / forwarded payments", false, + "Get statistics about routed payments, i.e., the ones we aren't the initiator or recipient, including a detailed list if {details} is true." +}; +AUTODATA(json_command, &getroutestats_command); diff --git a/tests/test_pay.py b/tests/test_pay.py index 62fbef033..9a2063e14 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -985,7 +985,7 @@ def test_forward_stats(node_factory, bitcoind): hash) and l5 will keep the HTLC dangling by disconnecting. """ - amount = 10**4 + amount = 10**5 l1, l2, l3 = node_factory.line_graph(3, announce=False) l4 = node_factory.get_node() l5 = node_factory.get_node(may_fail=True) @@ -1046,3 +1046,7 @@ def test_forward_stats(node_factory, bitcoind): assert outchan['out_msatoshi_offered'] == outchan['out_msatoshi_fulfilled'] assert outchan['out_msatoshi_fulfilled'] < inchan['in_msatoshi_fulfilled'] + + stats = l2.rpc.getroutestats() + + assert [f['status'] for f in stats['forwards']] == ['settled', 'failed', 'offered'] diff --git a/wallet/wallet.c b/wallet/wallet.c index 43facb44b..7a9517cd8 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -2430,3 +2430,63 @@ void wallet_forwarded_payment_add(struct wallet *w, const struct htlc_in *in, sqlite3_bind_int(stmt, 7, state); db_exec_prepared(w->db, stmt); } + +const struct forwarding_stats *wallet_forwarded_payments_stats(struct wallet *w, + const tal_t *ctx) +{ + struct forwarding_stats *stats = talz(ctx, struct forwarding_stats); + sqlite3_stmt *stmt; + stmt = db_prepare(w->db, + "SELECT" + " state" + ", COUNT(*)" + ", SUM(out_msatoshi) as total" + ", SUM(in_msatoshi - out_msatoshi) as fee " + "FROM forwarded_payments " + "GROUP BY state;"); + + while (sqlite3_step(stmt) == SQLITE_ROW) { + enum forward_status state = sqlite3_column_int(stmt, 0); + stats->count[state] = sqlite3_column_int64(stmt, 1); + stats->msatoshi[state] = sqlite3_column_int64(stmt, 2); + stats->fee[state] = sqlite3_column_int64(stmt, 3); + } + + db_stmt_done(stmt); + + return stats; +} + +const struct forwarding *wallet_forwarded_payments_get(struct wallet *w, + const tal_t *ctx) +{ + struct forwarding *results = tal_arr(ctx, struct forwarding, 0); + size_t count = 0; + sqlite3_stmt *stmt; + stmt = db_prepare(w->db, + "SELECT" + " f.state" + ", in_msatoshi" + ", out_msatoshi" + ", hin.payment_hash as payment_hash" + ", in_channel_scid" + ", out_channel_scid " + "FROM forwarded_payments f " + "LEFT JOIN channel_htlcs hin ON (f.in_htlc_id == hin.id)"); + + for (count=0; sqlite3_step(stmt) == SQLITE_ROW; count++) { + tal_resize(&results, count+1); + struct forwarding *cur = &results[count]; + cur->status = sqlite3_column_int(stmt, 0); + cur->msatoshi_in = sqlite3_column_int64(stmt, 1); + cur->msatoshi_out = sqlite3_column_int64(stmt, 2); + cur->fee = cur->msatoshi_in - cur->msatoshi_out; + sqlite3_column_sha256_double(stmt, 3, &cur->payment_hash); + sqlite3_column_short_channel_id(stmt, 4, &cur->channel_in); + sqlite3_column_short_channel_id(stmt, 5, &cur->channel_out); + } + + db_stmt_done(stmt); + + return results; +} diff --git a/wallet/wallet.h b/wallet/wallet.h index df2c5296b..46187276a 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -146,6 +146,31 @@ static inline enum forward_status wallet_forward_status_in_db(enum forward_statu fatal("%s: %u is invalid", __func__, s); } +static inline const char* forward_status_name(enum forward_status status) +{ + switch(status) { + case FORWARD_OFFERED: + return "offered"; + case FORWARD_SETTLED: + return "settled"; + case FORWARD_FAILED: + return "failed"; + } + abort(); +} + +struct forwarding { + struct short_channel_id channel_in, channel_out; + u64 msatoshi_in, msatoshi_out, fee; + struct sha256_double payment_hash; + enum forward_status status; +}; + +struct forwarding_stats { + /* One entry for each of the forward_statuses */ + u64 count[3], msatoshi[3], fee[3]; +}; + /* A database backed shachain struct. The datastructure is * writethrough, reads are performed from an in-memory version, all * writes are passed through to the DB. */ @@ -995,8 +1020,22 @@ u32 *wallet_onchaind_channels(struct wallet *w, struct channeltx *wallet_channeltxs_get(struct wallet *w, const tal_t *ctx, u32 channel_id); +/** + * Add of update a forwarded_payment + */ void wallet_forwarded_payment_add(struct wallet *w, const struct htlc_in *in, const struct htlc_out *out, enum forward_status state); +/** + * Retrieve global stats about all forwarded_payments + */ +const struct forwarding_stats *wallet_forwarded_payments_stats(struct wallet *w, + const tal_t *ctx); + +/** + * Retrieve a list of all forwarded_payments + */ +const struct forwarding *wallet_forwarded_payments_get(struct wallet *w, + const tal_t *ctx); #endif /* LIGHTNING_WALLET_WALLET_H */