| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pure-serve --memory BYTES gives the app a budget of state bytes: its
lists' items as they are stored, its texts, the outbox and the numbers
heard (docs/design/013-memory-budget.md). The count is deterministic,
the same on any device. An event after which the states hold more than
the budget, and more than before it, fails with the fault memory and
changes nothing; HTTP answers 503 memory. One that frees or keeps them
goes on, so an app over its budget can always shrink.
Sends waiting for their receivers are state: a full outbox refuses the
next send until some are stored, which is the backpressure a slow
receiver puts on clients. A delivery refused for memory is not heard:
the link keeps it (c9c0edf) and HTTP senders try again after 503.
An answer (--reply) now takes its send out of the outbox in its own
event (pure_app_answer), so it has the room its send held. Apart, an app
whose outbox filled the budget could never apply an answer that keeps
anything, and the send leaves only after its answer: both would stay for
good. An answer that still does not fit waits, with the wire's after it,
and is tried again every 100 ms: pure_wire_answer leaves it in place
until pure_wire_answered.
Checks: test-module memory_budget (refused and undone, freeing over the
budget, a full outbox, a delivery not heard, answers that fit or wait,
across a restart); check.py server_memory (a sender refused 503 memory
with its receiver down and taking buys once it is up; a receiver
restarted below what it keeps, refusing 7 times in 2 s, then taking all)
and server_answers_memory (answers wait while receipts fill the budget,
then apply once each; the API called once per order). The stats report
state_bytes and memory_refused.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A link's delivery the app had no room for was handed over again at the
next turn: a receiver over its memory budget would refuse the same
records as fast as the owner could apply them. And its sender, seeing
records received but not stored for a second, sent the whole window
again every second.
Now a host says a delivery found it full (PureLinkDone.full). The
receiver keeps the records and hands them over again after a pause, 100
ms doubling to 500 ms, and meanwhile its STORED frames carry flag 2: the
app is full and what came is kept. A sender hearing it starts its
one-second wait for the save again, and sends none of it again. The
spec (012) says both sides; the pause is a local choice that must stay
under the sender's wait.
pure_link_done takes the PureLinkDone itself. test-link's new case keeps
the app full for 5 s: 12 hand-overs meanwhile, taken within 0.5 s of
having room, and no record sent again (21,640 without the flag).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
An app's snapshots formed a chain, each holding the next, and what the
app's lists let go while a snapshot was the newest went with it. So one
snapshot held for long held every later one and every node and text let
go since: a surface holding the snapshot of 20,000 items while their
notes were set again grew the node from 6.5 MB to 696 MB in 30 s, 3.4 KB
an update, without end.
A snapshot reads only what was made before it. Snapshots are now
numbered (their epoch, given to the lists' copies and so to each node
and text made after), and the ones held are linked under a register
they share. What the app lets go while a snapshot is its newest goes
with it, as before; when that one goes, what the newest older one still
held may read (made before its epoch) goes to it, and the rest back to
the system at once. The links move under a spin the register keeps; what
is freed is freed outside it. The same held snapshot now costs its
states once: 6.6 MB to 10.6 MB, flat after the notes diverge.
test-module's snapshot_held holds a snapshot through 40,000 events and
snapshots: none of the second 20,000's allocations stays (0 B; with the
chain's keeping, it fails). The kept-snapshots tests read eight held in
random order, and the module tests pass under AddressSanitizer too.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Three broke what data means (a walk's range missed the indices an insert
moves; an uncertain save of numbers alone let the next save through; an
output whose fields swapped names kept its waiting sends), four were
errors (a stateless timer never ticked, Vega's terminal lost a key cut
across reads, an input named edit forced documents, timers waited on the
wall clock). Each is fixed with a check that fails on the code before
and passes now, in Pure's and Vega's make check. The review's other
advice (a whole app's memory budget, one real app grown, comparisons with
their conditions set first) is what comes next.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
102: the spec (012) and a receiver written from it alone; the four holes
writing it found (REFUSED, the record's size, a peer's run places, the
memo's places beside) and the credit M6 showed fixed at 1,024 records;
M6 with ngtcp2 as a third, two rounds. Under reordering Pure carries the
most (45 Mbit/s; ngtcp2 29-47, quinn 1-2), under random loss the three
are the same, on a clean bottleneck Pure carries 3-5 % less (its framing,
some 112 B a packet to QUIC's 41), and where loss and reordering meet
ngtcp2 carries 1.3-3.6 times as much: undoing a loss it took for
reordering, it restores the window a real loss had cut too (its log: 22
of 34 undos past the window before the last cut; none without
reordering). Shares beside TCP stay within 25 %.
011 and the status table say so; 012 also says that a receiver answers
within 25 ms (a sender's PTO counts on it), that a shape and a record's
bytes are the app's, and why a packet of the past does no harm.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A receiver gave every stream a credit of 1,024 records past its last
stored, however small the records: a link carried at most 1,024 records
a round trip. On a path of 100 ms that is 10,000 records a second of
61 B, a ninth of what 50 Mbit/s carries (the path simulator: 9,953 a
second, 90,814 at 10 ms); and records of 1 KiB, a queue's worth of them
in flight, went a record or two a packet as each STORED let a few more
through (M6: 107,968 packets where 20 ms took 84,106).
A receiver bounds what it holds of a stream (16 MiB) apart, so its
credit is now what fits there at the size its last delivery's records
had: 1 to 65,536 (what a sender takes), 1,024 before any delivery. It
takes records that far past its last stored, as its memory lets it.
Simulator at 100 ms: 56,296 records a second of 61 B. M6 at 100 ms:
45.4 Mbit/s of 1 KiB records in 83,566 packets (44.9 in 107,968). The
window, not the credit, now fills the bottleneck's queue: losses there
where the credit hid them (61 B at 10 ms: 87,130 a second, 1,371 lost).
012 says so.
|
| |
|
|
|
|
|
|
|
| |
The usage showed --send OUTPUT=http[s]:// first; links are now the way
between nodes (012), so it shows pure:// for a Pure node and HTTP for
anything else. The statistics at exit count the streams a node's links
were refused (link_refused) and the ones its listener refused
(listener_refused), so a wiring mistake is in the numbers too, not
only the line on stderr; server_links_refused checks both.
|
| |
|
|
|
|
|
|
|
|
| |
pure-serve took a module with an input named show or edit for a
document app and refused it unless both were keyed messages: an app with
a plain message named edit compiled and then would not start (review
004, an earlier P2). Documents are what a module keeps in a keyed
collection; now the host serves them when the module declares one, and
inputs named show and edit are otherwise messages as any other.
check.py's server_plain_edit starts such an app and posts to edit.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
run_timers ticked nothing in an app without states, taking a timer to
be able to change nothing else; but a tick may send, and the outbox is
the app's state (the host gives such an app an owner for it). A timer
that only sends never ticked (review 004, F4). Now an app ticks when it
has states or outputs; one with neither runs on every connection thread
and has nothing a tick could do.
When a timer is due was reckoned on the wall clock: set back, a timer
waited for the time it had reached again (review 004, an earlier P2).
The next tick is now reckoned on the monotonic clock; events still see
the wall clock (now).
check.py's server_timers runs the review's heartbeat (a timer, an
output, no state) and sees it tick, then again with the wall clock set
back an hour 0.2 s in (build/clock-back.so, an LD_PRELOAD of
clock_gettime): 29 ticks in 0.6 s, where the server before this stops at
9.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Replacing a module, or starting one over a store, kept an output's
waiting sends when the module had the output with as many fields of the
same types. Their names were not compared, and the stored links kept the
types alone: an output whose two numbers swapped places read debit 10,
credit 20 as credit 10, debit 20, replaced and started again (review
004, F3). A send's bytes mean its fields' names and types in order; now
both are compared, in memory and in the store, whose links (PLK4) keep
each field's name. Links kept before (PLK3) have the types alone, as
before; the next save writes PLK4.
test-module's output_meaning: the swapped module is refused, replacing
and starting, while a send waits; trimmed, it replaces and its sends go
on; and the store then refuses the module before.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A save whose tree commit may or may not be stored leaves the app apart
from its store: no save may follow until it starts again from what the
store has. The app was marked so only when the save also logged a list's
change, so a save of numbers alone that came back uncertain let the next
event (a list's alone) save on the state the memory had rolled back to:
after a restart, count 1 and a row made from count 0 (review 004, F2).
Now any uncertain tree commit marks it; a failed one still takes back
the logged entry. test-module's uncertain_numbers does the review's
case: the list's event is refused, and the store is the commit's.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A walk whose where bounds a keyed list's key finds the bound once, when
the walk begins, if its body writes nothing the bound reads. The body's
writes counted only states stored and lists changed, not what those
changes move with them: an insert or remove moves the indices into the
list, a text's change the cursors in it. A walk bounded by an index
into a list its body inserts into saw the bound it began with: of five
rows it took one (review 004, F1). Those states now count as written,
and a bound that moves an index (whose list is whichever the index's
is) is kept only when the body changes no list.
build/module-probe runs a module twice from the same keys, as it loads
and with every range let go, and says which state first differs.
check.py's module_ranges runs the review's case (all rows twice: done
10, at 10) and a walk whose bound its body cannot move, still ranged.
The probe finds the difference in the runtime before this.
|
| |
|
|
|
|
|
|
| |
OpenSSL 3.5's ClientHello offers ML-KEM with X25519 and takes two
Initials; when the path brought the second first, ngtcp2's server was
asked for a Retry and the harness gave up. The client now offers X25519
alone, as Pure's links agree, and the server lets go of a connection it
may not keep, so the client's next Initial begins another.
|
| |
|
|
|
|
|
|
|
| |
drain.c takes a rate and seconds as quinn's client does (paced messages
stamped with the time they were written, and no more written past the
seconds), and its service writes quinn's line for each stream: the
messages read in each 100 ms and how long they took. 020's run.py runs
it as a third, on port 40005 behind the same bottleneck, and --who
picks whom to run.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A receiver's memo kept one run's epoch a peer. A node whose outputs
send to this one sends in a run each, so after the receiver started
again (or let the peer rest) all runs but the last accepted were told
LOST and began their epochs again: records under the static keys for a
round trip, and an X25519 each. Now a run whose peer's place holds
another run still sending keeps its epoch beside, in one of two of
1,024 places its peer and run find (160 KiB at the file's head: the
memo is PLM3, and a PLM2 one is begun again). A run's place beside goes
once its peer's place has it, so an epoch's secrets stay only until the
next one's are.
test-link's three outputs of one node, each in epoch 1, go on after the
receiver starts again: no LOST, no X25519 (before: two LOSTs, an
accept). 012 says so.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
012 says what goes on the wire, enough for another implementation: the
header, the key id, the version and why it is not negotiated on the
wire, the static secret, HKDF's salt and infos, the nonce, epochs'
chain, the thirteen frames byte by byte, what each side must do, the
values that are the spec's and those each side picks, the two words a
receiver says (LOST, REFUSED), what an attacker gets, and what is known
not to hold. A receiver written from it alone, in Python, links with a
pure-serve node through epoch 1 (check.py).
011's last row of its stop table is met: H1-H4 held, every check passes,
CPU within 10 % of QUIC at 61 B and 25 % under it from 1 KiB, the share
with TCP within 25 %, 3,097 lines of C. Links without connections are
now the default between Pure nodes; HTTP stays the boundary to others,
and relayed sealed sends stay as they are. 006, 009 and the README say
so.
|
| |
|
|
|
|
|
|
|
|
| |
check.py's server_links_independent is a receiver in Python (the
cryptography package, no code of link/) written from the link protocol's
description: the static secret, HKDF's keys, the nonces, the frames, a
CHALLENGE, ACK and STORED, and ACCEPT for epoch 1. A pure-serve node
links to it: every one of 200 orders is stored as the node knows it,
the first packet opened in epoch 0 and the other 199 in the epoch the
two made apart. None where the package is not there.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A receiver that took no records of a stream (a source the key may not
send as, no input of that name, an input of another shape) said nothing:
its sender heard them acknowledged, never stored, and sent them all
again every second, for ever, and its operator saw nothing. Now the
receiver answers REFUSED (0x0c: the names, and why), and the sender
sends no records for a second, then a packet of them to learn whether
they are taken now, the wait twice as long each time (64 s at most). A
refusal ends when that packet is acknowledged with no REFUSED beside, or
a record is stored. link.c says it once on stderr, as a wire says a
receiver does not take its sends; serve's input says which of the two
the app lacks.
test-link: each of the three is refused seven times in 90 s with nine
packets, and a stream refused until the app has its input goes on then,
all 200 records stored. check.py links a node to an input its receiver
lacks: said once, two packets in 2.5 s.
|
| |
|
|
|
|
|
|
|
|
|
| |
A receiver kept two runs a peer: the sender's, and the one before it
restarted. A node with three outputs to another sends in three runs with
one key, and they pushed one another out: each packet keyed its run
again, and an answer marked for one went to the run that took its place.
Now more places are made beside when the run seen longest ago is still
live (up to 64), and one is reused only once its run is quiet the idle
period. test-link's three outputs of one node key three runs, not seven,
and are all stored in 16 ms, not 1016.
|
| |
|
|
|
|
|
| |
A sender took sends up to 64 MiB, but a receiver holds 16 MiB of a
stream not yet stored, so a send between the two went again forever.
The most a send may be is now PURE_LINK_RECORD_MOST, what a receiver
holds; test-link sends one that large whole and is refused one byte more.
|
| |
|
|
|
|
|
|
|
| |
A path MTU black hole is seen and the link goes back to 1,200 bytes; a
receiver that lost its memo says LOST and M2 without a memo catches up
in 0.4-0.5 s, not 7.7; M5 stands beside ngtcp2 too (Pure 25 % lighter
than both QUIC from 1 KiB, 1.04 of ngtcp2 and 0.90 of quinn at 61 B);
and the stream's name in every packet, 096's guess for 61 B's gap, cost
nothing measurable, so it stays. 098 notes the one line LOST changes.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
ngtcp2 1.25.0 with OpenSSL 3.5's QUIC TLS (its ossl backend), fetched
and built into build/third by ngtcp2/build.sh; drain.c does in C what
the quinn harness does: one connection, one stream, "pure" and then the
messages each after its u32 length, counted by the service, packets in
GSO bursts. Its stream bytes are a function of their offset, so what
ngtcp2 keeps to send again points into a pattern that never changes.
run.py's drain_ngtcp2 measures both sides' CPU as drain_quic does.
Five rounds each, both sides' CPU a message: 61 B Pure 0.280, ngtcp2
0.270, quinn 0.310 us; 1 KiB 2.62, 3.54, 3.94; 16 KiB 40.2, 54.4, 60.2.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A RECORDS frame of records in order, with nothing held and no record's
tail waiting, went through hold, on_line and line_append a record: the
same checks each time (heard, credit, the line's end, held, the memory
bound). Now they are made once for the frame and its records are copied
onto the line one after another. The receiver's CPU a 61-byte record in
M5 went from 0.15 to 0.133-0.143 us; both sides together 0.28 us, beside
ngtcp2's 0.27 and quinn's 0.31 in the same rounds (the M5 host here also
compares every record with what was sent). test-link's results are the
same to the byte.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A receiver that started again without its memo could open none of its
senders' packets (their epochs were gone), said nothing, and each sender
waited 2 s and four probes, then offered with no records. Worse, those
packets failed to open, so each cost a token (256 a second, the flood
guard): ten thousand senders came back 256 a second. M2 without a memo
stopped 0.8 s and caught up in 7.7 s.
Now the receiver answers a packet of a run it does not know, in an epoch
past 0, with LOST: sealed in epoch 0 under the nodes' keys, naming the
packet. As QUIC's stateless reset, it is smaller than the packet and
comes out of a bucket of its own (4,096 a second). A sender that has that
packet out begins again as a run begins: epoch 0 with its records at
once, epoch 1 offered. Only the receiver can say LOST, and one of the past
names a packet no longer out; silence alone (an attacker's doing, maybe)
still gets offers only. Such a packet costs no key token: no keys were
made for it, and the secret made is remembered, once a sender.
M2 without a memo (ten thousand devices, twice): no 100 ms step stopped,
caught up in 0.4-0.5 s, p99 5.8-5.9 ms, the most 48-57 ms (was 4.1 and
7.8 s). test-link: a changed packet may get a LOST smaller than itself
and nothing else; half of a flood in epoch 1 got 267 LOSTs, 12 KB for its
6 MB; a record goes under the nodes' keys after an epoch began only after
the receiver forgot, until the next epoch.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
A link that found the path carries 1,452 bytes kept sending them when a
route changed and larger packets were dropped with nothing said (a
black hole): it lost them one after another and stood still (test-link:
8 of 3,000 records in ten minutes). Now, as RFC 8899 §4.3 has it, it goes
back to 1,200 bytes when three larger packets in a row are lost while an
acknowledgment covers only smaller ones, or when two probes in a row go
unanswered, and tries the larger size again a second on (three tries,
then ten minutes). The case completes in 1.2 s of virtual time; the M6
simulator's results are the same to the byte in eight paths of loss,
reordering and a bottleneck.
|
| |
|
|
|
|
|
|
|
| |
(096, 100)
61-byte records: 0.287 us a record against quinn's 0.31 in the same five
rounds (0.93 of it); 096's 1.06-1.12 was against quinn at 0.27, which
moves between 0.27 and 0.32 from day to day. H4 is no longer at the
edge.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Profiling both sides in one thread, without sockets (61-byte records):
the rings of records and flights divided for each place, the record a
number names was found three times over, a record in one part was
searched for its part, and the receiver walked the line record by record
to hand it over and again to let it go. The rings are powers of two long
and now masked; a record is found once; a one-part record's part is the
first; a line of a batch or less goes whole without a walk, and a
delivery applied whole lets its bytes go at once. The protocol alone:
0.171 to 0.162 us a record. M5 (the link and listener threads, sockets
too), five rounds against the commit before: 61 B 0.298 to 0.287 us, 1
KiB 2.63-2.90 to 2.48-2.79 us, 16 KiB the same or less; quinn measured
0.31, 3.85 and 60 us in the same rounds. test-link's results are the
same to the byte.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
H3 of design 011 stands: with ten thousand devices sending 2,000 records
a second, the node's store, key and memo carried to another address,
nothing stopped for a 100 ms step, the moved node made no X25519, and
every record was applied once. Each device's first record after the move
waits one probe (30-45 ms here), as its first packet finds no way there.
The search found the listener answering from an address other than the
one a packet came to, and a link that looked for its receiver after its
probe went, losing every answer. H1-H4 are all measured now; 61 B's CPU
a delivery is at the edge of the criterion, to be looked at before step
5 (adoption and a specification).
|
| |
|
|
|
|
|
|
|
|
| |
run.py --restart --move runs in namespaces of its own (unshare -rmn: a
network and an /etc/hosts): the devices send to receiver.test, which
names 127.0.0.2; at --kill-at the node is killed, its store, key and
memo copied elsewhere, 127.0.0.2 made unreachable, the name pointed at
127.0.0.3, and the copy started there. The load generator's devices
look for the node again as link/link.c does: after three probes in a
row unanswered, or a send the system said had no way there.
|
| |
|
|
|
|
|
|
|
|
| |
A send that fails with no route, nobody at the port, or the network
gone (ECONNREFUSED, EHOSTUNREACH, ENETUNREACH, ENETDOWN, EADDRNOTAVAIL)
now has the link look its receiver's name up again before its next
packet, not three unanswered probes later: a receiver moved away from an
address that is gone costs a sender one probe's wait, not three. Such a
failure no longer turns GSO off either (it said nothing of GSO). In the
check that moves a receiver, the sender sent 24 records again, not 243.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A link resolved its receiver's name once, at its start: a receiver whose
store moved to another machine (another address under the same name)
was never found, and a sender whose own network changed kept a socket
that no longer went anywhere. Now, when three probes in a row go
unanswered, and at each probe after (a second apart at most), the link
looks the name up again and sends from a new socket connected there.
It looks before the probe goes, so the probe's answer comes to the new
socket (looking after it lost every answer, one socket after another).
A sender at rest does not look: only probes unanswered count.
make check moves a receiver (H3 of docs/design/011-pure-transport.md):
in namespaces of its own, the sender links to pure://receiver.test at
127.0.0.2; the receiver is killed, its store, key and memo copied, the
old address made unreachable, the name pointed at 127.0.0.3 and the
copy started there. Every order is applied once, and the moved receiver
makes no X25519.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
The listener's socket takes packets to any address of the node, and its
answers went out from whichever address the system chose for the way
back. A sender whose socket is connected to another of the node's
addresses (a node with many, or a name that points at a second one)
takes answers only from the address it sent to: it heard none, and its
records went on arriving unacknowledged. Now the listener asks for the
address each packet came to (IP_PKTINFO; IPv4 as ::ffff:a.b.c.d on a
socket of both) and answers from it. The path a receiver keeps for a run
is both addresses (48 bytes, not 28): a packet that comes to another of
the node's addresses moves it, and is checked, as one from elsewhere is.
Found by a receiver moved to 127.0.0.2 (the check that follows).
|
| |
|
|
|
|
|
|
| |
The window, threshold, the start of recovery and CUBIC's four values were
fields of their own, and an episode of congestion events copied each of
the seven aside and back. Now they are one Congestion: an episode keeps
a copy, and undoing it takes the copy back, the window grown since kept.
test-link's results and the M6 simulator's are the same to the byte.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A receiver's run kept seventeen fields of epochs: numbers, flags, secrets
and keys for now, the one before, the one accepted, and an offer waiting.
Now it keeps three epochs (now, before, next), each its number, secret
and the keys of the sender's packets in it, and every move is one of two:
made from a secret, or become the epoch now (the one it was goes before,
the answers are sealed in the new one). Using the one accepted, a chain
begun again from epoch 0, and a run begun from the memo are these moves.
The key of an offer waiting is the key offered: a new offer lets the one
accepted before go at once. A sender's moves (an epoch begun, one taken
for lost, the first) are one too: its packets sealed in epoch n.
test-link's results are the same to the byte.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A sender that took its epoch for lost offers epoch 1 again with a new
key. The receiver accepts it in the turn, after the answers; until then
its answers still carried the ACCEPT of the earlier offer, which named
the same epoch. The sender began its epoch from that one, the two sides
held different secrets, and 2 s later it took the epoch for lost again:
a turn of that each time the order fell so. Now an ACCEPT carries the
first four bytes of the key it answers, and a sender begins its epoch
only from one that answers its own.
test-link cuts the path for 3 s the moment the sender begins an epoch,
three times; a sender's counts say how many epochs it took for lost,
and it must be three. Before, it was four.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Forward secrecy, 098's first task left: keys that leak later open only
epoch 0. M2 after the epoch began and after a restart is as before it
(p50 0.4 ms, p99 0.8 ms, the same CPU, no X25519 on restart). It costs a
p50 of 0.7-1.0 ms more while ten thousand devices first meet and for
about 40 s of accepts after, and a receiver that lost its memo now
stalls 0.8 s and catches up in 7.7 s, not 0.2-0.7: it lost the epochs,
and each sender opens one again. The search also found a packet more a
device from each of M6's path probe and the security checks' challenge,
and a load generator whose own pass over its devices was the floor of
the latency it measured. 098's note that epochs narrow KCI to the first
epoch was wrong; corrected.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
nothing past
A sender offers the public half of a pair made for the next epoch
(OFFER) until the receiver accepts with a half of its own (ACCEPT); their
X25519 and the epoch's secret make the next one's, and the private
halves go. The header's low four bits say a packet's epoch; epoch 0 is
the nodes' static secret, as the keys were. The first epoch is offered in
the run's first packet (it begins with the first answer when the
receiver is not busy), then one an hour or GiB. One who later has both
nodes' keys opens only epoch 0, and no record went there after an epoch
began: test-link tries every packet sent under the static keys, with
epochs a second long, loss, reordering, restarts and replays.
The receiver writes the epoch it accepted in its memo (a place is 256
bytes now), so it goes on without an X25519 when it starts again; one
that forgot is not heard from, and its senders, after 2 s and four
probes unanswered, offer again in epoch 0 with no records until an epoch
begins. Keys of four epochs are tried (accepted, now, before, zero), as
two may share the four bits.
An accept costs an X25519 on each side, 220 µs on this Pi: one of a
sender with records costs a token (256 a second, 8 at once) and the
next packet offers again when there is none; one of a sender with
nothing else to send waits for none, one a second for it. The X25519 is
made after the turn's answers, never between a packet and its answer.
serve's report and M2's generator count keys, X25519s and epochs.
|
| |
|
|
|
|
|
|
|
|
|
| |
The load generator asked all ten thousand senders for their timers every
5 ms: 83 % of its CPU (47 ns a sender, mostly cache misses), and records
were timed from the start of that pass, so the pass (0.75-0.94 ms, longer
as the sender grew) was the floor of every latency it measured. A device
now keeps its sender's next timer, asked again only when the sender is
touched (the only time it changes), and the p50 of M2 falls from 0.8 to
0.4 ms for every version alike. It also says the p50 and p99 of the
records made in each second, and the keys and X25519s its senders made.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
M2 (ten thousand devices, a record each every 5 s) sent 118k packets for
80k records. A link whose packets were never full still probed for a
1,452-byte path: nothing larger would have been carried. And a sender
said a receiver's challenge back in a packet of its own, one for each
device each time the receiver started. Now a link probes once a packet
of its was full, and a challenge rides in the next packet that goes
(records, or a probe). Until then the receiver's answers there are at
most three times what came, which the answers to one packet are not;
when they are held back, the sender's probe or record sent again says it
back. M2 sends 80,036 again (docs/research/099-link-epochs.md).
|
| |
|
|
|
|
|
|
| |
An epoch's keys (docs/design/011-pure-transport.md): a pair made for the
next epoch, held raw (a link may hold one for long and a node many, so
not in OpenSSL's 16 KiB secure heap), and the secret its private half
shares with the other side's public half, never all zeros. The crypto
table's version is 5.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
test-link now also plays an attacker: packets changed on the way,
packets of the past sent again from the sender's address and its own,
keys and sources not allowed, and a flood of made-up packets. It found:
- A nonce used twice: a receiver sealed every answer in its one run, and
a sender at rest coming back with the same run got a new slot whose
answers began at number 0 again under the same key. Each slot now
draws the run its answers are sealed in.
- A link stopped for good by one packet of the past: a stream that did
not know its last heard (after a restart or a rest) handed over from
its first record held, and a stale part of an old one never came
whole. It hands over from its first whole record.
- Answers taken away: any packet that opened, however old, moved where
answers went. Only the newest of its run moves it now; a new address
is sent a challenge the sender says back (CHALLENGE, RESPONSE), and
until then gets at most three times the bytes that came from it.
- An answer made by a packet that did not open: a number heard before
was acknowledged before the packet was opened. It is opened first.
- An old answer lowering the sender's credit, or its gap making it send
again: the sender now sends up to the most heard and credit it was
told, and takes a gap only from a word not older than it knows.
- Ten thousand made-up packets made ten thousand keys: keys made for a
run that did not open now cost a token, 256 a second.
Counts now say runs keyed and secrets made.
|
| |
|
|
|
|
|
| |
A sample of 0 (a path within one clock tick, or a test's) made the
pacing rate the window over 0, and its bytes infinity times 0: NaN,
after which the sender never sent again. CUBIC's time-based estimate
divided by it too. The RTT they take is at least a microsecond.
|
| |
|
|
|
|
|
|
|
|
| |
(097)
M6 of design 011: with a 50 Mbit/s bottleneck, a link carries 21-41
times quinn's goodput when packets reorder, the same under random loss,
and 1.3-3.5 times with both; its p99 at 50 records a second is the same
or shorter; beside TCP CUBIC it takes 0.48-0.55 of the path, inside the
guardrail. Only on a clean path is it 3-5% below quinn.
|
| |
|
|
|
|
|
|
| |
With a fixed delay the drop-tail queue dropped one flow's packets eight
times as often as the other's by their phases alone (Floyd and Jacobson,
1992): Pure took 0.63-0.77 of the path from TCP and 0.69 from quinn,
0.47 from itself, and 0.49 under fq_codel. With 1 ms of jitter it takes
0.53 at an RTT of 20 ms.
|
| |
|
|
|
| |
Linux's CUBIC and quinn's keep it so; RFC 9438's whole datagram once
past the window before the event is left out.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
acknowledgments
Packets go at the window an RTT, a quarter more (twice in slow start),
up to 2 ms of it at once (10 to 256 datagrams, RFC 9002 7.7); the sender
says when the next may go. The Reno-friendly estimate grows by the
acknowledgments (RFC 9438) or by time (RFC 8312), whichever is ahead, so
a link its app holds back to a few records a second does not fall
behind it.
In the M6 simulator (RTT 100 ms, 5% loss, 50 records a second) the
median time a record takes falls from 134 to 51-71 ms and the p99 from
about 900 to 290-350 ms; a backlog with 1% loss at RTT 20 ms goes at
8.3 Mbit/s. The transport's CPU a record is unchanged.
|
| | |
|
| | |
|
| |
|
|
|
|
| |
The TCP flow started first and in a thread of the runner; two TCP flows
split 0.31 to 0.56. Now every flow is a process started at once, run
40 s, counted from 10 s on.
|