mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-03-01 01:32:34 +01:00
pay: make interface idempotent.
We stopped automatically retransmitting locally-generated add/removes after a reconnect, but this breaks the "pay" interface as it stands. The correct solution to this is to make the pay interface idempotent: you can trigger it as many times as you want and it will only succeed once. Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
43c0cdb5e4
commit
012574790d
7 changed files with 143 additions and 51 deletions
|
@ -246,6 +246,7 @@ static struct lightningd_state *lightningd_state(void)
|
||||||
"lightningd(%u):", (int)getpid());
|
"lightningd(%u):", (int)getpid());
|
||||||
|
|
||||||
list_head_init(&dstate->peers);
|
list_head_init(&dstate->peers);
|
||||||
|
list_head_init(&dstate->pay_commands);
|
||||||
timers_init(&dstate->timers, controlled_time());
|
timers_init(&dstate->timers, controlled_time());
|
||||||
txwatch_hash_init(&dstate->txwatches);
|
txwatch_hash_init(&dstate->txwatches);
|
||||||
txowatch_hash_init(&dstate->txowatches);
|
txowatch_hash_init(&dstate->txowatches);
|
||||||
|
|
|
@ -87,6 +87,9 @@ struct lightningd_state {
|
||||||
/* Addresses to contact peers. */
|
/* Addresses to contact peers. */
|
||||||
struct list_head addresses;
|
struct list_head addresses;
|
||||||
|
|
||||||
|
/* Any outstanding "pay" commands. */
|
||||||
|
struct list_head pay_commands;
|
||||||
|
|
||||||
/* Crypto tables for global use. */
|
/* Crypto tables for global use. */
|
||||||
secp256k1_context *secpctx;
|
secp256k1_context *secpctx;
|
||||||
|
|
||||||
|
|
164
daemon/pay.c
164
daemon/pay.c
|
@ -8,67 +8,106 @@
|
||||||
#include "peer.h"
|
#include "peer.h"
|
||||||
#include "routing.h"
|
#include "routing.h"
|
||||||
#include <ccan/str/hex/hex.h>
|
#include <ccan/str/hex/hex.h>
|
||||||
|
#include <ccan/structeq/structeq.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
|
|
||||||
/* Outstanding "pay" commands. */
|
/* Outstanding "pay" commands. */
|
||||||
struct pay_command {
|
struct pay_command {
|
||||||
struct list_node list;
|
struct list_node list;
|
||||||
|
struct sha256 rhash;
|
||||||
|
u64 msatoshis, fee;
|
||||||
|
struct pubkey id;
|
||||||
|
/* Set if this is in progress. */
|
||||||
struct htlc *htlc;
|
struct htlc *htlc;
|
||||||
|
/* Preimage if this succeeded. */
|
||||||
|
struct rval *rval;
|
||||||
struct command *cmd;
|
struct command *cmd;
|
||||||
};
|
};
|
||||||
|
|
||||||
void complete_pay_command(struct peer *peer, struct htlc *htlc)
|
static void json_pay_success(struct command *cmd, const struct rval *rval)
|
||||||
|
{
|
||||||
|
struct json_result *response;
|
||||||
|
|
||||||
|
response = new_json_result(cmd);
|
||||||
|
json_object_start(response, NULL);
|
||||||
|
json_add_hex(response, "preimage", rval, sizeof(*rval));
|
||||||
|
json_object_end(response);
|
||||||
|
command_success(cmd, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_json(struct command *cmd, const struct htlc *htlc)
|
||||||
|
{
|
||||||
|
FailInfo *f;
|
||||||
|
struct pubkey id;
|
||||||
|
const char *idstr = "INVALID";
|
||||||
|
|
||||||
|
if (htlc->r) {
|
||||||
|
json_pay_success(cmd, htlc->r);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
f = failinfo_unwrap(cmd, htlc->fail, tal_count(htlc->fail));
|
||||||
|
if (!f) {
|
||||||
|
command_fail(cmd, "failed (bad message)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proto_to_pubkey(cmd->dstate->secpctx, f->id, &id))
|
||||||
|
idstr = pubkey_to_hexstr(cmd, cmd->dstate->secpctx, &id);
|
||||||
|
|
||||||
|
command_fail(cmd,
|
||||||
|
"failed: error code %u node %s reason %s",
|
||||||
|
f->error_code, idstr, f->reason ? f->reason : "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
void complete_pay_command(struct lightningd_state *dstate,
|
||||||
|
const struct htlc *htlc)
|
||||||
{
|
{
|
||||||
struct pay_command *i;
|
struct pay_command *i;
|
||||||
|
|
||||||
list_for_each(&peer->pay_commands, i, list) {
|
list_for_each(&dstate->pay_commands, i, list) {
|
||||||
if (i->htlc == htlc) {
|
if (i->htlc == htlc) {
|
||||||
if (htlc->r) {
|
if (htlc->r)
|
||||||
struct json_result *response;
|
i->rval = tal_dup(i, struct rval, htlc->r);
|
||||||
|
i->htlc = NULL;
|
||||||
|
|
||||||
response = new_json_result(i->cmd);
|
/* Can be NULL if JSON RPC goes away. */
|
||||||
json_object_start(response, NULL);
|
if (i->cmd)
|
||||||
json_add_hex(response, "preimage",
|
handle_json(i->cmd, htlc);
|
||||||
htlc->r, sizeof(*htlc->r));
|
|
||||||
json_object_end(response);
|
|
||||||
command_success(i->cmd, response);
|
|
||||||
} else {
|
|
||||||
FailInfo *f;
|
|
||||||
f = failinfo_unwrap(i->cmd, htlc->fail,
|
|
||||||
tal_count(htlc->fail));
|
|
||||||
if (!f) {
|
|
||||||
command_fail(i->cmd,
|
|
||||||
"htlc failed (bad message)");
|
|
||||||
} else {
|
|
||||||
struct pubkey id;
|
|
||||||
secp256k1_context *secpctx;
|
|
||||||
const char *idstr = "INVALID";
|
|
||||||
|
|
||||||
secpctx = i->cmd->dstate->secpctx;
|
|
||||||
if (proto_to_pubkey(secpctx,
|
|
||||||
f->id, &id))
|
|
||||||
idstr = pubkey_to_hexstr(i->cmd,
|
|
||||||
secpctx, &id);
|
|
||||||
command_fail(i->cmd,
|
|
||||||
"htlc failed: error code %u"
|
|
||||||
" node %s, reason %s",
|
|
||||||
f->error_code, idstr,
|
|
||||||
f->reason ? f->reason
|
|
||||||
: "unknown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Can happen if RPC connection goes away. */
|
/* Can happen if RPC connection goes away. */
|
||||||
log_unusual(peer->log, "No command for HTLC %"PRIu64" %s",
|
log_unusual(dstate->base_log, "No command for HTLC %"PRIu64" %s",
|
||||||
htlc->id, htlc->r ? "fulfill" : "fail");
|
htlc->id, htlc->r ? "fulfill" : "fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void remove_from_list(struct pay_command *pc)
|
/* When JSON RPC goes away, cmd is freed: detach from any running paycommand */
|
||||||
|
static void remove_cmd_from_pc(struct command *cmd)
|
||||||
{
|
{
|
||||||
list_del(&pc->list);
|
struct pay_command *pc;
|
||||||
|
|
||||||
|
list_for_each(&cmd->dstate->pay_commands, pc, list) {
|
||||||
|
if (pc->cmd == cmd) {
|
||||||
|
pc->cmd = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* We can reach here, in the case where another pay command
|
||||||
|
* re-uses the pc->cmd before we get around to cleaning up. */
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct pay_command *find_pay_command(struct lightningd_state *dstate,
|
||||||
|
const struct sha256 *rhash)
|
||||||
|
{
|
||||||
|
struct pay_command *pc;
|
||||||
|
|
||||||
|
list_for_each(&dstate->pay_commands, pc, list) {
|
||||||
|
if (structeq(rhash, &pc->rhash))
|
||||||
|
return pc;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void json_pay(struct command *cmd,
|
static void json_pay(struct command *cmd,
|
||||||
|
@ -120,6 +159,39 @@ static void json_pay(struct command *cmd,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pc = find_pay_command(cmd->dstate, &rhash);
|
||||||
|
if (pc) {
|
||||||
|
log_debug(cmd->dstate->base_log, "json_pay: found previous");
|
||||||
|
if (pc->htlc) {
|
||||||
|
log_add(cmd->dstate->base_log, "... still in progress");
|
||||||
|
command_fail(cmd, "still in progress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pc->rval) {
|
||||||
|
log_add(cmd->dstate->base_log, "... succeeded");
|
||||||
|
/* Must match successful payment parameters. */
|
||||||
|
if (pc->msatoshis != msatoshis) {
|
||||||
|
command_fail(cmd,
|
||||||
|
"already succeeded with amount %"
|
||||||
|
PRIu64, pc->msatoshis);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!structeq(&pc->id, &id)) {
|
||||||
|
char *previd;
|
||||||
|
previd = pubkey_to_hexstr(cmd,
|
||||||
|
cmd->dstate->secpctx,
|
||||||
|
&pc->id);
|
||||||
|
command_fail(cmd,
|
||||||
|
"already succeeded to %s",
|
||||||
|
previd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
json_pay_success(cmd, pc->rval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log_add(cmd->dstate->base_log, "... retrying");
|
||||||
|
}
|
||||||
|
|
||||||
/* FIXME: Add fee param, check for excessive fee. */
|
/* FIXME: Add fee param, check for excessive fee. */
|
||||||
peer = find_route(cmd->dstate, &id, msatoshis, &fee, &route);
|
peer = find_route(cmd->dstate, &id, msatoshis, &fee, &route);
|
||||||
if (!peer) {
|
if (!peer) {
|
||||||
|
@ -141,8 +213,16 @@ static void json_pay(struct command *cmd,
|
||||||
expiry += get_block_height(cmd->dstate) + 1;
|
expiry += get_block_height(cmd->dstate) + 1;
|
||||||
|
|
||||||
onion = onion_create(cmd, cmd->dstate->secpctx, route, msatoshis, fee);
|
onion = onion_create(cmd, cmd->dstate->secpctx, route, msatoshis, fee);
|
||||||
pc = tal(cmd, struct pay_command);
|
|
||||||
|
if (!pc)
|
||||||
|
pc = tal(cmd->dstate, struct pay_command);
|
||||||
pc->cmd = cmd;
|
pc->cmd = cmd;
|
||||||
|
pc->rhash = rhash;
|
||||||
|
pc->rval = NULL;
|
||||||
|
pc->id = id;
|
||||||
|
pc->msatoshis = msatoshis;
|
||||||
|
pc->fee = fee;
|
||||||
|
|
||||||
err = command_htlc_add(peer, msatoshis + fee, expiry, &rhash, NULL,
|
err = command_htlc_add(peer, msatoshis + fee, expiry, &rhash, NULL,
|
||||||
onion, &error_code, &pc->htlc);
|
onion, &error_code, &pc->htlc);
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -151,13 +231,13 @@ static void json_pay(struct command *cmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wait until we get response. */
|
/* Wait until we get response. */
|
||||||
list_add_tail(&peer->pay_commands, &pc->list);
|
list_add_tail(&cmd->dstate->pay_commands, &pc->list);
|
||||||
tal_add_destructor(pc, remove_from_list);
|
tal_add_destructor(cmd, remove_cmd_from_pc);
|
||||||
}
|
}
|
||||||
|
|
||||||
const struct json_command pay_command = {
|
const struct json_command pay_command = {
|
||||||
"pay",
|
"pay",
|
||||||
json_pay,
|
json_pay,
|
||||||
"Send {id} {msatoshis} in return for preimage of {rhash}",
|
"Send {id} {msatoshis} in return for preimage of {rhash}",
|
||||||
"Returns an empty result on success"
|
"Returns the {preimage} on success"
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
#define LIGHTNING_DAEMON_PAY_H
|
#define LIGHTNING_DAEMON_PAY_H
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
struct peer;
|
struct lightningd_state;
|
||||||
struct htlc;
|
struct htlc;
|
||||||
struct rval;
|
|
||||||
|
|
||||||
void complete_pay_command(struct peer *peer, struct htlc *htlc);
|
void complete_pay_command(struct lightningd_state *dstate,
|
||||||
|
const struct htlc *htlc);
|
||||||
|
|
||||||
#endif /* LIGHTNING_DAEMON_PAY_H */
|
#endif /* LIGHTNING_DAEMON_PAY_H */
|
||||||
|
|
|
@ -594,7 +594,7 @@ static void our_htlc_failed(struct peer *peer, struct htlc *htlc)
|
||||||
htlc->fail, tal_count(htlc->fail));
|
htlc->fail, tal_count(htlc->fail));
|
||||||
command_htlc_fail(htlc->src->peer, htlc->src);
|
command_htlc_fail(htlc->src->peer, htlc->src);
|
||||||
} else
|
} else
|
||||||
complete_pay_command(peer, htlc);
|
complete_pay_command(peer->dstate, htlc);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void our_htlc_fulfilled(struct peer *peer, struct htlc *htlc)
|
static void our_htlc_fulfilled(struct peer *peer, struct htlc *htlc)
|
||||||
|
@ -603,7 +603,7 @@ static void our_htlc_fulfilled(struct peer *peer, struct htlc *htlc)
|
||||||
set_htlc_rval(htlc->src->peer, htlc->src, htlc->r);
|
set_htlc_rval(htlc->src->peer, htlc->src, htlc->r);
|
||||||
command_htlc_fulfill(htlc->src->peer, htlc->src);
|
command_htlc_fulfill(htlc->src->peer, htlc->src);
|
||||||
} else {
|
} else {
|
||||||
complete_pay_command(peer, htlc);
|
complete_pay_command(peer->dstate, htlc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2369,7 +2369,6 @@ struct peer *new_peer(struct lightningd_state *dstate,
|
||||||
peer->outpkt = tal_arr(peer, Pkt *, 0);
|
peer->outpkt = tal_arr(peer, Pkt *, 0);
|
||||||
peer->commit_jsoncmd = NULL;
|
peer->commit_jsoncmd = NULL;
|
||||||
list_head_init(&peer->outgoing_txs);
|
list_head_init(&peer->outgoing_txs);
|
||||||
list_head_init(&peer->pay_commands);
|
|
||||||
list_head_init(&peer->their_commits);
|
list_head_init(&peer->their_commits);
|
||||||
peer->anchor.ok_depth = -1;
|
peer->anchor.ok_depth = -1;
|
||||||
peer->order_counter = 0;
|
peer->order_counter = 0;
|
||||||
|
|
|
@ -99,9 +99,6 @@ struct peer {
|
||||||
/* If we're doing a commit, this is the command which triggered it */
|
/* If we're doing a commit, this is the command which triggered it */
|
||||||
struct command *commit_jsoncmd;
|
struct command *commit_jsoncmd;
|
||||||
|
|
||||||
/* Any outstanding "pay" commands. */
|
|
||||||
struct list_head pay_commands;
|
|
||||||
|
|
||||||
/* Global state. */
|
/* Global state. */
|
||||||
struct lightningd_state *dstate;
|
struct lightningd_state *dstate;
|
||||||
|
|
||||||
|
|
|
@ -1008,6 +1008,9 @@ if [ ! -n "$MANUALCOMMIT" ]; then
|
||||||
lcli1 add-route $ID2 $ID3 546000 10 36 36
|
lcli1 add-route $ID2 $ID3 546000 10 36 36
|
||||||
RHASH5=`lcli3 accept-payment $HTLC_AMOUNT | sed 's/.*"\([0-9a-f]*\)".*/\1/'`
|
RHASH5=`lcli3 accept-payment $HTLC_AMOUNT | sed 's/.*"\([0-9a-f]*\)".*/\1/'`
|
||||||
|
|
||||||
|
# FIXME: We don't save payments in db yet!
|
||||||
|
DO_RECONNECT=""
|
||||||
|
|
||||||
# Try wrong hash.
|
# Try wrong hash.
|
||||||
if lcli1 pay $ID3 $HTLC_AMOUNT $RHASH4; then
|
if lcli1 pay $ID3 $HTLC_AMOUNT $RHASH4; then
|
||||||
echo Paid with wrong hash? >&2
|
echo Paid with wrong hash? >&2
|
||||||
|
@ -1028,6 +1031,15 @@ if [ ! -n "$MANUALCOMMIT" ]; then
|
||||||
# starts.
|
# starts.
|
||||||
check lcli3 "getpeers | $FGREP \"\\\"our_amount\\\" : $(($HTLC_AMOUNT - $NO_HTLCS_FEE / 2))\""
|
check lcli3 "getpeers | $FGREP \"\\\"our_amount\\\" : $(($HTLC_AMOUNT - $NO_HTLCS_FEE / 2))\""
|
||||||
lcli3 close $ID2
|
lcli3 close $ID2
|
||||||
|
|
||||||
|
# Re-send should be a noop (doesn't matter that node3 is down!)
|
||||||
|
lcli1 pay $ID3 $HTLC_AMOUNT $RHASH5
|
||||||
|
|
||||||
|
# Re-send to different id or amount should complain.
|
||||||
|
lcli1 pay $ID2 $HTLC_AMOUNT $RHASH5 | $FGREP "already succeeded to $ID3"
|
||||||
|
lcli1 pay $ID2 $(($HTLC_AMOUNT + 1)) $RHASH5 | $FGREP "already succeeded with amount $HTLC_AMOUNT"
|
||||||
|
|
||||||
|
DO_RECONNECT=$RECONNECT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
lcli1 close $ID2
|
lcli1 close $ID2
|
||||||
|
|
Loading…
Add table
Reference in a new issue