core-lightning/test/test_protocol.c
Rusty Russell 73df39e0c9 test_protocol: attach states to each HTLC, rather than using queues.
This is simpler for database representation, and also allows simple
bit-tests for what is happening to a HTLC (eg. am I committed to it?
Are you?  etc.)

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
2016-08-18 14:23:44 +09:30

962 lines
26 KiB
C

/* Simple simulator for protocol. */
#include "config.h"
#include <assert.h>
#include <ccan/array_size/array_size.h>
#include <ccan/err/err.h>
#include <ccan/read_write_all/read_write_all.h>
#include <ccan/short_types/short_types.h>
#include <ccan/str/str.h>
#include <ccan/structeq/structeq.h>
#include <ccan/tal/tal.h>
#include <ccan/tal/str/str.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/uio.h>
#include <unistd.h>
#define A_LINEX 50
#define B_LINEX 195
#define A_TEXTX 45
#define B_TEXTX 200
#define LINE_HEIGHT 5
#define STEP_HEIGHT 10
#define LETTER_WIDTH 3
#define TEXT_STYLE "style=\"font-size:4;\""
struct commit_tx {
/* inhtlcs = htlcs they offered, outhtlcs = htlcs we offered */
u32 inhtlcs, outhtlcs;
/* This is a simple counter, reflecting fee updates. */
u32 fee;
};
/* We keep one for them, one for us. */
struct commit_info {
struct commit_info *prev;
/* How deep we are */
unsigned int number;
/* Have sent/received revocation secret. */
bool revoked;
/* Have their signature, ie. can be broadcast */
bool counterparty_signed;
};
/* A "signature" is a copy of the commit tx state, for easy diagnosis. */
struct signature {
struct commit_tx f;
};
/* What are we doing: adding or removing? */
#define ADDING 0x1000
#define REMOVING 0x2000
#define PENDING 0x001 /* Change is pending. */
#define COMMITTED 0x002 /* HTLC is in commit_tx */
#define REVOKED 0x004 /* Old pre-change tx revoked */
#define OWNER 0x020 /* This side owns it */
#define OURS LOCAL(OWNER)
#define THEIRS REMOTE(OWNER)
#define LOCAL_ 0
#define REMOTE_ 6
#define SIDE(flag,local_or_remote) ((flag) << local_or_remote)
#define OTHER_SIDE(flag,local_or_remote) ((flag) << (6 - local_or_remote))
#define LOCAL(flag) SIDE(flag, LOCAL_)
#define REMOTE(flag) SIDE(flag, REMOTE_)
enum htlc_state {
NONEXISTENT = 0,
/* When we add a new htlc, it goes in this order. */
SENT_ADD_HTLC = ADDING + OURS + REMOTE(PENDING),
SENT_ADD_COMMIT = SENT_ADD_HTLC - REMOTE(PENDING) + REMOTE(COMMITTED),
RECV_ADD_REVOCATION = SENT_ADD_COMMIT + REMOTE(REVOKED),
RECV_ADD_ACK_COMMIT = RECV_ADD_REVOCATION + LOCAL(COMMITTED),
SENT_ADD_ACK_REVOCATION = RECV_ADD_ACK_COMMIT + LOCAL(REVOKED) - ADDING,
/* When they remove an htlc, it goes from SENT_ADD_ACK_REVOCATION: */
RECV_REMOVE_HTLC = REMOVING + OURS + LOCAL(PENDING)
+ LOCAL(COMMITTED) + REMOTE(COMMITTED),
RECV_REMOVE_COMMIT = RECV_REMOVE_HTLC - LOCAL(PENDING) - LOCAL(COMMITTED),
SENT_REMOVE_REVOCATION = RECV_REMOVE_COMMIT + LOCAL(REVOKED),
SENT_REMOVE_ACK_COMMIT = SENT_REMOVE_REVOCATION - REMOTE(COMMITTED),
RECV_REMOVE_ACK_REVOCATION = SENT_REMOVE_ACK_COMMIT - REMOVING + REMOTE(REVOKED),
/* When they add a new htlc, it goes in this order. */
RECV_ADD_HTLC = ADDING + THEIRS + LOCAL(PENDING),
RECV_ADD_COMMIT = RECV_ADD_HTLC - LOCAL(PENDING) + LOCAL(COMMITTED),
SENT_ADD_REVOCATION = RECV_ADD_COMMIT + LOCAL(REVOKED),
SENT_ADD_ACK_COMMIT = SENT_ADD_REVOCATION + REMOTE(COMMITTED),
RECV_ADD_ACK_REVOCATION = SENT_ADD_ACK_COMMIT + REMOTE(REVOKED),
/* When we remove an htlc, it goes from RECV_ADD_ACK_REVOCATION: */
SENT_REMOVE_HTLC = REMOVING + THEIRS + REMOTE(PENDING)
+ LOCAL(COMMITTED) + REMOTE(COMMITTED),
SENT_REMOVE_COMMIT = SENT_REMOVE_HTLC - REMOTE(PENDING) - REMOTE(COMMITTED),
RECV_REMOVE_REVOCATION = SENT_REMOVE_COMMIT + REMOTE(REVOKED),
RECV_REMOVE_ACK_COMMIT = RECV_REMOVE_REVOCATION - LOCAL(COMMITTED),
SENT_REMOVE_ACK_REVOCATION = RECV_REMOVE_ACK_COMMIT + LOCAL(REVOKED) - REMOVING
};
static const char *htlc_statename(enum htlc_state state)
{
switch (state) {
case NONEXISTENT: return "NONEXISTENT";
case SENT_ADD_HTLC: return "SENT_ADD_HTLC";
case SENT_ADD_COMMIT: return "SENT_ADD_COMMIT";
case RECV_ADD_REVOCATION: return "RECV_ADD_REVOCATION";
case RECV_ADD_ACK_COMMIT: return "RECV_ADD_ACK_COMMIT";
case SENT_ADD_ACK_REVOCATION: return "SENT_ADD_ACK_REVOCATION";
case RECV_REMOVE_HTLC: return "RECV_REMOVE_HTLC";
case RECV_REMOVE_COMMIT: return "RECV_REMOVE_COMMIT";
case SENT_REMOVE_REVOCATION: return "SENT_REMOVE_REVOCATION";
case SENT_REMOVE_ACK_COMMIT: return "SENT_REMOVE_ACK_COMMIT";
case RECV_REMOVE_ACK_REVOCATION: return "RECV_REMOVE_ACK_REVOCATION";
case RECV_ADD_HTLC: return "RECV_ADD_HTLC";
case RECV_ADD_COMMIT: return "RECV_ADD_COMMIT";
case SENT_ADD_REVOCATION: return "SENT_ADD_REVOCATION";
case SENT_ADD_ACK_COMMIT: return "SENT_ADD_ACK_COMMIT";
case RECV_ADD_ACK_REVOCATION: return "RECV_ADD_ACK_REVOCATION";
case SENT_REMOVE_HTLC: return "SENT_REMOVE_HTLC";
case SENT_REMOVE_COMMIT: return "SENT_REMOVE_COMMIT";
case RECV_REMOVE_REVOCATION: return "RECV_REMOVE_REVOCATION";
case RECV_REMOVE_ACK_COMMIT: return "RECV_REMOVE_ACK_COMMIT";
case SENT_REMOVE_ACK_REVOCATION: return "SENT_REMOVE_ACK_REVOCATION";
}
return tal_fmt(NULL, "UNKNOWN STATE %i", state);
}
static const char *htlc_stateflags(const tal_t *ctx, enum htlc_state state)
{
char *flags = tal_strdup(ctx, "");
#define ADD_STATE(flags, flag) \
if (state & flag) \
tal_append_fmt(&flags, #flag ",");
ADD_STATE(flags, ADDING);
ADD_STATE(flags, REMOVING);
ADD_STATE(flags, OURS);
ADD_STATE(flags, THEIRS);
ADD_STATE(flags, LOCAL(PENDING));
ADD_STATE(flags, LOCAL(COMMITTED));
ADD_STATE(flags, LOCAL(REVOKED));
ADD_STATE(flags, REMOTE(PENDING));
ADD_STATE(flags, REMOTE(COMMITTED));
ADD_STATE(flags, REMOTE(REVOKED));
if (strends(flags, ","))
flags[strlen(flags)-1] = '\0';
return flags;
}
struct htlc {
enum htlc_state state;
/* 0 means this is actually a new fee, not a HTLC. */
unsigned int id;
};
static u32 htlc_mask(unsigned int htlc)
{
if (htlc > 32)
errx(1, "HTLC number %u too large", htlc);
if (!htlc)
errx(1, "HTLC number can't be zero");
return (1U << (htlc-1));
}
/* Make commit tx for local/remote */
static struct commit_tx make_commit_tx(struct htlc **htlcs, int local_or_remote)
{
size_t i, n = tal_count(htlcs);
int committed_flag = SIDE(COMMITTED, local_or_remote);
struct commit_tx tx = { 0, 0, 0 };
for (i = 0; i < n; i++) {
if (!(htlcs[i]->state & committed_flag))
continue;
if (!(htlcs[i]->state & SIDE(OWNER, local_or_remote))) {
/* We don't apply fee changes to each other. */
if (htlcs[i]->id)
tx.outhtlcs |= htlc_mask(htlcs[i]->id);
} else {
if (!htlcs[i]->id)
tx.fee++;
else
tx.inhtlcs |= htlc_mask(htlcs[i]->id);
}
}
return tx;
}
struct peer {
int infd, outfd, cmdfd, cmddonefd;
/* For drawing svg */
char *text;
char *io;
/* All htlcs. */
struct htlc **htlcs;
/* Last one is the one we're changing. */
struct commit_info *local, *remote;
};
static struct htlc *find_htlc(struct peer *peer, unsigned int htlc_id, int side)
{
size_t i, n = tal_count(peer->htlcs);
for (i = 0; i < n; i++) {
if ((peer->htlcs[i]->state & side)
&& peer->htlcs[i]->id == htlc_id)
return peer->htlcs[i];
}
return NULL;
}
static struct htlc *new_htlc(struct peer *peer, unsigned int htlc_id, int side)
{
size_t n = tal_count(peer->htlcs);
/* Fee changes don't have to be unique. */
if (htlc_id && find_htlc(peer, htlc_id, side))
errx(1, "%s duplicate new htlc %u",
side == OURS ? "Our" : "Their", htlc_id);
tal_resize(&peer->htlcs, n+1);
peer->htlcs[n] = tal(peer, struct htlc);
peer->htlcs[n]->state = NONEXISTENT;
peer->htlcs[n]->id = htlc_id;
return peer->htlcs[n];
}
static void htlc_changestate(struct htlc *htlc,
enum htlc_state old,
enum htlc_state new)
{
if (htlc->state != old)
errx(1, "htlc was in state %s not %s",
htlc_statename(htlc->state), htlc_statename(old));
htlc->state = new;
}
struct state_table {
enum htlc_state from, to;
};
static bool change_htlcs_(struct peer *peer,
const struct state_table *table,
size_t n_table)
{
size_t i, n = tal_count(peer->htlcs);
bool changed = false;
for (i = 0; i < n; i++) {
size_t t;
for (t = 0; t < n_table; t++) {
if (peer->htlcs[i]->state == table[t].from) {
htlc_changestate(peer->htlcs[i],
table[t].from, table[t].to);
changed = true;
break;
}
}
}
return changed;
}
#define change_htlcs(peer, table) \
change_htlcs_((peer), (table), ARRAY_SIZE(table))
static struct commit_info *new_commit_info(const struct peer *peer,
struct commit_info *prev)
{
struct commit_info *ci = tal(peer, struct commit_info);
ci->prev = prev;
ci->revoked = false;
ci->counterparty_signed = false;
if (prev)
ci->number = prev->number + 1;
else
ci->number = 0;
return ci;
}
static struct signature commit_sig(const struct commit_tx *commit_tx)
{
struct signature sig;
sig.f = *commit_tx;
return sig;
}
static void write_out(int fd, const void *p, size_t len)
{
if (!write_all(fd, p, len))
err(1, "Writing to peer");
}
static void dump_htlcs(struct htlc **htlcs,
const char *prefix,
bool verbose,
int flags_inc, int flags_exc)
{
size_t i, n = tal_count(htlcs);
char *ctx = tal(htlcs, char);
bool printed = false;
for (i = 0; i < n; i++) {
if ((htlcs[i]->state & flags_inc) != flags_inc)
continue;
if (htlcs[i]->state & flags_exc)
continue;
if (!htlcs[i]->id && !verbose)
continue;
if (!printed) {
printf("%s", prefix);
printed = true;
}
if (!htlcs[i]->id)
printf(" FEE");
else
printf(" %u", htlcs[i]->id);
if (verbose) {
printf(" (%s - %s)",
htlc_statename(htlcs[i]->state),
htlc_stateflags(ctx, htlcs[i]->state));
}
}
if (printed)
printf("\n");
tal_free(ctx);
}
static void dump_commit_info(const struct peer *peer,
const struct commit_info *ci,
int local_or_remote)
{
struct commit_tx tx;
int committed_flag = SIDE(COMMITTED, local_or_remote);
tx = make_commit_tx(peer->htlcs, local_or_remote);
printf(" Commit %u:\n", ci->number);
dump_htlcs(peer->htlcs, " Our htlcs:", false,
OURS|committed_flag, 0);
dump_htlcs(peer->htlcs, " Their htlcs:", false,
THEIRS|committed_flag, 0);
/* Don't clutter output if fee level untouched. */
if (tx.fee)
printf(" Fee level %u\n", tx.fee);
dump_htlcs(peer->htlcs, "Pending unacked:", true,
SIDE(PENDING, local_or_remote), committed_flag);
dump_htlcs(peer->htlcs, "Pending acked:", true,
OTHER_SIDE(COMMITTED, local_or_remote), committed_flag);
if (ci->counterparty_signed)
printf(" SIGNED\n");
if (ci->revoked)
printf(" REVOKED\n");
fflush(stdout);
}
static void dump_peer(const struct peer *peer, bool all)
{
printf("LOCAL COMMIT:\n");
dump_commit_info(peer, peer->local, LOCAL_);
printf("REMOTE COMMIT:\n");
dump_commit_info(peer, peer->remote, REMOTE_);
if (all)
dump_htlcs(peer->htlcs, "OLD HTLCs:", true,
0, LOCAL(COMMITTED)|REMOTE(COMMITTED));
}
static void read_in(int fd, void *p, size_t len)
{
alarm(5);
if (!read_all(fd, p, len))
err(1, "Reading from peer");
alarm(0);
}
static void read_peer(struct peer *peer, const char *str, const char *cmd)
{
char *p = tal_arr(peer, char, strlen(str)+1);
read_in(peer->infd, p, strlen(str));
p[strlen(str)] = '\0';
if (!streq(p, str))
errx(1, "%s: Expected %s from peer, got %s", cmd, str, p);
tal_free(p);
}
static void send_offer(struct peer *peer, unsigned int htlc)
{
struct htlc *h = new_htlc(peer, htlc, OURS);
htlc_changestate(h, NONEXISTENT, SENT_ADD_HTLC);
tal_append_fmt(&peer->io, "add_htlc %u", htlc);
write_out(peer->outfd, "+", 1);
write_out(peer->outfd, &htlc, sizeof(htlc));
}
static void send_remove(struct peer *peer, unsigned int htlc)
{
struct htlc *h = find_htlc(peer, htlc, THEIRS);
if (!h)
errx(1, "send_remove: htlc %u does not exist", htlc);
htlc_changestate(h, RECV_ADD_ACK_REVOCATION, SENT_REMOVE_HTLC);
tal_append_fmt(&peer->io, "fulfill_htlc %u", htlc);
write_out(peer->outfd, "-", 1);
write_out(peer->outfd, &htlc, sizeof(htlc));
}
static void send_feechange(struct peer *peer)
{
struct htlc *fee = new_htlc(peer, 0, OURS);
htlc_changestate(fee, NONEXISTENT, SENT_ADD_HTLC);
tal_append_fmt(&peer->io, "update_fee");
write_out(peer->outfd, "F", 1);
}
/*
* We don't enforce the rule that commits have to wait for revoke response
* before the next one.
*/
static struct commit_info *last_unrevoked(struct commit_info *ci)
{
struct commit_info *next = NULL;
/* If this is already revoked, all are. */
if (ci->revoked)
return NULL;
/* Find revoked commit; one we hit before that was last unrevoked. */
for (; ci; next = ci, ci = ci->prev) {
if (ci->revoked)
break;
}
return next;
}
static void send_commit(struct peer *peer)
{
struct signature sig;
struct commit_tx commit_tx;
static const struct state_table changes[] = {
{ SENT_ADD_HTLC, SENT_ADD_COMMIT },
{ SENT_REMOVE_REVOCATION, SENT_REMOVE_ACK_COMMIT },
{ SENT_ADD_REVOCATION, SENT_ADD_ACK_COMMIT},
{ SENT_REMOVE_HTLC, SENT_REMOVE_COMMIT}
};
/* BOLT #2:
*
* An implementation MAY choose not to send an `update_commit`
* until it receives the `update_revocation` response to the
* previous `update_commit`, so there is only ever one
* unrevoked local commitment. */
if (peer->remote->prev && !peer->remote->prev->revoked)
errx(1, "commit: must wait for previous commit");
tal_append_fmt(&peer->io, "update_commit");
/* BOLT #2:
*
* A node MUST NOT send an `update_commit` message which does
* not include any updates.
*/
if (!change_htlcs(peer, changes))
errx(1, "commit: no changes to commit");
/* BOLT #2:
*
* A sending node MUST apply all remote acked and unacked
* changes except unacked fee changes to the remote commitment
* before generating `sig`.
*/
commit_tx = make_commit_tx(peer->htlcs, REMOTE_);
peer->remote = new_commit_info(peer, peer->remote);
peer->remote->counterparty_signed = true;
sig = commit_sig(&commit_tx);
/* Tell other side about commit and result (it should agree!) */
write_out(peer->outfd, "C", 1);
write_out(peer->outfd, &sig, sizeof(sig));
}
static void receive_revoke(struct peer *peer, u32 number)
{
static const struct state_table changes[] = {
{ SENT_ADD_COMMIT, RECV_ADD_REVOCATION },
{ SENT_REMOVE_ACK_COMMIT, RECV_REMOVE_ACK_REVOCATION },
{ SENT_ADD_ACK_COMMIT, RECV_ADD_ACK_REVOCATION },
{ SENT_REMOVE_COMMIT, RECV_REMOVE_REVOCATION }
};
struct commit_info *ci = last_unrevoked(peer->remote);
if (!ci)
errx(1, "receive_revoke: no commit to revoke");
if (ci->number != number)
errx(1, "receive_revoke: revoked %u but %u is next",
number, ci->number);
/* This shouldn't happen if we don't allow multiple commits. */
if (ci != peer->remote->prev)
errx(1, "receive_revoke: always revoke previous?");
tal_append_fmt(&peer->io, "<");
ci->revoked = true;
if (!ci->counterparty_signed)
errx(1, "receive_revoke: revoked unsigned commit?");
if (!change_htlcs(peer, changes))
errx(1, "receive_revoke: no changes?");
}
/* BOLT #2:
*
* the receiving node MUST add the HTLC addition to the unacked
* changeset for its local commitment.
*/
static void receive_offer(struct peer *peer, unsigned int htlc)
{
struct htlc *h = new_htlc(peer, htlc, THEIRS);
htlc_changestate(h, NONEXISTENT, RECV_ADD_HTLC);
tal_append_fmt(&peer->io, "<");
}
/* BOLT #2:
*
* the receiving node MUST add the HTLC fulfill/fail to the unacked
* changeset for its local commitment.
*/
static void receive_remove(struct peer *peer, unsigned int htlc)
{
struct htlc *h = find_htlc(peer, htlc, OURS);
if (!h)
errx(1, "recv_remove: htlc %u does not exist", htlc);
htlc_changestate(h, SENT_ADD_ACK_REVOCATION, RECV_REMOVE_HTLC);
tal_append_fmt(&peer->io, "<");
}
/* BOLT #2:
*
* the receiving node MUST add the fee change to the unacked changeset
* for its local commitment.
*/
static void receive_feechange(struct peer *peer)
{
struct htlc *fee = new_htlc(peer, 0, THEIRS);
htlc_changestate(fee, NONEXISTENT, RECV_ADD_HTLC);
tal_append_fmt(&peer->io, "<");
}
/* Send revoke.
* - Queue changes to them.
*/
static void send_revoke(struct peer *peer, struct commit_info *ci)
{
static const struct state_table changes[] = {
{ RECV_ADD_ACK_COMMIT, SENT_ADD_ACK_REVOCATION },
{ RECV_REMOVE_COMMIT, SENT_REMOVE_REVOCATION },
{ RECV_ADD_COMMIT, SENT_ADD_REVOCATION },
{ RECV_REMOVE_ACK_COMMIT, SENT_REMOVE_ACK_REVOCATION }
};
tal_append_fmt(&peer->io, "update_revocation");
/* We always revoke in order. */
assert(!ci->prev || ci->prev->revoked);
assert(ci->counterparty_signed);
assert(!ci->revoked);
ci->revoked = true;
if (!change_htlcs(peer, changes))
errx(1, "update_revocation: no changes?");
write_out(peer->outfd, "R", 1);
write_out(peer->outfd, &ci->number, sizeof(ci->number));
}
/* Receive commit:
* - Apply changes to us.
*/
static void receive_commit(struct peer *peer, const struct signature *sig)
{
struct commit_tx commit_tx;
struct signature oursig;
static const struct state_table changes[] = {
{ RECV_ADD_REVOCATION, RECV_ADD_ACK_COMMIT },
{ RECV_REMOVE_HTLC, RECV_REMOVE_COMMIT },
{ RECV_ADD_HTLC, RECV_ADD_COMMIT },
{ RECV_REMOVE_REVOCATION, RECV_REMOVE_ACK_COMMIT }
};
tal_append_fmt(&peer->io, "<");
/* BOLT #2:
*
* A node MUST NOT send an `update_commit` message which does
* not include any updates.
*/
if (!change_htlcs(peer, changes))
errx(1, "receive_commit: no changes to commit");
commit_tx = make_commit_tx(peer->htlcs, LOCAL_);
oursig = commit_sig(&commit_tx);
if (!structeq(sig, &oursig))
errx(1, "Commit state %#x/%#x/%u, they gave %#x/%#x/%u",
sig->f.inhtlcs, sig->f.outhtlcs, sig->f.fee,
oursig.f.inhtlcs, oursig.f.outhtlcs, oursig.f.fee);
peer->local = new_commit_info(peer, peer->local);
peer->local->counterparty_signed = true;
/* This is the one case where we send without a command. */
tal_append_fmt(&peer->text, "\n");
send_revoke(peer, peer->local->prev);
}
static void do_cmd(struct peer *peer)
{
char cmd[80];
int i;
unsigned int htlc;
struct commit_info *ci;
struct iovec iov[2];
i = read(peer->cmdfd, cmd, sizeof(cmd)-1);
if (cmd[i-1] != '\0')
errx(1, "Unterminated command");
if (i == 1) {
fflush(stdout);
exit(0);
}
peer->io = tal_strdup(peer, "");
peer->text = tal_strdup(peer->io, "");
if (sscanf(cmd, "offer %u", &htlc) == 1)
send_offer(peer, htlc);
else if (sscanf(cmd, "remove %u", &htlc) == 1)
send_remove(peer, htlc);
else if (streq(cmd, "feechange"))
send_feechange(peer);
else if (streq(cmd, "commit"))
send_commit(peer);
else if (streq(cmd, "recvrevoke")) {
u32 number;
read_peer(peer, "R", cmd);
read_in(peer->infd, &number, sizeof(number));
receive_revoke(peer, number);
} else if (streq(cmd, "recvoffer")) {
read_peer(peer, "+", cmd);
read_in(peer->infd, &htlc, sizeof(htlc));
receive_offer(peer, htlc);
} else if (streq(cmd, "recvremove")) {
read_peer(peer, "-", cmd);
read_in(peer->infd, &htlc, sizeof(htlc));
receive_remove(peer, htlc);
} else if (streq(cmd, "recvfeechange")) {
read_peer(peer, "F", cmd);
receive_feechange(peer);
} else if (streq(cmd, "recvcommit")) {
struct signature sig;
read_peer(peer, "C", cmd);
read_in(peer->infd, &sig, sizeof(sig));
receive_commit(peer, &sig);
} else if (streq(cmd, "checksync")) {
struct commit_tx ours, theirs;
ours = make_commit_tx(peer->htlcs, LOCAL_);
theirs = make_commit_tx(peer->htlcs, REMOTE_);
write_all(peer->cmddonefd, &ours, sizeof(ours));
write_all(peer->cmddonefd, &theirs, sizeof(theirs));
return;
} else if (streq(cmd, "dump")) {
dump_peer(peer, false);
} else if (streq(cmd, "dumpall")) {
dump_peer(peer, true);
} else
errx(1, "Unknown command %s", cmd);
iov[0].iov_base = peer->io;
iov[0].iov_len = strlen(peer->io)+1;
iov[1].iov_base = peer->text;
iov[1].iov_len = strlen(peer->text)+1;
writev(peer->cmddonefd, iov, 2);
tal_free(peer->io);
/* We must always have (at least one) signed, unrevoked commit. */
for (ci = peer->local; ci; ci = ci->prev) {
if (ci->counterparty_signed && !ci->revoked) {
return;
}
}
errx(1, "No signed, unrevoked commit!");
}
static void new_peer(int infdpair[2], int outfdpair[2], int cmdfdpair[2],
int cmddonefdpair[2])
{
struct peer *peer;
switch (fork()) {
case 0:
break;
case -1:
err(1, "Forking");
default:
return;
}
close(infdpair[1]);
close(outfdpair[0]);
close(cmdfdpair[1]);
close(cmddonefdpair[0]);
peer = tal(NULL, struct peer);
peer->htlcs = tal_arr(peer, struct htlc *, 0);
/* Create first, signed commit info. */
peer->local = new_commit_info(peer, NULL);
peer->local->counterparty_signed = true;
peer->remote = new_commit_info(peer, NULL);
peer->remote->counterparty_signed = true;
peer->infd = infdpair[0];
peer->outfd = outfdpair[1];
peer->cmdfd = cmdfdpair[0];
peer->cmddonefd = cmddonefdpair[1];
while (1)
do_cmd(peer);
}
struct sent {
int y;
const char *desc;
};
static void add_sent(struct sent **sent, int y, const char *msg)
{
size_t n = tal_count(*sent);
tal_resize(sent, n+1);
(*sent)[n].y = y;
(*sent)[n].desc = tal_strdup(*sent, msg);
}
static void draw_line(char **str,
int old_x, struct sent **sent, int new_x, int new_y)
{
size_t n = tal_count(*sent);
if (n == 0)
errx(1, "Receive without send?");
tal_append_fmt(str, "<line x1=\"%i\" y1=\"%i\" x2=\"%i\" y2=\"%i\" marker-end=\"url(#tri)\" stroke=\"black\" stroke-width=\"0.5\"/>\n",
old_x, (*sent)[0].y - LINE_HEIGHT/2,
new_x, new_y - LINE_HEIGHT/2);
tal_append_fmt(str, "<text text-anchor=\"middle\" "TEXT_STYLE" x=\"%i\" y=\"%i\">%s</text>\n",
(old_x + new_x) / 2,
((*sent)[0].y + new_y) / 2,
(*sent)[0].desc);
memmove(*sent, (*sent)+1, sizeof(**sent) * (n-1));
tal_resize(sent, n-1);
}
static void append_text(char **svg, bool is_a, int *y, char *text,
size_t *max_chars)
{
char *eol;
eol = strchr(text, '\n');
if (eol)
*eol = '\0';
tal_append_fmt(svg,
"<text x=\"%i\" y=\"%i\" text-anchor=\"%s\" "TEXT_STYLE">%s</text>",
is_a ? A_TEXTX : B_TEXTX, *y,
is_a ? "end" : "start",
text);
if (strlen(text) > *max_chars)
*max_chars = strlen(text);
if (eol) {
*y += LINE_HEIGHT;
append_text(svg, is_a, y, eol+1, max_chars);
}
*y += STEP_HEIGHT;
}
int main(int argc, char *argv[])
{
char cmd[80], output[200], *svg = tal_strdup(NULL, "");
int a_to_b[2], b_to_a[2], acmd[2], bcmd[2], adonefd[2], bdonefd[2];
int y = STEP_HEIGHT + LINE_HEIGHT;
struct sent *a_sent = tal_arr(NULL, struct sent, 0),
*b_sent = tal_arr(NULL, struct sent, 0);
bool output_svg = false;
size_t max_chars = 0;
err_set_progname(argv[0]);
if (argv[1] && streq(argv[1], "--svg"))
output_svg = true;
if (pipe(a_to_b) || pipe(b_to_a) || pipe(adonefd) || pipe(acmd))
err(1, "Creating pipes");
new_peer(a_to_b, b_to_a, acmd, adonefd);
if (pipe(bdonefd) || pipe(bcmd))
err(1, "Creating pipes");
new_peer(b_to_a, a_to_b, bcmd, bdonefd);
close(acmd[0]);
close(bcmd[0]);
close(adonefd[1]);
close(bdonefd[1]);
close(b_to_a[0]);
close(b_to_a[1]);
close(a_to_b[0]);
close(a_to_b[1]);
while (fgets(cmd, sizeof(cmd), stdin)) {
int cmdfd, donefd, r;
char *io, *text;
if (!strends(cmd, "\n"))
errx(1, "Truncated command");
cmd[strlen(cmd)-1] = '\0';
if (strstarts(cmd, "A:")) {
cmdfd = acmd[1];
donefd = adonefd[0];
} else if (strstarts(cmd, "B:")) {
cmdfd = bcmd[1];
donefd = bdonefd[0];
} else if (strstarts(cmd, "echo ")) {
if (!output_svg) {
printf("%s\n", cmd + 5);
fflush(stdout);
}
continue;
} else if (streq(cmd, "checksync")) {
struct commit_tx fa_us, fa_them, fb_us, fb_them;
if (!write_all(acmd[1], cmd, strlen(cmd)+1)
|| !write_all(bcmd[1], cmd, strlen(cmd)+1))
errx(1, "Failed writing command to peers");
alarm(5);
if (!read_all(adonefd[0], &fa_us, sizeof(fa_us))
|| !read_all(adonefd[0], &fa_them, sizeof(fa_them))
|| !read_all(bdonefd[0], &fb_us, sizeof(fb_us))
|| !read_all(bdonefd[0], &fb_them, sizeof(fb_them)))
errx(1, "Failed reading status from peers");
if (!structeq(&fa_us, &fb_them)
|| !structeq(&fa_them, &fb_us))
errx(1, "checksync: not equal");
continue;
} else if (strstarts(cmd, "#") || streq(cmd, ""))
continue;
else
errx(1, "Unknown command %s", cmd);
/* Don't dump if outputting svg. */
if (output_svg && strstarts(cmd+2, "dump"))
continue;
if (!write_all(cmdfd, cmd+2, strlen(cmd)-1))
errx(1, "Sending %s", cmd);
alarm(5);
r = read(donefd, output, sizeof(output)-2);
if (r <= 0)
errx(1, "Failed on cmd %s", cmd);
output[r] = output[r+1] = '\0';
io = output;
text = output + strlen(output) + 1;
if (r != strlen(text) + strlen(io) + 2)
errx(1, "Not nul-terminated: %s+%s gave %zi not %u",
io, text, strlen(text) + strlen(io) + 2, r);
alarm(0);
/* We can recv and send for recvcommit */
if (strstarts(io, "<")) {
if (strstarts(cmd, "A:"))
draw_line(&svg, B_LINEX, &b_sent, A_LINEX, y);
else
draw_line(&svg, A_LINEX, &a_sent, B_LINEX, y);
memmove(io, io+1, strlen(io));
}
if (!streq(io, "")) {
if (strstarts(cmd, "A:"))
add_sent(&a_sent, y, io);
else
add_sent(&b_sent, y, io);
}
if (!streq(text, "") && output_svg) {
append_text(&svg,
strstarts(cmd, "A:"),
&y,
text,
&max_chars);
}
}
write_all(acmd[1], "", 1);
write_all(bcmd[1], "", 1);
/* Make sure they've finished */
alarm(5);
if (read_all(adonefd[0], &y, 1)
|| read_all(bdonefd[0], &y, 1))
errx(1, "Response after sending exit command");
alarm(0);
if (output_svg)
printf("<svg width=\"%zu\" height=\"%u\">\n"
"<marker id=\"tri\" "
"viewBox=\"0 0 5 5\" refX=\"0\" refY=\"5\" "
"markerUnits=\"strokeWidth\" "
"markerWidth=\"4\" markerHeight=\"3\" "
"orient=\"auto\">"
"<path d=\"M 0 0 L 10 5 L 0 10 z\" />"
"</marker>"
"<text x=\"%i\" y=\"%i\" text-anchor=\"middle\">Node A</text>\n"
"<text x=\"%i\" y=\"%i\" text-anchor=\"middle\">Node B</text>\n"
"%s\n"
"</svg>\n",
B_TEXTX + max_chars*LETTER_WIDTH, y + LINE_HEIGHT,
A_LINEX, STEP_HEIGHT, B_LINEX, STEP_HEIGHT,
svg);
return 0;
}