diff --git a/Makefile b/Makefile index d48623b49..72adb6dd3 100644 --- a/Makefile +++ b/Makefile @@ -112,7 +112,7 @@ lightning.pb-c.c lightning.pb-c.h: lightning.proto $(PROTOCC) lightning.proto --c_out=. $(TEST_CLI_PROGRAMS): % : %.o $(HELPER_OBJS) $(BITCOIN_OBJS) $(CCAN_OBJS) libsecp256k1.a -$(TEST_PROGRAMS): % : %.o $(BITCOIN_OBJS) $(CCAN_OBJS) $(CCAN_EXTRA_OBJS) libsecp256k1.a +$(TEST_PROGRAMS): % : %.o $(BITCOIN_OBJS) $(CCAN_OBJS) $(CCAN_EXTRA_OBJS) version.o libsecp256k1.a $(PROGRAMS:=.o) $(HELPER_OBJS): $(HEADERS) $(CCAN_OBJS) $(HELPER_OBJS) $(PROGRAM_OBJS) $(BITCOIN_OBJS) $(CDUMP_OBJS): ccan/config.h @@ -126,6 +126,23 @@ doc/deployable-lightning.pdf: doc/deployable-lightning.lyx doc/bitcoin.bib doc/deployable-lightning.tex: doc/deployable-lightning.lyx lyx -E latex $@ $< +state-diagrams: doc/normal-states.svg doc/simplified-states.svg doc/error-states.svg doc/full-states.svg + +%.svg: %.dot + dot -Tsvg $< > $@ || (rm -f $@; false) + +doc/simplified-states.dot: test/test_state_coverage + test/test_state_coverage --dot --dot-simplify > $@ + +doc/normal-states.dot: test/test_state_coverage + test/test_state_coverage --dot > $@ + +doc/error-states.dot: test/test_state_coverage + test/test_state_coverage --dot-all --dot-include-errors > $@ + +doc/full-states.dot: test/test_state_coverage + test/test_state_coverage --dot-all --dot-include-errors --dot-include-nops > $@ + gen_version.h: FORCE @(echo "#define VERSION \"`git describe --always --dirty`\"" && echo "#define VERSION_NAME \"$(NAME)\"" && echo "#define BUILD_FEATURES \"$(FEATURES)\"") > $@.new @if cmp $@.new $@ >/dev/null 2>&2; then rm -f $@.new; else mv $@.new $@; fi diff --git a/test/test_state_coverage.c b/test/test_state_coverage.c index b90fb1986..09eeb1366 100644 --- a/test/test_state_coverage.c +++ b/test/test_state_coverage.c @@ -7,6 +7,8 @@ #include #include #include +#include +#include "version.h" static bool record_input_mapping(int b); #define MAPPING_INPUTS(b) \ @@ -15,6 +17,11 @@ static bool record_input_mapping(int b); #include "state.h" #include "gen_state_names.h" +static bool dot_simplify = false; +static bool dot_enable = false; +static bool dot_include_abnormal = false; +static bool dot_include_errors = false; +static bool dot_include_nops = false; static enum state_input *mapping_inputs; static bool do_decline; @@ -101,6 +108,41 @@ static bool situation_eq(const struct situation *a, const struct situation *b) return sdata_eq(&a->a, &b->a) && sdata_eq(&a->b, &b->b); } +struct dot_edge { + const char *oldstate, *newstate; + enum state_input i; + const char *pkt; +}; + +static const struct dot_edge *dot_edge_keyof(const struct dot_edge *dot_edge) +{ + return dot_edge; +} + +static size_t dot_edge_hash(const struct dot_edge *d) +{ + uint32_t pkthash; + + if (d->pkt) + pkthash = hash(d->pkt, strlen(d->pkt), d->i); + else + pkthash = d->i; + return hash_pointer(d->oldstate, hash_pointer(d->newstate, pkthash)); +} + +static bool dot_edge_eq(const struct dot_edge *a, const struct dot_edge *b) +{ + return a->oldstate == b->oldstate + && a->newstate == b->newstate + && a->i == b->i + && ((a->pkt == NULL && b->pkt == NULL) + || streq(a->pkt, b->pkt)); +} + +HTABLE_DEFINE_TYPE(struct dot_edge, + dot_edge_keyof, dot_edge_hash, dot_edge_eq, + edge_hash); + HTABLE_DEFINE_TYPE(struct situation, situation_keyof, situation_hash, situation_eq, sithash); @@ -114,6 +156,9 @@ struct hist { /* The different outputs. */ enum state_input *outputs; + + /* Edges for the dot graph, if any. */ + struct edge_hash edges; }; static const char *state_name(enum state s) @@ -487,6 +532,7 @@ static void copy_peers(struct state_data *dst, struct state_data *peer, /* Recursion! */ static struct trail *run_peer(const struct state_data *sdata, + bool normalpath, bool errorpath, struct hist *hist); /* Returns false if we've been here before. */ @@ -681,8 +727,101 @@ static void record_output(enum state_input **outputs, enum state_input out) (*outputs)[n] = out; } +static bool error_path(enum state_input i, enum state src, enum state dst) +{ + return state_is_error(dst) || i == PKT_ERROR; +} + +static bool normal_path(enum state_input i, enum state src, enum state dst) +{ + if (error_path(i, src, dst)) + return false; + + /* Weird inputs. */ + if (i == BITCOIN_ANCHOR_TIMEOUT + || i == BITCOIN_ANCHOR_UNSPENT + || i == BITCOIN_ANCHOR_THEIRSPEND + || i == BITCOIN_ANCHOR_OTHERSPEND + || i == BITCOIN_STEAL_DONE + || i == PKT_UPDATE_DECLINE_HTLC + || i == PKT_UPDATE_ROUTEFAIL_HTLC + || i == PKT_UPDATE_TIMEDOUT_HTLC + || i == INPUT_CLOSE_COMPLETE_TIMEOUT) + return false; + + return true; +} + +/* These clutter the graph, so only handle from normal state. */ +static bool too_cluttered(enum state_input i, enum state src) +{ + if (i == CMD_CLOSE || i == PKT_CLOSE || i == PKT_UPDATE || i == PKT_UPDATE_ADD_HTLC || i == PKT_UPDATE_COMPLETE_HTLC) + return src != STATE_NORMAL_LOWPRIO + && src != STATE_NORMAL_HIGHPRIO; + return false; +} + +static void add_dot(struct edge_hash *hash, + const char *oldstate, + const char *newstate, + enum state_input i, + const Pkt *pkt) +{ + struct dot_edge *d = tal(NULL, struct dot_edge); + d->oldstate = oldstate; + d->newstate = newstate; + d->i = i; + if (pkt) + d->pkt = tal_strdup(d, (const char *)pkt); + else + d->pkt = NULL; + + if (edge_hash_get(hash, d)) + tal_free(d); + else + edge_hash_add(hash, d); +} + +static const char *simplify_state(enum state s) +{ + /* Turn all high prio into low prio, and merge some open states */ + switch (s) { + case STATE_OPEN_WAITING_OURANCHOR: + case STATE_OPEN_WAITING_THEIRANCHOR: + return "STATE_OPEN_WAITING"; + + case STATE_OPEN_WAIT_FOR_COMPLETE_OURANCHOR: + case STATE_OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR: + return "STATE_OPEN_WAIT_FOR_COMPLETE"; + + case STATE_NORMAL_LOWPRIO: + case STATE_NORMAL_HIGHPRIO: + return "STATE_NORMAL"; + + case STATE_WAIT_FOR_HTLC_ACCEPT_LOWPRIO: + case STATE_WAIT_FOR_HTLC_ACCEPT_HIGHPRIO: + return "STATE_WAIT_FOR_HTLC_ACCEPT"; + + case STATE_WAIT_FOR_UPDATE_ACCEPT_LOWPRIO: + case STATE_WAIT_FOR_UPDATE_ACCEPT_HIGHPRIO: + return "STATE_WAIT_FOR_UPDATE_ACCEPT"; + + case STATE_WAIT_FOR_UPDATE_COMPLETE_LOWPRIO: + case STATE_WAIT_FOR_UPDATE_COMPLETE_HIGHPRIO: + return "STATE_WAIT_FOR_UPDATE_COMPLETE"; + + case STATE_WAIT_FOR_UPDATE_SIG_LOWPRIO: + case STATE_WAIT_FOR_UPDATE_SIG_HIGHPRIO: + return "STATE_WAIT_FOR_UPDATE_SIG"; + + default: + return state_name(s); + } +} + static struct trail *try_input(const struct state_data *sdata, enum state_input i, + bool normalpath, bool errorpath, struct hist *hist) { struct state_data copy, peer; @@ -698,6 +837,27 @@ static struct trail *try_input(const struct state_data *sdata, idata.pkt = (Pkt *)tal(effect, char); newstate = state(sdata->state, sdata, i, &idata, effect); + normalpath &= normal_path(i, sdata->state, newstate); + errorpath |= error_path(i, sdata->state, newstate); + + if (dot_enable + && (dot_include_abnormal || normalpath) + && (dot_include_errors || !errorpath) + && (dot_include_abnormal || !too_cluttered(i, sdata->state))) { + const char *oldstr, *newstr; + + /* Simplify folds high and low prio, skip "STATE_" */ + if (dot_simplify) { + oldstr = simplify_state(sdata->state) + 6; + newstr = simplify_state(newstate) + 6; + } else { + oldstr = state_name(sdata->state) + 6; + newstr = state_name(newstate) + 6; + } + if (newstr != oldstr || dot_include_nops) + add_dot(&hist->edges, oldstr, newstr, i, effect->send); + } + if (newstate == STATE_ERR_INTERNAL) return new_trail(i, sdata, newstate, effect, "Internal error"); @@ -743,9 +903,9 @@ static struct trail *try_input(const struct state_data *sdata, } /* Try inputs from here down. */ - t = run_peer(©, hist); + t = run_peer(©, normalpath, errorpath, hist); if (!t) - t = run_peer(&peer, hist); + t = run_peer(&peer, normalpath, errorpath, hist); if (!t) { tal_free(effect); return NULL; @@ -782,6 +942,7 @@ static void activate_event(struct state_data *sdata, enum state_input i) } static struct trail *run_peer(const struct state_data *sdata, + bool normalpath, bool errorpath, struct hist *hist) { struct state_data copy, peer; @@ -804,7 +965,7 @@ static struct trail *run_peer(const struct state_data *sdata, if (i != BITCOIN_ANCHOR_OTHERSPEND) copy.event_notifies &= ~(1ULL << i); activate_event(©, i); - t = try_input(©, i, hist); + t = try_input(©, i, normalpath, errorpath, hist); if (t) return t; copy.event_notifies = old_notifies; @@ -827,7 +988,8 @@ static struct trail *run_peer(const struct state_data *sdata, for (i = 0; i < sizeof(cmds) / sizeof(cmds[i]); i++) { copy.current_command = cmds[i]; - t = try_input(©, cmds[i], hist); + t = try_input(©, cmds[i], + normalpath, errorpath, hist); if (t) return t; } @@ -844,7 +1006,8 @@ static struct trail *run_peer(const struct state_data *sdata, if (copy.state != copy.deferred_state) { i = copy.deferred_pkt; copy.deferred_pkt = INPUT_NONE; - return try_input(©, i, hist); + return try_input(©, i, + normalpath, errorpath, hist); } /* Can't send anything until that's done. */ return NULL; @@ -857,7 +1020,7 @@ static struct trail *run_peer(const struct state_data *sdata, memmove(peer.outputs, peer.outputs + 1, sizeof(peer.outputs) - sizeof(peer.outputs[0])); peer.num_outputs--; - return try_input(©, i, hist); + return try_input(©, i, normalpath, errorpath, hist); } } return NULL; @@ -932,17 +1095,51 @@ static void report_trail(const struct trail *t) } } -int main(void) +int main(int argc, char *argv[]) { struct state_data a, b; unsigned int i; struct hist hist; struct trail *t; + err_set_progname(argv[0]); + + opt_register_noarg("--help|-h", opt_usage_and_exit, + "" + "Test lightning state machine", + "Print this message."); + opt_register_noarg("--dot", + opt_set_bool, &dot_enable, + "Output dot format for normal paths"); + opt_register_noarg("--dot-all", + opt_set_bool, &dot_include_abnormal, + "Output dot format for all non-error paths"); + opt_register_noarg("--dot-include-errors", + opt_set_bool, &dot_include_errors, + "Output dot format for error paths"); + opt_register_noarg("--dot-include-nops", + opt_set_bool, &dot_include_nops, + "Output dot format even for inputs which don't change state"); + opt_register_noarg("--dot-simplify", + opt_set_bool, &dot_simplify, + "Merge high and low priority states"); + opt_register_version(); + + opt_parse(&argc, argv, opt_log_stderr_exit); + if (dot_include_abnormal) + dot_enable = true; + if (dot_simplify && !dot_enable) + opt_usage_exit_fail("--dot-simplify needs --dot/--dot-all"); + if (dot_include_errors && !dot_enable) + opt_usage_exit_fail("--dot-include-errors needs --dot/--dot-all"); + if (dot_include_nops && !dot_enable) + opt_usage_exit_fail("--dot-include-nops needs --dot/--dot-all"); + /* Map the inputs tested in each state. */ hist.inputs_per_state = map_inputs(); sithash_init(&hist.sithash); hist.outputs = tal_arr(NULL, enum state_input, 0); + edge_hash_init(&hist.edges); /* Initialize universe. */ sdata_init(&a, &b, STATE_INIT_WITHANCHOR, "A"); @@ -951,7 +1148,7 @@ int main(void) abort(); /* Now, try each input in each state. */ - t = run_peer(&a, &hist); + t = run_peer(&a, true, false, &hist); if (t) { report_trail(t); exit(1); @@ -961,7 +1158,7 @@ int main(void) do_decline = true; sithash_init(&hist.sithash); sithash_update(&hist.sithash, &a); - t = run_peer(&a, &hist); + t = run_peer(&a, true, false, &hist); if (t) { report_trail(t); exit(1); @@ -1008,5 +1205,24 @@ int main(void) if (!find_output(hist.outputs, i)) warnx("Never sent output %s", input_name(i)); } + + if (dot_enable) { + struct dot_edge *d; + struct edge_hash_iter i; + + printf("digraph lightning {\n"); + for (d = edge_hash_first(&hist.edges, &i); + d; + d = edge_hash_next(&hist.edges, &i)) { + printf("%s -> %s ", d->oldstate, d->newstate); + if (!d->pkt) + printf("[label=\"<%s\"];\n", input_name(d->i)); + else { + printf("[label=\"<%s\\n>%s\"];\n", + input_name(d->i), d->pkt); + } + } + printf("}\n"); + } return 0; }