aboutsummaryrefslogtreecommitdiffstats
path: root/link/link-protocol.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-30 05:58:38 +0100
committereunhhu <[email protected]>2026-09-30 05:58:38 +0100
commit5ce7655d613689223050715ff3f8fe5c456ebf11 (patch)
tree03c55f96e604f6d42a102cbea7de73bc040e5a1c /link/link-protocol.c
parentb0bec448d30f198dc4817d71fc8c72654907a3a9 (diff)
downloadpure-5ce7655d613689223050715ff3f8fe5c456ebf11.tar.gz
pure-5ce7655d613689223050715ff3f8fe5c456ebf11.zip
Take a delivery's fields by name, dropping none
A message from another node was taken only in its input's exact shape (the link's STREAM shape, fields and order alike; over HTTP, every field). So a board whose nodes are upgraded one after the other stopped both ways until both were, and an output and its input whose fields differ only in order were refused for good (docs/design/014). An input now takes a message when it has every field the sender sends, by name, of the same type or one that holds its values (a u32 in a number, text in bytes); a field only the input has is 0 or empty. One that sends a field the input lacks is refused: taking it would drop the value, and with its number heard the value would never come again (two boards apart for good). So add fields to receivers first and take them from senders first, and nothing stops or is lost. The link carries a sender's fields in a new frame, FIELDS (0x0d: names, shape, u8 count, each {type, name length, name}): after a REFUSED for another shape the sender puts it in its packets (after OFFER, before the records, so a receiver that knows no FIELDS reads what it read before) until the stream is taken, the first time at once. A receiver seeing another shape waits for the packet's FIELDS before refusing, and asks its host with them (PureLinkHost.input takes them); REFUSED says 4 when the input lacks a field. Deliveries carry the fields and the host lays each record out anew (pure_module_takes, pure_module_recast, pure_module_fields, over pure_send_recast's rule). A stream whose sender starts again in another shape lets go what it held; the sender sends it again laid out anew. A refusal already queued for a stream now says the last reason judged, not the first. Over HTTP a delivery (X-Pure-Source) may lack fields; an unknown member is still refused. test-link named: a stream of another shape naming its fields is taken, 200 stored in 47 ms with every delivery naming them; an input that does not take them refuses (4) and the sender backs off. check.py server_fields: five outputs, three linked and two over HTTP, fields in another order taken, an input with a field more taking messages without it, one lacking a field refusing until it starts again with it, then taking all. server_upgrade's new receiver declares its fields in another order. 012 names the frame and the rules.
Diffstat (limited to 'link/link-protocol.c')
-rw-r--r--link/link-protocol.c136
1 files changed, 114 insertions, 22 deletions
diff --git a/link/link-protocol.c b/link/link-protocol.c
index 4fa5aab..169705f 100644
--- a/link/link-protocol.c
+++ b/link/link-protocol.c
@@ -19,7 +19,7 @@ enum { DATAGRAM_MOST = PURE_LINK_DATAGRAM_MOST, BODY_MOST = DATAGRAM_MOST - HEAD
* at least this much of it fits. */
enum { PART_LEAST = 64 };
enum { F_PADDING = 0, F_STREAM = 1, F_RECORD = 2, F_ACK = 3, F_STORED = 4, F_PING = 5, F_RECORDS = 6, F_CHALLENGE = 7,
- F_RESPONSE = 8, F_OFFER = 9, F_ACCEPT = 10, F_LOST = 11, F_REFUSED = 12 };
+ F_RESPONSE = 8, F_OFFER = 9, F_ACCEPT = 10, F_LOST = 11, F_REFUSED = 12, F_FIELDS = 13 };
/* STORED's flags: what came after a gap was not kept; the app is full (no
* room for more now), and what came is kept for when it has room. */
enum { STORED_GAP = 1, STORED_FULL = 2 };
@@ -307,6 +307,11 @@ struct PureLinkSender {
* answer with no REFUSED. */
unsigned refusal, tried;
uint64_t refused_until, refused_wait, tried_pn;
+ /* The fields its records are laid out with, named (FIELDS) in each
+ * packet from a refusal for another shape until the stream is taken. */
+ unsigned char *fields;
+ size_t fields_size;
+ bool naming;
PureLinkCounts counts;
/* Epochs (forward secrecy). Keys come from the secret of the epoch it
* sends in (epoch 0's is the two nodes' static secret), each made from
@@ -696,6 +701,7 @@ static void on_stored(PureLinkSender *l, const unsigned char *p, size_t size, ui
unsigned flags = p[names + 8];
uint32_t credit = get32(p + names + 9);
l->refusal = l->tried = 0, l->refused_until = l->refused_wait = 0; /* the stream is taken, and stored */
+ l->naming = false;
if (heard > l->stored) {
while (l->count && record_at(l, 0)->seq <= heard) {
Record *r = record_at(l, 0);
@@ -725,8 +731,12 @@ static void on_stored(PureLinkSender *l, const unsigned char *p, size_t size, ui
static void refused(PureLinkSender *l, unsigned why, uint64_t now) {
if (l->refusal && l->tried != TRIED) return;
l->refusal = why ? why : PURE_LINK_NO_INPUT;
- l->refused_wait = l->refused_wait ? least(2 * l->refused_wait, REFUSED_MOST) : REFUSED_FIRST;
- l->refused_until = now + l->refused_wait;
+ /* Another shape, and its fields not named yet: named in a packet at once. */
+ bool naming = (why == PURE_LINK_OTHER_SHAPE || why == PURE_LINK_FIELDS_LACKED) && l->fields;
+ bool at_once = naming && !l->naming;
+ l->naming = l->naming || naming;
+ if (!at_once) l->refused_wait = l->refused_wait ? least(2 * l->refused_wait, REFUSED_MOST) : REFUSED_FIRST;
+ l->refused_until = at_once ? 0 : now + l->refused_wait;
l->tried = 0;
++l->counts.refused;
for (size_t k = 0; k < l->count; ++k) {
@@ -804,7 +814,8 @@ void pure_link_sender_take(PureLinkSender *l, const unsigned char *packet, size_
} else if (type != F_PING)
break; /* padding, or what this side does not know: the rest */
}
- if (l->refusal && l->tried == TRIED && !refusing) l->refusal = l->tried = 0, l->refused_until = 0; /* taken now */
+ if (l->refusal && l->tried == TRIED && !refusing) /* taken now */
+ l->refusal = l->tried = 0, l->refused_until = 0, l->naming = false;
}
/* A packet of what is not sent yet, in order, within the receiver's credit
@@ -831,6 +842,17 @@ static size_t fill(PureLinkSender *l, unsigned char *body, Flight *f, bool pad,
memcpy(body + at + 5, l->offer_public, PURE_SEAL_KEY);
at += EPOCH_FRAME;
}
+ /* The fields, for a receiver whose input takes another shape: after the
+ * frames one that knows no FIELDS reads, before the records. */
+ size_t names = 2 + strlen(l->source) + strlen(l->input);
+ if (l->naming && at + 1 + names + 4 + l->fields_size <= most) {
+ body[at++] = F_FIELDS;
+ at += put_names(body + at, l->source, l->input);
+ put32(body + at, l->shape);
+ at += 4;
+ memcpy(body + at, l->fields, l->fields_size);
+ at += l->fields_size;
+ }
if (l->renegotiating) { /* no record under the static keys */
l->responding = 0;
return at;
@@ -1196,6 +1218,15 @@ uint64_t pure_link_sender_stored(const PureLinkSender *l) { return l->stored; }
size_t pure_link_sender_held(const PureLinkSender *l) { return l->held; }
+bool pure_link_sender_fields(PureLinkSender *l, const unsigned char *fields, size_t size) {
+ unsigned char *kept = size ? malloc(size) : NULL;
+ if (size && !kept) return false;
+ if (size) memcpy(kept, fields, size);
+ free(l->fields);
+ l->fields = kept, l->fields_size = size;
+ return true;
+}
+
void pure_link_sender_counts(const PureLinkSender *l, PureLinkCounts *out) {
*out = l->counts;
out->stored = l->stored;
@@ -1213,6 +1244,7 @@ void pure_link_sender_free(PureLinkSender *l) {
explicit_bzero(l->epoch_secret, sizeof(l->epoch_secret));
explicit_bzero(l->before_secret, sizeof(l->before_secret));
explicit_bzero(l->offer_private, sizeof(l->offer_private));
+ free(l->fields);
free(l);
}
@@ -1298,6 +1330,8 @@ typedef struct Stream {
size_t held_first, held_count, held_capacity, held_bytes;
Run *run; /* the run that sent to it last: where STORED goes */
uint64_t run_id;
+ unsigned char *fields; /* the fields its records are laid out with, when its input took them by name */
+ size_t fields_size;
} Stream;
struct Active {
uint32_t peer; /* its place among the peers */
@@ -1720,6 +1754,7 @@ static void let_go(PureLinkReceiver *s, Active *a) {
while (a->streams[k]->held_count) drop_first(a->streams[k]);
free(a->streams[k]->held);
free(a->streams[k]->line);
+ free(a->streams[k]->fields);
free(a->streams[k]);
}
free(a->streams);
@@ -1845,30 +1880,56 @@ static bool allowed(const PureLinkReceiver *s, const Peer *peer, const char *sou
return false;
}
+/* Whose fields a stream's records are laid out with, kept (NULL, 0: its
+ * input's). False when memory runs out. */
+static bool keep_fields(Stream *st, const unsigned char *fields, size_t size) {
+ unsigned char *kept = fields && size ? malloc(size) : NULL;
+ if (fields && size && !kept) return false;
+ if (kept) memcpy(kept, fields, size);
+ free(st->fields);
+ st->fields = kept, st->fields_size = kept ? size : 0;
+ return true;
+}
+
/* The stream of a peer's source to input, made the first time: NULL when it
- * is refused (why: the source is not allowed from the peer, or the host has
- * no input that takes such messages), or memory runs out (why 0). */
+ * is refused (why: the source is not allowed from the peer, the host has no
+ * input that takes such messages, one of another shape unless it takes the
+ * fields named, which come with FIELDS), or memory runs out (why 0). A
+ * stream the sender now sends in another shape (it started again with
+ * another module) starts over with what it holds let go: the sender sends
+ * those again, laid out anew. */
static Stream *stream_of(PureLinkReceiver *s, Active *a, const char *source, const char *input, uint32_t shape,
- unsigned *why) {
+ const unsigned char *fields, size_t fields_size, unsigned *why) {
*why = 0;
- for (unsigned k = 0; k < a->stream_count; ++k) {
- Stream *st = a->streams[k];
- if (strcmp(st->input_name, input) || strcmp(st->source, source)) continue;
- if (st->shape != shape) *why = PURE_LINK_OTHER_SHAPE;
- return *why ? NULL : st;
- }
- int index = allowed(s, &s->peers[a->peer], source) ? s->host.input(s->host.context, input, shape)
- : -PURE_LINK_NOT_ALLOWED;
+ Stream *st = NULL;
+ for (unsigned k = 0; k < a->stream_count && !st; ++k)
+ if (!strcmp(a->streams[k]->input_name, input) && !strcmp(a->streams[k]->source, source)) st = a->streams[k];
+ if (st && st->shape == shape) return st;
+ int index = !allowed(s, &s->peers[a->peer], source) ? -PURE_LINK_NOT_ALLOWED :
+ s->host.input(s->host.context, input, shape, fields, fields ? fields_size : 0);
if (index < 0) {
- *why = index == -PURE_LINK_OTHER_SHAPE || index == -PURE_LINK_NOT_ALLOWED ? (unsigned)-index : PURE_LINK_NO_INPUT;
+ *why = index == -PURE_LINK_OTHER_SHAPE || index == -PURE_LINK_NOT_ALLOWED || index == -PURE_LINK_FIELDS_LACKED
+ ? (unsigned)-index : PURE_LINK_NO_INPUT;
return NULL;
}
+ if (st) { /* another shape now */
+ if (st->out) { *why = PURE_LINK_OTHER_SHAPE; return NULL; } /* its last delivery first */
+ if (!keep_fields(st, fields, fields_size)) return NULL;
+ while (st->held_count) drop_first(st);
+ st->line_head = st->line_size = 0;
+ st->line_count = 0;
+ st->tail = st->known = false;
+ st->input = (uint32_t)index;
+ st->shape = shape;
+ return st;
+ }
size_t capacity = a->stream_capacity;
- Stream **streams = room(a->streams, &capacity, a->stream_count, sizeof(*streams), 2), *st;
+ Stream **streams = room(a->streams, &capacity, a->stream_count, sizeof(*streams), 2);
if (!streams) return NULL;
a->streams = streams;
a->stream_capacity = (unsigned)capacity;
if (!(st = calloc(1, sizeof(*st)))) return NULL;
+ if (!keep_fields(st, fields, fields_size)) { free(st); return NULL; }
st->active = a;
st->input = (uint32_t)index;
st->shape = shape;
@@ -2023,7 +2084,8 @@ static bool hand_over(PureLinkReceiver *s, Stream *st) {
size_t bytes = st->line_size - st->line_head; /* the whole line, when it is a batch or less */
if (n > BATCH)
for (n = 0, bytes = 0; n < BATCH; ++n) bytes += 4 + (size_t)get32(st->line + st->line_head + bytes);
- PureLinkDelivery d = {st->input, st->source, st->line_first, n, st->line + st->line_head, bytes, st};
+ PureLinkDelivery d = {st->input, st->source, st->line_first, n, st->line + st->line_head, bytes, st,
+ st->fields, st->fields_size};
if (!s->host.deliver(s->host.context, &d)) return false;
st->out = true;
st->out_count = n, st->out_bytes = bytes;
@@ -2052,7 +2114,8 @@ static bool hand_over(PureLinkReceiver *s, Stream *st) {
memcpy(s->batch + at + 4, held_at(st, k)->bytes, held_at(st, k)->size);
at += 4 + held_at(st, k)->size;
}
- PureLinkDelivery d = {st->input, st->source, held_at(st, first)->seq, (unsigned)n, s->batch, bytes, st};
+ PureLinkDelivery d = {st->input, st->source, held_at(st, first)->seq, (unsigned)n, s->batch, bytes, st,
+ st->fields, st->fields_size};
if (!s->host.deliver(s->host.context, &d)) return false;
st->out = true;
++s->counts.deliveries;
@@ -2215,8 +2278,10 @@ static void lost(PureLinkReceiver *s, const Peer *peer, const Active *a, uint64_
* to its run: once a turn. */
static void refuse(PureLinkReceiver *s, Run *r, const unsigned char *names, size_t size, unsigned why) {
for (size_t k = 0; k < s->refusal_count; ++k)
- if (s->refusals[k].run == r && s->refusals[k].size == size + 2 && !memcmp(s->refusals[k].frame + 1, names, size))
+ if (s->refusals[k].run == r && s->refusals[k].size == size + 2 && !memcmp(s->refusals[k].frame + 1, names, size)) {
+ s->refusals[k].frame[1 + size] = (unsigned char)why; /* said once, as last judged (named fields, say) */
return;
+ }
void *more = room(s->refusals, &s->refusal_capacity, s->refusal_count, sizeof(*s->refusals), 4);
if (!more) return;
s->refusals = more;
@@ -2318,6 +2383,10 @@ static bool take_from(PureLinkReceiver *s, Peer *peer, const unsigned char *pack
Stream *st = NULL;
bool eliciting = false, records = false;
const unsigned char *offer = NULL; /* taken after the frames: whether records came with it */
+ /* A stream of another shape: refused at the end unless this packet's
+ * FIELDS names fields its input takes. */
+ const unsigned char *other = NULL;
+ size_t other_names = 0;
for (size_t at = 0; at < size;) {
unsigned type = plain[at++];
if (type == F_STREAM) {
@@ -2325,14 +2394,36 @@ static bool take_from(PureLinkReceiver *s, Peer *peer, const unsigned char *pack
size_t names = get_names(plain + at, size - at, source, input);
if (!names || at + names + 4 > size) break;
unsigned why;
- st = stream_of(s, a, source, input, get32(plain + at + names), &why);
+ st = stream_of(s, a, source, input, get32(plain + at + names), NULL, 0, &why);
if (st) {
st->run = r;
st->run_id = r->run;
- } else if (why)
+ } else if (why == PURE_LINK_OTHER_SHAPE)
+ other = plain + at, other_names = names;
+ else if (why)
refuse(s, r, plain + at, names, why);
at += names + 4;
eliciting = true;
+ } else if (type == F_FIELDS) {
+ char source[PURE_LINK_NAME + 1], input[PURE_LINK_NAME + 1];
+ size_t names = get_names(plain + at, size - at, source, input), length = 1;
+ if (!names || at + names + 5 > size) break;
+ const unsigned char *fields = plain + at + names + 4;
+ for (unsigned f = 0; f < fields[0] && length != SIZE_MAX; ++f) /* each {type, name length, name} */
+ length = at + names + 4 + length + 2 <= size ? length + 2 + fields[length + 1] : SIZE_MAX;
+ if (length == SIZE_MAX || at + names + 4 + length > size) break;
+ if (other && names == other_names && !memcmp(plain + at, other, names + 4)) {
+ unsigned why;
+ st = stream_of(s, a, source, input, get32(plain + at + names), fields, length, &why);
+ if (st) {
+ st->run = r;
+ st->run_id = r->run;
+ } else if (why)
+ refuse(s, r, other, other_names, why);
+ other = NULL;
+ }
+ at += names + 4 + length;
+ eliciting = true;
} else if (type == F_RECORD) {
if (at + RECORD_HEAD - 1 > size) break;
uint32_t length = get16(plain + at + 16);
@@ -2384,6 +2475,7 @@ static bool take_from(PureLinkReceiver *s, Peer *peer, const unsigned char *pack
} else
break; /* padding, or what this side does not know: the rest */
}
+ if (other) refuse(s, r, other, other_names, PURE_LINK_OTHER_SHAPE); /* its fields not named */
if (offer) offer_taken(s, a, r, get32(offer), offer + 4, e, !records, now);
if (eliciting) {
r->ack_due = true;