diff --git a/Makefile b/Makefile index f225731a3..08c9cb9cb 100644 --- a/Makefile +++ b/Makefile @@ -216,6 +216,11 @@ test-onion4: test/test_onion test/onion_key test-protocol: test/test_protocol set -e; TMP=`mktemp`; [ -n "$(NO_VALGRIND)" ] || PREFIX="valgrind -q --error-exitcode=7"; for f in test/commits/*.script; do if ! $$PREFIX test/test_protocol < $$f > $$TMP; then echo "test/test_protocol < $$f FAILED" >&2; exit 1; fi; diff -u $$TMP $$f.expected; done; rm $$TMP +doc/protocol-%.svg: test/test_protocol + test/test_protocol --svg < test/commits/$*.script > $@ + +protocol-diagrams: $(patsubst %.script, doc/protocol-%.svg, $(notdir $(wildcard test/commits/*.script))) + check: daemon-tests test-onion test-protocol bitcoin-tests include bitcoin/Makefile diff --git a/test/test_protocol.c b/test/test_protocol.c index f0ac887b3..3f1077eec 100644 --- a/test/test_protocol.c +++ b/test/test_protocol.c @@ -7,12 +7,25 @@ #include #include #include +#include #include #include #include #include +#include #include +#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 funding { /* inhtlcs = htlcs they offered, outhtlcs = htlcs we offered */ u32 inhtlcs, outhtlcs; @@ -50,7 +63,11 @@ struct peer { /* Are we allowed to send another commit before receiving revoke? */ bool commitwait; - + + /* For drawing svg */ + char *text; + char *io; + /* Last one is the one we're changing. */ struct commit_info *us, *them; }; @@ -69,27 +86,47 @@ static bool have_htlc(u32 htlcs, unsigned int htlc) return htlcs & htlc_mask(htlc); } +static void PRINTF_FMT(2,3) add_text(struct peer *peer, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + tal_append_vfmt(&peer->text, fmt, ap); + va_end(ap); +} + /* Each side can add their own incoming HTLC, or close your outgoing HTLC. */ -static bool do_change(u32 *add, u32 *remove, u32 *fees, int htlc) +static bool do_change(struct peer *peer, const char *what, + u32 *add, u32 *remove, u32 *fees, int htlc) { /* We ignore fee changes from them: they only count when reflected * back to us via revocation. */ if (htlc == 0) { - if (fees) + if (fees) { + if (what) + add_text(peer, " FEE"); (*fees)++; + } else { + if (what) + add_text(peer, " (ignoring FEE)"); + } } else if (htlc < 0) { if (!have_htlc(*remove, -htlc)) return false; *remove &= ~htlc_mask(-htlc); + if (what) + add_text(peer, " -%u", -htlc); } else { if (have_htlc(*add, htlc)) return false; *add |= htlc_mask(htlc); + if (what) + add_text(peer, " +%u", htlc); } return true; } -static void add_change_internal(struct commit_info *ci, int **changes, int c) +static void add_change_internal(struct peer *peer, int **changes, int c) { size_t i, n = tal_count(*changes); @@ -103,6 +140,10 @@ static void add_change_internal(struct commit_info *ci, int **changes, int c) errx(1, "Already requestd htlc %+i", c); add: + if (c) + add_text(peer, "%+i", c); + else + add_text(peer, "FEE"); tal_resize(changes, n+1); (*changes)[n] = c; } @@ -111,12 +152,17 @@ add: * Once we're sending/receiving revoke, we queue the changes on the * alternate side. */ -static bool apply_changes_other(struct commit_info *ci, int *changes) +static bool apply_changes_other(struct peer *peer, + struct commit_info *ci, int *changes, + const char *what) { size_t i, n = tal_count(changes); + if (n) + add_text(peer, "Changes to %s:", what); + for (i = 0; i < n; i++) { - if (!do_change(&ci->funding_next.outhtlcs, + if (!do_change(peer, what, &ci->funding_next.outhtlcs, &ci->funding_next.inhtlcs, &ci->funding_next.fee, changes[i])) @@ -129,12 +175,17 @@ static bool apply_changes_other(struct commit_info *ci, int *changes) /* * Normally, we add incoming changes. */ -static bool add_incoming_change(struct commit_info *ci, int c) +static bool add_incoming_change(struct peer *peer, + struct commit_info *ci, int c, const char *who) { - if (!do_change(&ci->funding_next.inhtlcs, &ci->funding_next.outhtlcs, + if (!do_change(NULL, NULL, + &ci->funding_next.inhtlcs, &ci->funding_next.outhtlcs, NULL, c)) return false; - add_change_internal(ci, &ci->changes_incoming, c); + + add_text(peer, "Queue "); + add_change_internal(peer, &ci->changes_incoming, c); + add_text(peer, " to %s", who); return true; } @@ -155,10 +206,30 @@ static struct commit_info *new_commit_info(const struct peer *peer, } /* We duplicate the commit info, with the changes applied. */ -static struct commit_info *apply_changes(const struct peer *peer, - struct commit_info *old) +static struct commit_info *apply_changes(struct peer *peer, + struct commit_info *old, + const char *who) { - return new_commit_info(peer, old); + struct commit_info *next = new_commit_info(peer, old); + size_t i; + + add_text(peer, "%s:[", who); + if (old->funding_next.inhtlcs) + add_text(peer, "in"); + for (i = 1; i <= 32; i++) + if (have_htlc(old->funding_next.inhtlcs, i)) + add_text(peer, " %zu", i); + if (old->funding_next.outhtlcs) + add_text(peer, "%sout", + old->funding_next.inhtlcs ? ", " : ""); + for (i = 1; i <= 32; i++) + if (have_htlc(old->funding_next.outhtlcs, i)) + add_text(peer, " %zu", i); + if (old->funding_next.fee) + add_text(peer, " fee %u", old->funding_next.fee); + add_text(peer, "]"); + + return next; } static struct signature commit_sig(const struct commit_info *ci) @@ -260,8 +331,9 @@ static void read_peer(struct peer *peer, const char *str, const char *cmd) */ static void send_offer(struct peer *peer, unsigned int htlc) { + tal_append_fmt(&peer->io, "add_htlc %u", htlc); /* Can't have sent already. */ - if (!add_incoming_change(peer->them, htlc)) + if (!add_incoming_change(peer, peer->them, htlc, "them")) errx(1, "offer: already offered %u", htlc); write_out(peer->outfd, "+", 1); write_out(peer->outfd, &htlc, sizeof(htlc)); @@ -272,8 +344,9 @@ static void send_offer(struct peer *peer, unsigned int htlc) */ static void send_remove(struct peer *peer, unsigned int htlc) { + tal_append_fmt(&peer->io, "fulfill_htlc %u", htlc); /* Can't have removed already. */ - if (!add_incoming_change(peer->them, -htlc)) + if (!add_incoming_change(peer, peer->them, -htlc, "them")) errx(1, "remove: already removed of %u", htlc); write_out(peer->outfd, "-", 1); write_out(peer->outfd, &htlc, sizeof(htlc)); @@ -284,7 +357,8 @@ static void send_remove(struct peer *peer, unsigned int htlc) */ static void send_feechange(struct peer *peer) { - if (!add_incoming_change(peer->them, 0)) + tal_append_fmt(&peer->io, "update_fee"); + if (!add_incoming_change(peer, peer->them, 0, "them")) errx(1, "INTERNAL: failed to change fee"); write_out(peer->outfd, "F", 1); } @@ -325,7 +399,8 @@ static void send_commit(struct peer *peer) && peer->them->prev && !peer->them->prev->revoked) errx(1, "commit: must wait for previous commit"); - peer->them = apply_changes(peer, peer->them); + tal_append_fmt(&peer->io, "update_commit"); + peer->them = apply_changes(peer, peer->them, "THEM"); sig = commit_sig(peer->them); peer->them->counterparty_signed = true; @@ -351,12 +426,13 @@ static void receive_revoke(struct peer *peer, u32 number) if (peer->commitwait && ci != peer->them->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?"); /* The changes we sent with that commit, add them to us. */ - if (!apply_changes_other(peer->us, ci->changes_incoming)) + if (!apply_changes_other(peer, peer->us, ci->changes_incoming, "us")) errx(1, "receive_revoke: could not add their changes to us"); /* Cleans up dump output now we've consumed them. */ @@ -369,7 +445,8 @@ static void receive_revoke(struct peer *peer, u32 number) */ static void receive_offer(struct peer *peer, unsigned int htlc) { - if (!add_incoming_change(peer->us, htlc)) + tal_append_fmt(&peer->io, "<"); + if (!add_incoming_change(peer, peer->us, htlc, "us")) errx(1, "receive_offer: already offered of %u", htlc); } @@ -378,7 +455,8 @@ static void receive_offer(struct peer *peer, unsigned int htlc) */ static void receive_remove(struct peer *peer, unsigned int htlc) { - if (!add_incoming_change(peer->us, -htlc)) + tal_append_fmt(&peer->io, "<"); + if (!add_incoming_change(peer, peer->us, -htlc, "us")) errx(1, "receive_remove: already removed %u", htlc); } @@ -387,7 +465,8 @@ static void receive_remove(struct peer *peer, unsigned int htlc) */ static void receive_feechange(struct peer *peer) { - if (!add_incoming_change(peer->us, 0)) + tal_append_fmt(&peer->io, "<"); + if (!add_incoming_change(peer, peer->us, 0, "us")) errx(1, "INTERNAL: failed to change fee"); } @@ -396,6 +475,8 @@ static void receive_feechange(struct peer *peer) */ static void send_revoke(struct peer *peer, struct commit_info *ci) { + tal_append_fmt(&peer->io, "update_revocation"); + /* We always revoke in order. */ assert(!ci->prev || ci->prev->revoked); assert(ci->counterparty_signed); @@ -403,7 +484,8 @@ static void send_revoke(struct peer *peer, struct commit_info *ci) ci->revoked = true; /* Queue changes. */ - if (!apply_changes_other(peer->them, ci->changes_incoming)) + if (!apply_changes_other(peer, peer->them, ci->changes_incoming, + "them")) errx(1, "Failed queueing changes to them for send_revoke"); /* Clean up for dump output. */ @@ -426,13 +508,18 @@ static void receive_commit(struct peer *peer, const struct signature *sig) && !peer->us->changes_outgoing) errx(1, "receive_commit: no changes to commit"); - peer->us = apply_changes(peer, peer->us); + tal_append_fmt(&peer->io, "<"); + + peer->us = apply_changes(peer, peer->us, "US"); oursig = commit_sig(peer->us); 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->us->counterparty_signed = true; + + /* This is the one case where we send without a command. */ + tal_append_fmt(&peer->text, "\n"); send_revoke(peer, peer->us->prev); } @@ -442,16 +529,19 @@ static void do_cmd(struct peer *peer) int i; unsigned int htlc; struct commit_info *ci; + struct iovec iov[2]; - for (i = 0; i < sizeof(cmd); i++) { - if (!read_all(peer->cmdfd, cmd+i, 1)) - err(1, "Reading command pipe"); - if (!cmd[i]) - break; + i = read(peer->cmdfd, cmd, sizeof(cmd)-1); + if (cmd[i-1] != '\0') + errx(1, "Unterminated command"); + + if (i == 1) { + fflush(stdout); + exit(0); } - if (i == 0) - exit(0); + peer->io = tal_strdup(peer, ""); + peer->text = tal_strdup(peer->io, ""); if (sscanf(cmd, "offer %u", &htlc) == 1) send_offer(peer, htlc); @@ -497,10 +587,16 @@ static void do_cmd(struct peer *peer) } 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->us; ci; ci = ci->prev) { if (ci->counterparty_signed && !ci->revoked) { - write(peer->cmddonefd, "", 1); return; } } @@ -546,13 +642,77 @@ static void new_peer(int infdpair[2], int outfdpair[2], int cmdfdpair[2], 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, "\n", + old_x, (*sent)[0].y - LINE_HEIGHT/2, + new_x, new_y - LINE_HEIGHT/2); + tal_append_fmt(str, "%s\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, + "%s", + 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]; + 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"); @@ -573,12 +733,13 @@ int main(int argc, char *argv[]) close(a_to_b[1]); while (fgets(cmd, sizeof(cmd), stdin)) { - int cmdfd, donefd; + 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]; @@ -586,8 +747,10 @@ int main(int argc, char *argv[]) cmdfd = bcmd[1]; donefd = bdonefd[0]; } else if (strstarts(cmd, "echo ")) { - printf("%s\n", cmd + 5); - fflush(stdout); + if (!output_svg) { + printf("%s\n", cmd + 5); + fflush(stdout); + } continue; } else if (streq(cmd, "checksync")) { struct funding fa_us, fa_them, fb_us, fb_them; @@ -609,14 +772,74 @@ int main(int argc, char *argv[]) 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); - if (!read_all(donefd, &cmdfd, 1)) + 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("\n" + "" + "" + "" + "Node A\n" + "Node B\n" + "%s\n" + "\n", + B_TEXTX + max_chars*LETTER_WIDTH, y + LINE_HEIGHT, + A_LINEX, STEP_HEIGHT, B_LINEX, STEP_HEIGHT, + svg); + return 0; }