This reverts commit 8e1e40d75b3ab15c194b6bf9570f3edc46e2de58. This reverts commit f073c490f9fd7c5abc033af4857df92229877de7. This reverts commit f187d2d7e01a54823f3e979af9bbd148b398e7e9. This reverts commit bc272862a73cfce1b118586ca39d3a377d841f1b. This reverts commit 30a397513f8890a3406dc7ab91c6e067e3bbfbbb. This reverts commit 4fc6856fb50d88c20a0f533392ca606641c5f38f. Conflicts: urb/urbit.pill urb/zod/base/lib/drum.hoon
79 KiB
%ames
commentary
%ames
is our networking protocol.
First we give commentary on the code, the algorithms involved, and the
protocol. We trace through the code touched when a packet is sent,
received, acknowledged, and that acknowledgment applied. This is fairly
comprehensive, and contains many implementation details, but if you
understand this, then you understand %ames
.
If you've scrolled down this page, you may be intimidated by the amount
of Hoon code, especially if you are new to the language. Don't be afraid
of it, you don't have to read any of it if you don't want to -- every
interesting action the code takes is explained in plain English. In
fact, if you are new to the language, this may be a good learning
opportunity. Even if you don't understand every line of Hoon code,
you'll hopefully be able to follow most lines. By the time you've worked
through this, you'll have seen many common patterns and best practices.
Hoon, much more than other languages, is best learned by reading and
understanding large quantities of existing code. In this way, it is
similar to learning a natural language. All of this code is in
arvo/ames.hoon
.
After the commentary, we have reference documentation for all the data
structures that are specific to %ames
. If you see a data structure or
a variable used that you don't recognize, search for it in the code, and
it's very likely defined in one of these data structures. We recommend
that another tab is kept open for easy access to the data structure
reference documentation. The code for these is split between
arvo/ames.hoon
and arvo/zuse.hoon
.
The Lifecycle of a Packet (or, How a Packet Becomes Law)
Here, we will trace a packet as it makes its way through ames. There are
actually two pathways through ames: the legacy path through %want
, and
the modern way, entered through %wont
, with full end-to-end
acknowledgments. Here we will only trace the modern way, though much of
the path is the same for both.
When an app (or a vane) wishes to send a packet to another ship, it must
send a %wont
card:
[%wont p=sock q=path r=*] :: e2e send message
This card takes three arguments. The p
is a sock
, that is, a pair of
two ships, the first of which is the sender and the second is the
receiver. But wait, you ask, why do I get to decide who is the sender?
Can I fake like I'm someone else? The reason is that there are
potentially multiple ships on the same pier, and the kernel can send a
message from any of them. If you attempt to send a message from a ship
not on your pier, then ames will refuse to send it. If you hack around
in your own copy of ames to go ahead and send it anyway, then the other
ship will reject it because your key is bad. Only send messages from
yourself.
The q
is a path, representing the place on the other side that you
want to receive your message. It is approximately equivalent to a port
number. Messages on the same path are guaranteed to arrive in the same
order as they were sent. No such guarantees are made across paths.
The r
is the actual data that you are sending. As the type implies,
this can be an arbitrary noun, and it will be transferred to the
receiver exactly as-is, in a well-typed way. Of course, this is data
that is sent over the wire, so be careful not to send anything too
massive unless you're willing to wait.
But enough about the interface. Grepping in ames.hoon for %wont
, we
find that it appears in exactly two places: at its definition in
++kiss
, and in ++knob
, where it is handled. We see that we go
directly into ++wise:am
.
++ wise :: wise:am
|= [soq=sock hen=duct cha=path val=* ete=?] :: send a statement
^- [p=(list boon) q=fort]
zork:zank:(wool:(ho:(um p.soq) q.soq) hen cha val ete)
The inputs to this gate are exactly the sort of thing you'd expect. In
particular, everything in the %wont
gate is here plus the calling duct
so that we know where to send the acknowledgment and ete
to determine
if we're going to do the modern end-to-end acknowledgments.
The actual line of code looks intimidating, but it's really not all that
bad. Working from the inside out, the call to ++um
sets up our
domestic server, and the call to ++ho
sets up our knowledge about the
neighbor we're sending to. From the outside, ++zork
and ++zank
just
apply the changes made to our ++um
and ++am
cores, respectively. If
you're familiar with the common idiom of ++abet
, that's all this is.
The code predates the widespread usage of that name.
The interesting part, then, is in ++wool:ho:um:am
. Let's look at the
code.
++ wool :: wool:ho:um:am
|= [hen=duct cha=path val=* ete=?] :: send a statement
^+ +>
=+ ^= rol ^- rill
=+ rol=(~(get by ryl.bah) cha)
?~(rol *rill u.rol)
=+ sex=sed.rol
:: ~& [%tx [our her] cha sex]
=. ryl.bah
%+ ~(put by ryl.bah) cha
rol(sed +(sed.rol), san (~(put by san.rol) sex hen))
=+ cov=[p=p:sen:gus q=clon:diz]
%+ wind [cha sex]
?: ete
[%bund q.cov cha sex val]
[%bond q.cov cha sex val]
This is slightly more complicated, but it's still not all that bad. Our inputs, at least, are fairly obvious.
If you glance at the code for a second, you'll see that
++wind:ho:um:am
seems to be able to send a message, or ++meal
, given
a ++soup
. This gate, then, just sets up the things we need to for
++wind
to do its job.
We first get rol
, which is a ++rill
, that is, a particular outbound
stream. This stream is specific to the path on which we're sending. If
the path hasn't been used before, then we create it. We let sex
be the
number of messages we've already sent on this path.
Then, we update the outbound stream by incrementing the number of
messages sent and placing an entry in san.rol
that associates the
message number with the duct
that sent the message. This allows us to
give the acknowledgment to the one who sent the message.
We let cov
be the current life of our crypto and our neighbor's
crypto. At the moment, we only need our neighbor's life, which we put
into the meal.
Finally, we call ++wind:ho:um:am
with the ++soup
of the path and
message number and the ++meal
of the payload itself. For end-to-end
acknowledged messages, we use %bund
.
[%bund p=life q=path r=@ud s=*] :: e2e message
Looking at how we create the %bund
, we can easily see what each field
is for.
Following the trail a little further, we go to ++wind:ho:um:am
.
++ wind :: wind:ho:um:am
|= [gom=soup ham=meal]
:: ~& [%wind her gom]
^+ +>
=^ wyv diz (zuul:diz now ham)
=^ feh puz (whap:puz now gom wyv)
(busk xong:diz feh)
++wind
does three things: it (1) encodes the message into a list of
possibly-encrypted packets, (2) puts the message into the packet pump,
and (3) sends any packets that are ready to be sent. Yes, our nice
little linear run of each gate calling exactly one other interesting
gate is over. We'll go in order here.
++zuul:lax:as:go
is the what converts a ++meal
into a list of
actual, 1KB packets.
++ zuul :: zuul:lax:as:go
|= [now=@da ham=meal] :: encode message
^- [p=(list rock) q=_+>]
=< weft
++ wasp :: null security
++ weft :: fragment message
++ wisp :: generate message
For organizational purposes, ++zuul
constructs an internal core with
three arms. ++wasp
encodes the meal into an atom with no encryption.
++wisp
encodes a meal with possible encryption (else it simply calls
++wasp
). ++weft
takes the result of ++wisp
and splits it into
actual packets.
++ wasp :: null security
^-([p=skin q=@] [%none (jam ham)])
This simply jams the meal, wrapping it with the skin
of %none
,
meaning no encryption.
Since ++wisp
is a little long, we'll go through it line-by-line.
++ wisp :: generate message
^- [[p=skin q=@] q=_..wisp]
++wisp
produces a pair of a skin
and an atom, which is the meal
encoded as a single atom and possibly encrypted.
?: =(%carp -.ham)
[wasp ..wisp]
If the meal that we're encoding is a %carp
, then we don't encrypt it.
A %carp
meal is a partial meal, used when a message is more than 1KB.
Since the entire message is already encrypted, we don't need to encrypt
each packet individually again.
?: !=(~ yed.caq.dur)
?> ?=(^ yed.caq.dur)
:_ ..wisp
:- %fast
%^ cat 7
p.u.yed.caq.dur
(en:r:cluy q.u.yed.caq.dur (jam ham))
If we have a symmetric key set up with this neighbor, then we simply use
it. The skin %fast
is used to indicate a symmetric key.
?: &(=(~ lew.wod.dur) |(=(%back -.ham) =(%buck -.ham)))
[wasp ..wisp]
If we do not yet have our neighbor's will, then there is no way that we can seal the message so that only they may read it. If what we're sending is an acknowledgment, then we go ahead and just send it in the clear.
=^ tuy +>.$
?:(=(~ lew.wod.dur) [*code +>.$] (griz now))
If we don't have our neighbor's will, then we "encrypt" with a key of 0. If we do have their will, then we generate a new symmetric key that we will propose.
:_ ..wisp
=+ yig=sen
=+ bil=law.saf :: XX send whole will
=+ hom=(jam ham)
yig
will be the life and engine for our current crypto. bil
is our
will. hom
is the meal encoded as a single atom.
?: =(~ lew.wod.dur)
:- %open
%^ jam
[~ `life`p.yig]
bil
(sign:as:q.yig tuy hom)
If we do not have our neighbor's will, then we send our current life along with our will and the message. The message itself is "signed" with a key of 0.
:- %full
=+ cay=cluy
%^ jam
[`life`p.cay `life`p.yig]
bil
(seal:as:q.yig pub:ex:r.cay tuy hom)
-- :: --zuul:lax:as:go
If we do have our neighbor's will, then we send our perception of their current life, our current life, our will, and the message. The message is sealed with their public key so that only they can read our message.
Once we have the message encoded as an atom, ++weft
goes to work.
++ weft :: fragment message
^- [p=(list rock) q=_+>.$]
=^ gim ..weft wisp
:_ +>.$
^- (list rock)
We're going to produce a list of the packets to send. First, we use the
aforementioned ++wisp
to get the message as an atom.
=+ wit=(met 13 q.gim)
?< =(0 wit)
wit
is the number of 1KB (2^13 bit) blocks in the message. We assert
that there is at least one block.
?: =(1 wit)
=+ yup=(spit [our her] p.gim q.gim)
[yup ~]
If there is exactly one block, then we just call ++spit
to turn the
message into a packet. We'll explain what ++spit
does momentarily.
=+ ruv=(rip 13 q.gim)
=+ gom=(shaf %thug q.gim)
=+ inx=0
If there is more than one block, then we rip it into blocks in ruv
.
gom
is a hash of the message, used as an id. inx
is the number of
packets we've already made.
|- ^- (list rock)
?~ ruv ~
=+ ^= vie
%+ spit
[our her]
wasp(ham [%carp (ksin p.gim) inx wit gom i.ruv])
:- vie
$(ruv t.ruv, inx +(inx))
Here we package each block into a packet with ++spit
and produce the
list of packets.
++ spit :: cake to packet
|= kec=cake ^- @
=+ wim=(met 3 p.p.kec)
=+ dum=(met 3 q.p.kec)
=+ yax=?:((lte wim 2) 0 ?:((lte wim 4) 1 ?:((lte wim 8) 2 3)))
=+ qax=?:((lte dum 2) 0 ?:((lte dum 4) 1 ?:((lte dum 8) 2 3)))
=+ wix=(bex +(yax))
=+ vix=(bex +(qax))
=+ bod=:(mix p.p.kec (lsh 3 wix q.p.kec) (lsh 3 (add wix vix) r.kec))
=+ tay=(ksin q.kec)
%+ mix
%+ can 0
:~ [3 1]
[20 (mug bod)]
[2 yax]
[2 qax]
[5 tay]
==
(lsh 5 1 bod)
This is how we turn a message into a real packet. This has the definition of the packet format.
wim
is the length of the sending ship, and dum
is the length of the
receiving ship. There are only five possibilities for each of those,
corresponding to carriers, cruisers, destroyers, yachts, and submarines.
These are encoded in yax
and qax
as 0, 0, 1, 2, and 3, respectively.
Thus, wix
and vix
are the number of bytes that must be reserved for
the ship names in a packet.
Next, we construct bod
by simply concatenating the sending ship, the
receiving ship, and the body of the message. Then, we get the encryption
mechanism from ++skin
, which may be a 0, 1, 2, or 3, and put it in
tay
.
Next, we concatenate together, bit by bit, some final metadata. We use three bits for our protocol number, which is incremented modulo eight when there is a continuity breach or the protocol changes. We use the final twenty bits of a hash of the body (which, we suppose, makes it a twenty bit hash) for error-checking. We use two bits to tell how much room is used in the body for the sending ship, and another two bits for the receiving ship. Finally, we use five bits to store the encryption type. Note that since there are only two bits worth of encryption types, there are three unused bits here. This adds up to 32 bits of header data. Finally, we concatenate this onto the front of the packet. Thus, we can summarize the packet header format as follows.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Proto| Hash of Body |yax|qax| Crypto |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
After this, there are yax
bits of the sender name, qax
bits of the
receiver name, and up to 8192 bits of data. Thus, the maximum size of a
packet is achieved in a message between two submarines with 8192 bits of
data. This will require 32+128+128+8192 = 8480 bits, or 1060 bytes.
This concludes our discussion of ++zuul:lax:as:go
. If you recall from
++wind:ho:um:am
, the list of packets from ++zuul
is passed into
++whap:pu
to update the packet pump and get any packets that can be
sent immediately.
++ whap :: whap:pu
|= [now=@da gom=soup wyv=(list rock)] :: send a message
^- [(list rock) _+>]
=. pyz (~(put by pyz) gom (lent wyv))
=. +>
|- ^+ +>.^$
?~ wyv +>.^$
%= $
wyv t.wyv
nus +(nus)
diq (~(put by diq) (shaf %flap i.wyv) nus)
puq (~(put to puq) [nus `soul`[gom 0 | ~2000.1.1 i.wyv]])
==
(harv now)
First, we put into pyz
the id for this message and the number of its
packets that have not yet been acknowledged, which is of course the
total number of packets since we haven't even sent the packets.
For every packet, we change three things in the state (++shed
) of our
packet pump: (1) we increment nus
, the number of packets sent; (2) we
put the packet number into diq
keyed by a hash of the packet; and (3)
we put the packet into the packet queue, with the basic metadata of its
id gom
, 0 transmissions, not live yet, last sent in the year 2000, and
the packet itself.
Finally, we harvest the packet pump.
++ harv :: harv:pu
|= now=@da :: harvest queue
^- [(list rock) _+>]
?: =(~ puq) [~ +>(rtn ~)]
?. (gth caw nif) [~ +>]
=+ wid=(sub caw nif)
=| rub=(list rock)
=< abet =< apse
|%
++harv
contains a core for most of its work. The meat is in ++apse
.
First, though, it sets itself up. If there aren't any packets in the
queue, then we simply do nothing except set rtn
, our next timeout, to
nil because we don't have any packets that may need to be retransmitted.
If we have more live (that is, sent and unacknowledged) packets than our
window size, then we don't do anything.
Otherwise, we let wid
be the width of our remaining packet window, and
we initialize rub
to nil. rub
will be the list of packets that are
ready to be sent. We then call ++apse
and pass the result to ++abet
.
++apse
decides which packets are ready to be sent.
++ apse
^+ .
?~ puq .
?: =(0 wid) .
=> rigt =< left
?> ?=(^ puq)
?: =(0 wid) .
?. =(| liv.q.n.puq) .
:: ~& [%harv nux.q.n.puq p.n.puq]
%_ .
wid (dec wid)
rub [pac.q.n.puq rub]
nif +(nif)
liv.q.n.puq &
nux.q.n.puq +(nux.q.n.puq)
lys.q.n.puq now
==
If there are no remaining packets to send, or if we've filled the packet
window, do nothing. We call ++rigt
and ++left
to process the left
and right branches of the packet queue.
Now we assert that the queue is not empty, and we again check that we haven't filled the packet window. We will operate on the head of the queue. If the packet is live, then do nothing. Otherwise, we go ahead and send it.
To send, we (1) decrement wid
, our packet window width; (2) cons the
packet onto the rub
, which will be returned as the list of packets to
send; (3) increment nif
, the number of live packets; (4) set the
packet to be live; (5) increment the number of transmissions of the
packet; and (6) set the last sent time of the packet to now.
++ left
?> ?=(^ puq)
^+(. =+(lef=apse(puq l.puq) lef(puq [n.puq puq.lef r.puq])))
++ rigt
?> ?=(^ puq)
^+(. =+(rig=apse(puq r.puq) rig(puq [n.puq l.puq puq.rig])))
These do exactly what you would expect: they traverse the packet queue
so that ++apse
gets called recursively through it.
Finally, ++abet
gets called, which resolves the changes.
++ abet
?~ rub [~ +>.$]
[(flop rub) +>.$(rtn [~ (add rto now)])]
This returns the packets that we wish to send, and it updates the timeout so that we know when to try resending unacknowledged packets.
This concludes our discussion of ++whap:pu
. To finish
++wind:ho:um:am
, we just need to delve into ++busk:ho:um:am
. But
wait, in the call to ++busk
, the first argument is xong:diz
. What is
this? This, my dear reader, is one more detour, this time into
++xong:lax:as:go
.
++ xong :: xong:lax:as:go
^- (list ship) :: route unto
=+ [fro=xen too=xeno]
=+ ^= oot ^- (list ship)
=| oot=(list ship)
|- ^+ oot
?~ too ~
?: (lien fro |=(a=ship =(a i.too))) ~
[i.too $(too t.too)]
:: ~& [%xong-to [our her] (weld oot ?>(?=(^ fro) t.fro))]
(weld oot ?>(?=(^ fro) t.fro))
This gets the list of intermediate ships needed to get a packet from us
to our neighbor. First, we get fro
and too
, the "canons" of ourself
and our neighbor, respectively.
What is this "canon", you ask? A canon is simply a ship plus its
"ancestors", as defined by ++sein
. For example, the canon of
~hoclur-bicrel
is:
~hoclur-bicrel/try=> (saxo ~hoclur-bicrel)
~[~hoclur-bicrel ~tasruc ~tug]
If we follow the algorithm in ++xong
, we see that we are simply
creating a list of ships that form a path from our neighbor to ourself.
Essentially, we look through the canon of our neighbor until we find
something in our own cannon -- a common ancestor. Or, if we are from
different carriers, then there is no common ancestor. We then weld this
onto the tail of our own canon. In the end, this is simply a list of
possible ships to try to route via to get to our neighbor, ordered by
preferability (that is, closeness to our neighbor). We will end up
trying, in order, to find a lane to these.
Now, we can finally get to ++busk:ho:um:am
.
++ busk :: busk:ho:um:am
|= [waz=(list ship) pax=(list rock)] :: send packets
%_ +>
bin
|- ^+ bin
?~ pax bin
$(pax t.pax, bin (weld (flop (wist:diz now waz ~ i.pax)) bin))
==
Thankfully, ++busk
is fairly simple. We go through the list of packets
and convert them to ++boon
s with ++wist:lax:as:go
. These boons are
placed into bin
, and they end up getting processed by ++clop
(this
happens in ++knob
).
++ wist :: wist:lax:as:go
|= $: now=@da :: route via
waz=(list ,@p)
ryn=(unit lane)
pac=rock
==
^- (list boon)
?: =(our her) [[%ouzo *lane pac] ~]
?~ waz ~
=+ dyr=?:(=(her i.waz) dur (gur i.waz))
?. ?& !=(our i.waz)
?=(^ lun.wod.dyr)
==
$(waz t.waz)
:_ ?: ?=(%ix -.u.lun.wod.dyr)
$(waz t.waz)
~
:+ %ouzo u.lun.wod.dyr
?: &(=(i.waz her) =(~ ryn)) pac
=+ mal=(jam `meal`[%fore her ryn pac])
%- spit
^- cake
:* [our i.waz]
?~ yed.caq.dyr [%none mal]
:- %fast
%^ cat 7
p.u.yed.caq.dyr
(en:crua q.u.yed.caq.dyr mal)
==
This takes a sample of the current time, the list of ships that we just generated, a lane if we already know it, and the packet itself.
First, if we are sending a message to ourself, then we simply create a
%ouzo
boon with a bunted lane. Otherwise, if there are no routing
candidates, there is nothing we can do, so we return nil.
Next, we get the dore
of the first routing candidate. If we're looking
at the neighbor to whom we're trying to send the message, then we simply
use the dore
that we already have. Otherwise, we get a default dore
.
If we're the first routing candidate, or if we have don't have a lane to this candidate, then we skip this candidate and move on to the next one.
If we have only a provisional ip address, then we try to send on it, but we also try to send on later routing candidates as well. Otherwise, we only send on this one candidate.
Finally, we create the actual %ouzo
boon. The lane is the one from our
dore
. If we're sending it directly to our intended recipient, and we
haven't been told to use a specific lane, then we just send the packet
directly. Otherwise, we wrap it in a little %fore
meal, telling the
intermediary to whom we wish it to be sent. If we have already set up a
symmetric key with the intermediary, then we encrypt it with that.
Otherwise, we send it in the clear.
Now, if you recall, we have traced all the way through from the
beginning when, in ++knob
, the %wont
card was handled by a call to
++wise
. There is only one more step before the packet is finally sent.
Looking in ++knob
, we see that the resultant list of boons is passed
into ++clop
, which will execute the correct actions and return a list
of moves. In ++clop
, we see the handling of each specific boon. The
one we are interested in is %ouzo
, since that is the only one we have
sent thus far.
%ouzo
:: ~& [%send now p.bon `@p`(mug (shaf %flap q.bon))]
:_ fox
[[gad.fox [%give %send p.bon q.bon]] ~]
Very simply, we give a %send
gift along the special duct that goes
straight into the bowels of unix. This is the last stop before we drop
into vere, and later libuv. And then... the world.
The packet, after its creation, embarks on a journey across physical
time and space into the great unknown. Hurtling through fiber-optic
cables at hundreds of thousands of kilometers per second, it finally
arrives at our neighbor's network adapter. The adapter tells unix, unix
tells libuv, libuv tells vere, and vere sends a %hear
kiss to ames.
And now we reenter the kernel.
The %hear
kiss goes straight to ++knob
, just as did the %wont
kiss
earlier.
%hear
(~(gnaw am [now fox]) %good p.kyz q.kyz)
Here, though, we call ++gnaw:am
to process the packet. The arguments
to ++gnaw
are the same as those to the %hear
kiss: the lane on which
the packet was received and the packet itself. The other argument is
just %good
, which is a ++cape
saying that we expect the packet to
succeed. If a formal error occurs, then since we have a transactional
event system, the %hear
event will never be considered to have
actually happened, and unix will send a %hole
kiss so that we may send
a negative acknowledgment.
++ gnaw :: gnaw:am
|= [kay=cape ryn=lane pac=rock] :: process packet
^- [p=(list boon) q=fort]
?. =(2 (end 0 3 pac)) [~ fox]
=+ kec=(bite pac)
?: (goop p.p.kec) [~ fox]
?. (~(has by urb.ton.fox) q.p.kec)
[~ fox]
=< zork
=< zank
%- ~(chew la:(ho:(um q.p.kec) p.p.kec) kay ryn %none (shaf %flap pac))
[q.kec r.kec]
First, we check the protocol number. If it is not correct, then we
simply ignore the packet entirely. Otherwise, we parse the packet with
++bite
, which converts a packet atom into a cake
, that is, a triple
of the sock
(pair of sender and receiver), the skin
(encryption
type), and the data.
++ bite :: packet to cake
|= pac=rock ^- cake
=+ [mag=(end 5 1 pac) bod=(rsh 5 1 pac)]
=+ :* vez=(end 0 3 mag) :: protocol version
chk=(cut 0 [3 20] mag) :: checksum
wix=(bex +((cut 0 [23 2] mag))) :: width of receiver
vix=(bex +((cut 0 [25 2] mag))) :: width of sender
tay=(cut 0 [27 5] mag) :: message type
==
?> =(2 vez)
?> =(chk (end 0 20 (mug bod)))
:+ [(end 3 wix bod) (cut 3 [wix vix] bod)]
(kins tay)
(rsh 3 (add wix vix) bod)
This is exactly the inverse of ++spit
. Note that here we check both
the protocol number and the hash, crashing on error. Remember that a
crash will result in a negative acknowledgment being sent.
Continuing in ++gnaw
, we see that if the intended recipient is not on
our pier, then we drop the packet.
If we've gotten this far, then we wish to process the packet. Recall
that ++ho
and ++um
set up the domestic server and foreign client
cores, respectively, and that ++zork
and ++zank
resolve any changes
to these cores.
The new stuff here, then, is the ++la
core and the ++chew
arm. The
++la
sets up a core for this particular packet, containing the current
success/failure cape
, the lane it was sent on, the encryption type,
and a hash of the packet, used as an id.
++chew
is called with the encryption type and the message itself. It
contains a little helper core inside of it, which starts immediately
with ++apse
.
++ apse
^+ +>.$
=+ oub=bust:puz
=+ neg==(~ yed.caq.dur.diz)
=. +>.$ east
=+ eng==(~ yed.caq.dur.diz)
=+ bou=bust:puz
=. bin
?. &(oub !bou) bin
:_(bin [%wine [our her] " is ok"])
=. bin
?. &(neg !eng) bin
:_(bin [%wine [our her] " is your neighbor"])
+>.$
First, we let oub
be true if our neighbor hasn't been responding to us
for more than sixteen seconds. Let neg
be true if we haven't yet
proposed a symmetric key, meaning that we haven't yet corresponded with
this ship, so they are not our neighbor. Next, we run ++east
, which
we'll go into in just a minute.
We now do the same two checks and store the results in eng
and bou
.
If our neighbor has, like the prodigal son, returned after an extended
absense, then we send a %wine
boon as the proverbial fatted calf,
which is simply printed out to the console. Likewise, if we are meeting
one with whom we have never had the pleasure of acquainting ourselves,
we send a message to the console to that effect.
We skipped over ++east
, which contains the meat of the processing. It
first decrypts the message, then calls ++chow:la:ho:um:am
with the
resultant meal. We'll go through each of the four cases in turn, but
first since each one calls ++bilk:pu
, we'll take a brief detour.
++ bilk :: bilk:pu
|= now=@da :: inbound packet
^+ +>
=+ trt=(mul 2 rtt)
%= +>.$
rue [~ now]
rto trt
rtn ?~(puq ~ [~ (add now trt)])
==
This updates the timing information in our packet pump. rue
, the last
time we have heard from this neighbor, is set to now. rto
, the
retransmit timeout is set to twice the current ping time, and if there
is anything in the packet queue, then we reset the next timeout, since
we've just heard a message.
Back to ++east
.
%none
=. puz (bilk:puz now)
(chow ((hard meal) (cue msg)))
The simplest case is when the encryption type is %none
. We first call
++bilk
to update the packet pump, then we cue (unjam) the message into
a meal. We hard cast it into a meal -- if the cast fails, then we do
want to crash since someone is sending us malformed data. Finally, we
send the result to ++chow
for interpretation and handling.
%fast
=+ [mag=`hand`(end 7 1 msg) bod=(rsh 7 1 msg)]
=+ dey=(kuch:diz mag)
?~ dey
~& [%bad-key her mag]
+>.$ :: ignore unknown key
=. puz (bilk:puz now)
=^ key diz u.dey
(chow(aut sin) ((hard meal) (cue (dy:q:sen:gus key bod))))
For symmetric encryption, we first get the hand
, which is the hash of
the symmetric key. We pass it to ++kuch:lax:as:go
, which returns the
key if we either have used it before or we have proposed it. If we have
proposed it, then we change its status from proposed to real. If
++kuch
fails, then we drop the packet and print out a %bad-key
message.
Otherwise, we call ++bilk
as before to update the packet pump and pass
into ++chow
the decrypted data.
%full
=+ mex=((hard ,[p=[p=life q=life] q=will r=@]) (cue msg))
=. diz (deng:diz q.mex)
=+ wug=cluy:diz
?> =(q.p.mex p.wug)
=+ gey=(sev:gus p.p.mex)
=+ mes=(need (tear:as:q.gey pub:ex:r.wug r.mex))
=. diz (wasc:diz p.mes)
=. puz (bilk:puz now)
(west(msg q.mes))
For sealed asymmetric encryption, we first take off the the layer of
data that gives us the life and will of our neighbor, and we apply try
to extend their former will with the new data. ++deng
will fail if
this is impossible.
Next, we get our most current understanding of our neighbor's crypto,
and we verify that it's the same life as what they're sending. Then, we
get our own crypto from ++sev
and decrypt the message with the public
key from our neighbor's crypto. We register the proposed symmetric key,
update the packet pump, and call ++west
, which simply casts the
message to a meal and calls ++chow
, reporting any error.
%open
=+ mex=((hard ,[p=[~ q=life] q=will r=@]) (cue msg))
=. diz (deng:diz q.mex)
=+ wug=cluy:diz
?> =(q.p.mex p.wug)
=+ mes=(need (sure:as:r.wug *code r.mex))
=. puz (bilk:puz now)
(west(msg mes))
Finally, for signed asymmetric encryption, we, as before, take off the layer of data that gives us the life and will of our neighbor. This time, of course, we do not get our own crypto -- only that of our neighbor.
The rest you have seen. We call ++deng
to extend the will, we verify
that their crypto life is what we think it ought to be, we "decrypt" the
data, we update the packet pump, and we call ++west
to call ++chow
.
++ chow :: chow:la:ho:um:am
|= fud=meal :: interpret meal
^+ +>
=. diz ?:(=(%none aut) diz (wast:diz ryn))
(dine fud)
Here, if the message was encrypted at all, then we call
++wast:lax:as:go
, which simply updates the lane (route) to our
neighbor (unless we're given a provisional route). This ensures that we
always have the most direct possible path to them.
We've been handling this meal for so long, we've almost forgotten what we want to do with it. The telos is of any meal to be dined on. We will choose out the cases here that are important to our current investigation.
%fore
=+ ^= lyn ^- lane
?~ q.fud ryn
?. ?=(%if -.u.q.fud) u.q.fud
[%ix +.u.q.fud]
:: u.q.fud
?: =(our p.fud)
(emit %mead lyn r.fud)
=+ zid=(myx:gus p.fud)
(emir (wist:zid now xong:zid [~ lyn] r.fud))
Forwarding is the simplest case, since we've seen all the arms before,
except perhaps ++emit
and ++emir
, which simply take a boon or list
of boons respectively and queue them up to be handled when the core
resolves. If we're told to forward a packet to ourselves, then we emit a
%mead
boon which simply sends another %hear
kiss to ourselves with
the data. Otherwise, we try to find a route to the recipient, as before.
%carp
=+ zol=(~(get by olz.weg) s.fud)
?^ zol cock(kay u.zol)
=^ neb nys.weg
=+ neb=(~(get by nys.weg) s.fud)
?^ neb [u.neb nys.weg]
=+ neb=`bait`[(kins p.fud) 0 r.fud ~]
[neb (~(put by nys.weg) s.fud neb)]
?> (lth q.fud p.r.neb)
?> =((kins p.fud) p.neb)
?> =(r.fud p.r.neb)
=+ doy=`(unit ,@)`(~(get by q.r.neb) q.fud)
?^ doy cock
=> ^+ . %= .
q.r.neb (~(put by q.r.neb) q.fud t.fud)
q.neb +(q.neb)
==
:: ~& [%carp q.fud s.fud q.neb p.r.neb]
?: =(q.neb p.r.neb)
=: nys.weg (~(del by nys.weg) s.fud)
olz.weg (~(put by olz.weg) s.fud kay)
==
(golf p.neb r.neb)
=. +>.$ cock
+>.$(nys.weg (~(put by nys.weg) s.fud neb))
Here, we have received a partial message, and we're just assembling the individual packets into a message. Most of this code is fairly algorithmic, so we'll just hit the high points. In the beginning, we check if we've already received this message, and if so, we resend the acknowledgment. Remember, "always ack a dupe, never ack an ack".
In nys.weg
we keep track of an incoming set of partial packets,
indexed by the flap
hash that comes with every packet. We check to see
if we have already received this partial message, and if so we
acknowledge it. Otherwise, we put it in nys.weg
unless this is the
last message, in which case we ack the last partial message, move the
complete message into olz.weg
, and call ++golf
, which assembles the
message and calls ++chew
, to start the dance again with the complete
message.
%bund
:: ~& [%bund q.fud r.fud]
?> =(p:sen:gus p.fud)
(deer q.fud r.fud ?-(kay %dead ~, %good [~ s.fud]))
What if we're just receiving a regular old, garden variety message? We
call ++deer
with the data from the message. If we already know that
the message processing will fail (that is, if we got a %hole
card from
unix rather than a %hear
card), then we don't even send the data at
all. Remember, if a packet fails to process, it's as if it never even
arrived, except that we send a negative acknowledgment.
++ deer :: deer:la:ho:um:am
|= [cha=path num=@ud dut=(unit)] :: interpret message
^+ +>
=+ rum=(fall (~(get by raz.bah) cha) *race)
%= +>.$
+>
?. (gte num did.rum) :: always ack a dup
(cook (~(get by bum.rum) num) cha ~ ryn dam)
?: dod.rum
(coat cha rum(mis (~(put by mis.rum) num [kay ryn dam dut])))
%= +>.+>.$
raz.bah
%+ ~(put by raz.bah) cha
rum(mis (~(put by mis.rum) num [kay ryn dam dut]))
==
==
First, we get the race for this particular triple of sender, receiver,
and path, creating it if it doesn't exist. If we've already acked the
message, then we resend the ack. Note that did.rum
is the number of
packets we acknowledged, positively or negatively while bum.rum
is a
map of message numbers to negative acknowledgments. Thus, if a message
number is less than did.rum
, then if it's in bum.rum
then it was
negatively acknowledged, otherwise it's postively acknowledged. Thus, we
are constant in space with the number of successful messages and linear
in the number of failed messages. We'll document ++cook
later on, but
suffice it to say that it sends an acknowledgment. It is to end-to-end
acknowledgments what ++cock
is to packet-level acknowledgments.
If we are still processing a message (that is, dod.rum
is false), then
we simply put this message in the map of misordered packets to be
processed when their time comes. "Processing a message" in this case
means that we've received the message and notified the correct
application, but we're still waiting for the application-level
acknowledgment.
Otherwise, we're ready for a packet, so we process it.
++ coat :: coat:ho:um:am
|= [cha=path rum=race] :: update input race
^+ +>
=+ cun=(~(get by mis.rum) did.rum)
?~ cun
+>.$(raz.bah (~(put by raz.bah) cha rum))
?. =(%good p.u.cun) +>.$
?> ?=(^ s.u.cun)
%= +>.$
raz.bah (~(put by raz.bah) cha rum(dod |))
bin
:_ bin
:^ %mulk
[our her]
`soap`[[p:sen:gus clon:diz] cha did.rum]
u.s.u.cun
==
First, we grab the message we want to process and store it in cun
. If
it's a good packet, then we change dod.rum
to false, meaning that
we're in the middle of processing a packet and should not start
processing another one. We also put a %mulk
boon into the queue so
that, when it all resolves, we send a mesage to the intended recipient
application. The boon contains the sender, the receiver, the identity of
the message, and the message itself.
This bubbles up all the way back to ++knob
, where we were handling the
%hear
card. Following the logic in ++knob
, we can see that the boons
get sent into ++clop
to be turned into actual arvo-level moves. We've
been here before, if you recall, when we handled the %cake
boon to
send a message. Now, we're handling the %mulk
boon, which is
unfortunately slightly more complicated.
%mulk
:: ~& [%mulk p.bon q.bon]
?> ?=([@ @ *] q.q.bon)
?> ?=(%q i.q.q.bon)
?+ i.t.q.q.bon
~& %mulk-bad
:_ fox
:~ :- (claw p.p.bon)
[%sick %wart p.bon i.t.q.q.bon t.t.q.q.bon r.bon]
==
%ge :: %gall request
?> ?=([@ ~] t.t.q.q.bon)
=+ app=`term`(need ((sand %tas) i.t.t.q.q.bon))
=+ ^= pax
:+ (scot %p p.p.bon)
(scot %p q.p.bon)
q.q.bon
:_ fox [hen %pass pax %g %rote p.bon app r.bon]~
%gh :: %gall response
?> ?=([@ ~] t.t.q.q.bon)
=+ app=`term`(need ((sand %tas) i.t.t.q.q.bon))
=+ ^= pax
:+ (scot %p p.p.bon)
(scot %p q.p.bon)
q.q.bon
:_ fox [hen %pass pax %g %roth p.bon app r.bon]~
==
We're dispatching messages based on the prefix of their path. Since only
%gall
apps use end-to-end acknowledgments at the moment, every path
must have at least two elements, and the first one must be %q
. Beyond
that, we handle the /q/ge
and /q/gh
cases for gall requests and
responses, respectively.
In both cases, we require the next term in the path to be the name of
the intended recipient %gall
app. Thus, a message to /q/ge/talk
for
example, will send a message to the talk app.
We then send a message to the app itself. The message is either a
%rote
or a %roth
for a request and a response, respectively. The
content is the rook
or roon
that was sent (stored in r.bon
), but
we don't actually handle that at all here. That's completely a
%gall
-level thing. We're just the messenger.
Notice the path we send this over. We encode the sender, the receiver,
and the path over which it was sent. This fully specifies the race
so
that when the app gives us the acknowledgment we know where to send it.
We now have another interlude. We have entrusted our precious data, so carefully guarded and guided from the app on that far-away ship, to our local app. It has the ability to do whatever it pleases with it. It may take a significant amount of time to process. When the message has been handled by this app, though, it must produce an acknowledgment. Our final task is to deliver this acknowledgment to the sending app.
We should describe here what exactly these oft-mentioned acknowledgments
actually consist of. There are two kinds of acknowledgments: positive
and negative. A positive acknowledgment contains no data other than its
existence. A negative acknowledgment may optionally include a reason for
said negativity. Formally, a negative acknowledgment is an ares
, which
is a unit pair of a term and a list of tanks. If this is null, this is
simply a failure with no associated information. If the pair exists, the
term is a short error code that is usually both human and computer
readable. For example, if you try to send a message to a valid %gall
app that doesn't have any ++poke
to handle it, then %gall
will give
a negative acknowledgment with error term %poke-find-fail
. The list of
tanks is a human-readable description of the error. This often contains
a stack trace. At any rate, all this information is returned to the
sending app on the other end of the wire.
After this brief interlude, our story resumes in ++knap
, where we
receive responses. In particular, a %mean
indicates a negative
acknowledgment while a %nice
indicates a positive acknowledgment.
?(%mean %nice)
?> ?=([@ @ @ *] tea)
=+ soq=[(slav %p i.tea) (slav %p i.t.tea)]
=+ pax=t.t.tea
=+ ^= fuy
=< zork =< zank
%^ ~(rack am [now fox]) soq pax
?-(+<.sih %mean `p.+.sih, %nice ~)
=> %_(. fox q.fuy)
=| out=(list move)
|- ^- [p=(list move) q=_+>.^$]
?~ p.fuy
[(flop out) +>.^$]
=^ toe fox (clop now hen i.p.fuy)
$(p.fuy t.p.fuy, out (weld (flop toe) out))
Recall the format of the path we sent the message on, and you'll
understand why soq
and pax
are the sender/receiver pair and path on
which the message was sent. The rest of this is structured much like
++knob
, so we call ++rack:am
and send the resulting boons to
++clop
. Business as usual.
++ rack :: rack:am
|= [soq=sock cha=path cop=coop] :: e2e ack
=+ oh=(ho:(um p.soq) q.soq)
=. oh (cook:oh cop cha ~)
(cans:oh cha)
First, we set up ++um
and ++ho
, as we've done twice before, for our
domestic and foreign servers, respectively. The other two things are
new, though. Well, ++cook
is not actually new, but we delayed the
explanation saying only that it sends an acknowledgment. The time has
come.
++ cook :: cook:ho:um:am
|= [cop=coop cha=path ram=(unit ,[ryn=lane dam=flap])]
^+ +> :: acknowledgment
=+ rum=(need (~(get by raz.bah) cha))
=+ lat=(~(get by mis.rum) did.rum)
?: &(?=(~ lat) ?=(~ ram)) ~&(%ack-late-or-redundant +>.$)
=+ ^- [ryn=lane dam=flap]
?^ ram [ryn.u.ram dam.u.ram]
?< ?=(~ lat)
[q r]:u.lat
=. raz.bah
?^ ram raz.bah
%+ ~(put by raz.bah) cha
rum(dod &, bum ?~(cop bum.rum (~(put by bum.rum) did.rum u.cop)))
=^ roc diz (zuul:diz now [%buck cop dam ~s0])
(busk(diz (wast:diz ryn)) xong:diz roc)
If we are acknowledging a message that we have already acked, the ram
will contain the new lane and flap to send the duplicate ack to. This
happens if we call ++cook
in ++deer
, but it doesn't happen from
++rack
. If there is no message waiting to be acknowledged and we're
not given an explicit lane and flap (that is, we're not sending a
duplicate ack), then the app must have sent us multiple acknowledgments.
We do the only sensible thing we can do and drop all acknowledgments
after the first, printing a message. This is, in fact, an error, so it
could be argued that we ought to crash. Whatever you do, don't depend on
this not crashing.
First, we grab the race specified by the given path, and we get the most recent in-order message, which must be the one which is being acknowledged.
Then, we decide which lane/flap to respond on/to. Basically, in the
usual case we respond on the lane through which the initial message was
sent, which is stored along with the other packet information in
mis.rum
, since it has to be remembered across calls to ames. However,
if we receive a duplicate message, then we must respond to the new
message. It's quite possible the reason the other acknowledgment didn't
get returned was that the lane between the ships was broken.
At any rate, we update the race by saying that we've finished processing
this packet (unless we're sending a duplicate ack) and, if we're sending
a negative acknowledgment, putting the negative ack into bum.rum
so
that we can resend it if necessary.
We encode our new message, updating the packet pump, with ++zuul
, as
before, and we send it off with ++busk
, routed via ++wast
to one of
the ships in ++xong
. Of course, in practice, we don't even look at the
ships in ++xong
because we already have a lane directly to our
neighbor (the one over which they sent their message to us).
We glossed over the actual message we're sending back. We're sending a
%buck
meal, which is an acknowledgment. The cop
specifies whether
this is a positive or a negative ack, dam
specifies the message we're
acknowledging, and the ~s0
is a placeholder for the processing time
required. This time is neither calculated (though it is hopefully
obvious how to do so) nor used at present, but this information may be
used in the future for improved congestion control. Since the round-trip
time for an end-to-end acknowledged packet includes the processing time
on the other end, most common congestion control algorithms will stumble
when some messages take much longer to process than others. As noted,
though, this is simply an opportunity for improvement -- our congestion
control algorithms are relatively naive at the moment.
Recall that ++busk
calls ++wist
to put the actual %ouzo
boon in
the queue, which gets handled by ++clop
to actually send the message.
This is the same pipeline as sending any other message, so we'll refer
you to the explanation above if you've forgotten it.
The last thing we need to do on this ship is move on to the next packet
in the queue if there is one. If you recall, in ++rack
after the call
to ++cook
there was a call to ++cans:ho:um:am
.
++ cans :: cans:ho:um:am
|= cha=path
=+ rum=(need (~(get by raz.bah) cha))
=. rum
%= rum
did +(did.rum)
mis (~(del by mis.rum) did.rum)
==
(coat cha rum)
This is very simple. We increment the number of packets that we've acknowledged on this race and we delete the packet that we just acknowledged from the set of misordered packets.
Then, we call ++coat
again to process the next packet if we've already
received it. And that's it for this.
The acknowledgment now travels the same path that its forebearer, the
original message, once tread, but this time not into the great unknown.
The weary traveler is seeking out its familial roots, finding the app
from whom sprung forth the original message way back in paragraph three.
When it arrives at the network adapter of its ancestors, the adapter
tells unix, unix tells libuv, libuv tells vere, and vere sends a %hear
kiss to ames. Once more into the kernel.
The %hear
kiss is handled in ++knob
as before, leading to ++gnaw
,
going over to ++chew
, ++apse
, ++chow
, and eventualy to ++dine
.
We've seen most of the cases in ++dine
, but we haven't yet looked at
the handling of this %buck
meal.
%buck
=. +> ?.(=(%full aut) +> cock) :: finish key exch
+>(..la (tock p.fud q.fud r.fud))
We send a packet level acknowledgment if we're finishing a key exchange,
else we call ++tock
to process the acknowledgment.
This will get a little involved, so if you don't much care about how
exactly an acknowledgment happens, just know that the result gets gifted
as a %woot
card back to the app who sent it. For those brave souls who
wish to see this thing through to the end, it's once more into the
breach.
++ tock :: tock:ho:um:am
|= [cop=coop fap=flap cot=@dr] :: e2e ack by hash
^+ +>
=^ yoh puz (bick:puz now fap)
=. +>.$
?~ p.yoh +>.$
=^ hud +>.$
(done p.u.p.yoh q.u.p.yoh)
?~ hud +>.$
%= +>.$
bin
:_ bin
`boon`[%cake [our her] [[p:sen:gus clon:diz] u.p.yoh] cop u.hud]
==
(busk xong:diz q.yoh)
We're going to work through this one a little backwards since it's
mostly fairly simple except the call to ++bick:pu
. In fact, we'll just
skip ++bick
for the moment and finish the rest.
If ++bick
succesfully acks the message, then we call ++done
.
++ done :: done:ho:um:am
|= [cha=path num=@ud] :: complete outgoing
^- [(unit duct) _+>]
=+ rol=(need (~(get by ryl.bah) cha))
=+ rix=(~(get by san.rol) num)
?~ rix [~ +>.$]
:- rix
%_ +>.$
ryl.bah
(~(put by ryl.bah) cha rol(san (~(del by san.rol) num)))
==
This very simply gets the rill (the outgoing counterpart to a race, if you recall), pulls out of the map of outstanding messages the duct over which the original message was sent, and produces this duct while deleting that entry from the map of outstanding messages.
Going back to ++tock
, we now have the duct we need to return the
result over. We do the very sensible thing and put a %cake
boon in the
queue to be processed later by ++clop
.
In q.yoh
we have a list of messages that may need to be sent, which we
pass to ++busk
to send, as usual. When an acknowledgment arrives, that
may trigger other messages immediately. This often happens when sending
more messages than the width of the logical window since for congestion
control reasons another message cannot be sent until some of the earlier
ones have been acknowledged.
We'll look at the processing of the %cake
boon in ++clop
before we
get back to talking about ++bick
.
%cake
:_ fox
:~ [s.bon %give %woot q.p.bon r.bon]
==
We very simply give, along the duct we found above, a %woot
card with
the ship who sent us the ack and the ack itself. This allows the
application to decide what to do about the result. In case of a failure,
we usually either resend the message or display it to the user.
Sometimes, we recognize the error term and handle it internally. In any
case, the decision of how to handle the acknowledgment is entirely up to
the application. Our job is done.
Well, except that we skipped ++bick:pu
. Let's go back to that.
++ bick :: bick:pu
|= [now=@da fap=flap] :: ack by hash
^- [[p=(unit soup) q=(list rock)] _+>]
=+ sun=(~(get by diq) fap)
?~ sun
[[~ ~] +>.$]
=. diq (~(del by diq) fap)
=^ gub +>.$ (bock now u.sun)
=^ yop +>.$ (harv now)
[[gub yop] +>.$]
If you recall, in ++whap:pu
we created the packet pump's
representation of the message, which included putting the message into
diq
, which maps from packet hashes to packet sequence numbers. Thus,
u.sun
is the sequence number of this particular message.
We delete this message from diq
since we have now received an ack for
it. We call ++bock
to perform the ack by sequence number. We call
++harv
to harvest the packet queue, sending any messages that are now
able to be sent.
In ++bock
, there are three arms we haven't seen before: ++bine
,
+wept
, and ++beet
. We'll describe each of these before we get to
++bock
. ++bine
looks scariest.
++ bine :: bine:pu
|= [now=@da num=@ud] :: apply ack
^- [(unit soup) _+>]
?~ puq !!
?. =(num p.n.puq)
?: (gth num p.n.puq)
=+ lef=$(puq l.puq)
[-.lef +.lef(puq [n.puq puq.lef r.puq])]
=+ rig=$(puq r.puq)
[-.rig +.rig(puq [n.puq l.puq puq.rig])]
=: rtt ?. &(liv.q.n.puq =(1 nux.q.n.puq)) rtt
=+ gap=(sub now lys.q.n.puq)
:: ~& [%bock-trip num (div gap (div ~s1 1.000))]
(div (add (mul 2 rtt) gap) 3)
nif (sub nif !liv.q.n.puq)
==
=+ lez=(dec (need (~(get by pyz) gom.q.n.puq)))
=^ gub pyz
?: =(0 lez)
[[~ gom.q.n.puq] (~(del by pyz) gom.q.n.puq)]
[~ (~(put by pyz) gom.q.n.puq lez)]
:- gub
+>.$(puq ~(nap to puq))
The first few lines are simply looking through the packet queue until we
find the correct packet to ack. This is basic queue manipulation that
operates directly on the treap structure of the queue. If you understand
treap queues, the logic is easy to follow. Otherwise, just trust us that
by the time we get to the =:
, the packet with sequence number num
is
on the top of the packet queue (that is, at n.puq
).
We first update the round-trip time. If the packet is either not alive
or had to be transmitted more than once, then we don't have any reliable
way of calculating the round-trip time since we're unsure of exactly
which transmission was acknowledged. Otherwise, the round-trip time is
the difference between now and when the packet was last sent. We set
rtt
by a little weighted average where the previous smoothed RTT is
weighted twice as much as the RTT of the current packet. Thus,
(2*rtt+gap)/3
. This gives us a nice smooth RTT that is somewhat
resilient to outlier data while still being responsive to our
ever-changing world.
If the packet wasn't already dead, then we decrement the number of live packets, which may allow more packets to be sent.
We decrement the number of unacknowledged packets in our pyz
for this
particular message. If you recall, this was set in ++whap
to the
number of packets required to send a message.
If that was the last packet in the messge that needed to be acked, then
we delete the messgae reference from pyz
and produce the id of the
message. Otherwise, we simply update pyz
with the new number of
unacked messages. In either case, we remove the packet from the packet
queue.
++ wept :: wept:pu
|= [fip=@ud lap=@ud] :: fip thru lap-1
=< abet =< apse
|%
++ abet +>.$
++ apse
^+ .
?~ puq .
?: (lth p.n.puq fip) ?~(l.puq . left)
?: (gte p.n.puq lap) ?~(r.puq . rigt)
=> rigt =< left
?> ?=(^ puq)
?.(liv.q.n.puq . .(nif (dec nif), liv.q.n.puq |))
::
++ left
?> ?=(^ puq)
^+(. =+(lef=apse(puq l.puq) lef(puq [n.puq puq.lef r.puq])))
++ rigt
?> ?=(^ puq)
^+(. =+(rig=apse(puq r.puq) rig(puq [n.puq l.puq puq.rig])))
--
The algorithm is a simple case of traversing the packet queue.
Essentialy, we mark as dead all packets in the queue between fip
and
(dec lap)
. We also update nif
, the number of live packets. Lest you
mourn too much the passing of these packets, know that they shall soon
rise again. Recall that in ++bick
after the call to ++bock
we call
++harv
. This will resend the packets that have just been labeled dead.
++ beet :: beet:pu
^+ . :: advance unacked
=- +(nep ?~(foh nus u.foh))
^= foh
|- ^- (unit ,@ud)
?~ puq ~
?: (lte p.n.puq nep) $(puq l.puq)
=+ rig=$(puq r.puq)
?^(rig rig [~ p.n.puq])
Here we search for the next expected packet number. Basically, we search
the queue for the leftmost packet whose number is greater than the
current nep
. If we don't find any such packet, we just use the total
number of packets sent.
We can now dive into ++bock
, our last arm.
++ bock :: bock:pu
|= [now=@da num=@ud] :: ack by sequence
^- [(unit soup) _+>]
=^ gym +> (bine now num)
:- gym
?: (gth num nep)
=+ cam=(max 2 (div caw 2))
:: ~& [%bock-hole num nep cam]
beet:(wept(nep num, cag cam, caw cam) nep num)
=. caw ?: (lth caw cag) +(caw)
(add caw !=(0 (mod (mug now) caw)))
?: =(num nep)
:: ~& [%bock-fine num nif caw cag]
beet
:: ~& [%bock-fill num nif caw cag]
+>.$
First, we call ++bine
to apply the ack to the packet pump information.
We produce gym
, which, if it exists, is the id of the packet that was
acked. If we received an ack for a packet later than the one we
expected, then we halve the logical packet window and kill all the
earlier packets so that they may be resent.
Otherwise, we possibly increase the congestion window. If the window is
less than the congestion threshold, then we increment the size of the
window. Otherwise, we only increment one out of every caw
times.
If we received an ack for the packet we expected, then we simply advance
nep
with ++beet
. If we received an ack for a packet earlier than we
expected, we do nothing.
It may be hard to believe, but we are, in fact, done. The message has
been sent, received, acknowledged, and the acknowledgment has been
returned to the original sender. We hope it's clear that, while the
process has been somewhat involved, the algorithms are not all that
complicated. If you've read this far, you know %ames
. The only other
code involves initialization, timeouts, and the like.
Below, we give detailed reference documentation for the data models involved.
Data Models
++fort
, formal state
++ fort :: formal state
$: %0 :: version
gad=duct :: client interface
hop=@da :: network boot date
ton=town :: security
zac=(map ship corn) :: flows by server
== ::
This is the state of our vane. Anything that must be remembered between calls to ames must be stored in this state.
%0
is the version of the ames state model itself. If the data model
++fort
changes, then this number needs to be incremented, and an
adapter must be written to upgrade the old state into the new state.
Note that this is the version number of the model itself, not the
contents. When the data changes, there is of course no need to change
this.
gad
is a duct
over which we send %send
cards to unix. This card is
initialized when unix sends a %barn
card as vere starts up. Vere
treats this duct specially -- don't send anything weird over it.
hop
is the network boot date. This is set when the %kick
card is
sent by vere on start up.
ton
is a ++town
, where we store all of our security/encryption
state. Note that this is shared across all ships on a pier.
zac
is a map of ships to ++corn
. This stores all the per-ship state.
The keys to this map are the ships on the current pier.
++town
, all security state
++ town :: all security state
$: lit=@ud :: imperial modulus
any=@ :: entropy
urb=(map ship sufi) :: all keys and routes
fak=? ::
== ::
This is the security state of our pier.
lit
is unused.
any
is 256 bits of entropy. This entropy is used and updated in
exactly two places: when we send a %junk
card, and when we generate a
new symmetric key in ++griz:lax:as:go
. When it is updated, it is
updated by a SHA-256 hash of the current time and the old value of the
entropy.
urb
is a map of ships to ++sufi
. This is where we store all the
per-ship state for the pier. The keys to this map are the ships on the
current pier.
fak
is true if we are on a fake network. This disables certain
security checks so that anyone may run a fake ~zod
. This is used only
for development. To use, run vere with the -F
option (and the
-I ~zod
option for a fake ~zod
).
++sufi
, domestic host
++ sufi :: domestic host
$: hoy=(list ship) :: hierarchy
val=wund :: private keys
law=will :: server will
seh=(map hand ,[p=ship q=@da]) :: key cache
hoc=(map ship dore) :: neighborhood
== ::
This is the security state of a domestic server.
hoy
is a list of the ships directly above us in the hierarchy of
ships. For example, for ~hoclur-bicrel
, this would be ~tasruc
and
~tug
. See ++sein
.
val
is a list of our private keys.
law
is our certificate, which is a list of the XXX
seh
hoc
is a map of ships to ++dore
. The stores all the security
informatoin about foreign ships. The keys to this map are the neighbors
(ships we have been in contact with) of this domestic server.
++wund
, private keys
++ wund (list ,[p=life q=ring r=acru]) :: mace in action
This is a list of our own private keys, indexed by life. The key itself
is the ++ring
, and the ++acru
is the encryption engine. We generate
the ++acru
from the private key by calling ++weur
. Thus, we can at
any time regenerate our ++wund
from a ++mace
. The current crypto is
at the head of the list and can be accessed with ++sen:as:go
.
++ring
, private key
++ ring ,@ :: private key
This is a private key. The first byte is reserved to identify the type
of cryptography. Lower-case means public key, upper-case means public
key, and the letter identifies which ++acru
to use.
++pass
, public key
++ pass ,@ :: public key
This is a public key. The first byte is reserved to identify the type of
cryptography. Lower-case means public key, upper-case means public key,
and the letter identifies which ++acru
to use.
++mace
, private secrets
++ mace (list ,[p=life q=ring]) :: private secrets
This is a list of the our private keys, indexed by life. From this we
can generate a ++wund
for actual use.
++skin
, encoding stem
++ skin ?(%none %open %fast %full) :: encoding stem
This defines the type of encryption used for each message. %none
refers to messages sent in the clear, %open
refers to signed messages,
%full
refers to sealed messages, and %fast
refers to symmetrically
encrypted messages. See ++acru
for details.
++acru
, asymmetric cryptosuite
++ acru :: asym cryptosuite
$_ ^? |% :: opaque object
++ as ^? :: asym ops
|% ++ seal |=([a=pass b=@ c=@] _@) :: encrypt to a
++ sign |=([a=@ b=@] _@) :: certify as us
++ sure |=([a=@ b=@] *(unit ,@)) :: authenticate from us
++ tear |= [a=pass b=@] :: accept from a
*(unit ,[p=@ q=@]) ::
-- ::
++ de |+([a=@ b=@] *(unit ,@)) :: symmetric de, soft
++ dy |+([a=@ b=@] _@) :: symmetric de, hard
++ en |+([a=@ b=@] _@) :: symmetric en
++ ex ^? :: export
|% ++ fig _@uvH :: fingerprint
++ pac _@uvG :: default passcode
++ pub *pass :: public key
++ sec *ring :: private key
--
++ nu ^? :: reconstructors
|% ++ pit |=([a=@ b=@] ^?(..nu)) :: from [width seed]
++ nol |=(a=@ ^?(..nu)) :: from naked ring
++ com |=(a=@ ^?(..nu)) :: from naked pass
--
--
This is an opaque interface for a general asymmetric cryptosuite. Any
form of asymmetric cryptography can be dropped in to be used instead of
the default. Right now, there are two cryptosuites, ++crua
, which is
your standard RSA, and ++crub
, which is elliptic curve crypto but is
mostly stubbed out at the moment.
++as:acru
, asymmetric operations
++ as ^? :: asym ops
|% ++ seal |=([a=pass b=@ c=@] _@) :: encrypt to a
++ sign |=([a=@ b=@] _@) :: certify as us
++ sure |=([a=@ b=@] *(unit ,@)) :: authenticate from us
++ tear |= [a=pass b=@] :: accept from a
*(unit ,[p=@ q=@]) ::
-- ::
This is the core that defines the standard asymmetric cryptography operations.
++seal:as:acru
allows us to send a message encrypted with someone's
public key so that only they may read it. If Alice seals a message with
Bob's public key, then she can be sure that Bob is the only one who can
read it. This is associated with the ++skin
%full
.
++sign:as:acru
allows us to sign a message with our private key so
that others can verify that we sent the message. If Alice signs a
message with her private key, then Bob can verify with her public key
that it was indeed Alice who sent it. This is associated with the
++skin
%open
.
++sure:as:acru
is the dual to ++sign:as:acru
. It allows us to verify
that a message we have received is indeed from the claimed sender. If
Alice sends a message with her private key, then Bob can use this arm to
verify that it was indeed Alice who sent it. This is associated with the
++skin
%open
.
++tear:as:acru
is the dual to ++seal:as:acru
. It allows us to read a
message that we can be sure is only read by us. If Alice seals a message
with Bob's public key, then Bob can use this arm to read it. This is
associated with the ++skin
%full
.
++de:acru
, ++dy:acru
, and ++en:acru
, symmetric encryption/decryption
++ de |+([a=@ b=@] *(unit ,@)) :: symmetric de, soft
++ dy |+([a=@ b=@] _@) :: symmetric de, hard
++ en |+([a=@ b=@] _@) :: symmetric en
Symmetric encryption is associated with the ++skin
%fast
.
++de:acru
decrypts a message with a symmetric key, returning ~
on
failure and [~ u=data]
on success.
++dy:acru
decrypts a message with a symmetric key, crashing on
failure. This should almost always be defined as, and should always be
semantically equivalent to, (need (de a b))
.
++en:acru
encrypts a message with a symmetric key.
++ex:acru
, exporting data
++ ex ^? :: export
|% ++ fig _@uvH :: fingerprint
++ pac _@uvG :: default passcode
++ pub *pass :: public key
++ sec *ring :: private key
--
++fig:ex:acru
is our fingerprint, usually a hash of our public key.
This is used, for example, in ++zeno
, where every carrier owner's
fingerprint is stored so that we can ensure that carriers are indeed
owned by their owners
++pac:ex:acru
is our default passcode, which is unused at present.
++pub:ex:acru
is the ++pass
form of our public key.
++sec:ex:acru
is the ++ring
form of our private key.
++nu:acru
, reconstructors
++ nu ^? :: reconstructors
|% ++ pit |=([a=@ b=@] ^?(..nu)) :: from [width seed]
++ nol |=(a=@ ^?(..nu)) :: from naked ring
++ com |=(a=@ ^?(..nu)) :: from naked pass
--
These arms allow us to reconstruct a ++acru
from basic data.
++pit:nu:acru
constructs a ++acru
from the width of our intended key
and seed entropy. This is usually used in the initial construction of
the ++acru
.
++nol:nu:acru
constructs a ++acru
from a "naked ring", meaning a
++ring
without the initial byte identifying the type of crypto. There
is often a helper arm that that wraps this; see ++weur
for ++crua
and ++wear
for ++crub
.
++com:nu:acru
constructs a ++acru
from a "naked pass", meaning a
++ring
without the initial byte identifying the type of crypto. There
is often a helper arm that that wraps this; see ++haul
for ++crua
and ++hail
for ++crub
.
++will
, certificate
++ will (list deed) :: certificate
This is a list of deeds associated with the current ship. There should be an item in this list for every ship from this point up in the hierarchy times the number of lives that each ship has had. For example, ~hoclur-bicrel may have a will with three items: one for itself, one for ~tasruc (who issued ~hoclur-bicrel's deed) and one for ~tug (who issued ~tasruc's deed).
++deed
, identity
++ deed ,[p=@ q=step r=?] :: sig, stage, fake?
p
is the signature of a particular deed, which is a signed copy of
q
.
q
is the stage in the identity.
r
is true if we're working on a fake network, where we don't check
that the carrier fingerprints are correct. This allows us to create fake
networks for development without interfering with the real network.
++step
, identity stage
++ step ,[p=bray q=gens r=pass] :: identity stage
This is a single stage in our identity. Thus, this is specific to a single life in a single ship. Everything in here may change between lives.
p
q
r
is the public key for this stage in the identity.
++bray
++ bray ,[p=life q=(unit life) r=ship s=@da] :: our parent us now
XXX
++gens
, general identity
++ gens ,[p=lang q=gcos] :: general identity
p
is the IETF language code for the preferred language of this
identity. This is unused at the moment, but in the future text should be
localized based on this.
q
is the description of the ship.
++gcos
, identity description
++ gcos :: id description
$% [%czar ~] :: 8-bit ship
[%duke p=what] :: 32-bit ship
[%earl p=@t] :: 64-bit ship
[%king p=@t] :: 16-bit ship
[%pawn p=(unit ,@t)] :: 128-bit ship
== ::
This is the description of the identity of a ship. Most types of
identity have a @t
field, which is their human-readable name. The
identity of a %duke
is more involved.
A %czar
, a carrier, is a ship with an 8-bit address. Thus, there are
only 256 carriers. These are at the top of the namespace hierarchy, and
the fingerprint of each carrier is stored in ++zeno
. These are the
"senators" of Urbit.
A %king
, a cruiser, is a ship with a 16-bit address. Thus, there are
65,536 cruisers. Each carrier may issue 256 cruisers. These are the
infrastructure of Urbit.
A %duke
, a destroyer, is a ship with a 32-bit address. Thus, there are
4,294,967,296 destroyers. Each cruiser may issue 65,536 cruisers. These
are the individuals of Urbit.
A %earl
, a yacht, is a ship with a 64-bit address. Thus, there are
18,446,744,073,709,551,616 yachts. Each destroyer may issue
4,294,967,296 yachts. These are the devices of Urbit.
A %pawn
, a submarine, is a ship with a 128-bit address. Thus, there
are a lot of submarines. The chance of random name collision is
negligible, so submarines are not issued by any ship. They must simply
assert their presence, and they are all considered children of ~zod.
This is the underworld of Urbit, where anonymity reigns supreme.
++what
, logical destroyer identity
++ what :: logical identity
$% [%anon ~] :: anonymous
[%lady p=whom] :: female person ()
[%lord p=whom] :: male person []
[%punk p=sect q=@t] :: opaque handle ""
== ::
This is the logical identity of a destroyer.
A %anon
is a completely anonymous destroyer. The difference between
this and a submarine is that a submarine is ephemeral while a %anon
destroyer is not. Thus, we may not know who ~hoclur-bicrel is, but we
do know that it's always the same person.
A %lady
is a female person. The name used here should be a real name.
A %lord
is a male person. The name used here should be a real name.
A %punk
is a person who is identified only by a handle.
++whom
, real person
++ whom ,[p=@ud q=govt r=sect s=name] :: year/govt/id
Ths is the information associated with a real person. It is mostly information that could be observed with the briefest of interactions.
p
is the birth year.
q
is the location of a user, usually of the form "country/zip".
r
is the sect of the user.
s
is the real name of the person.
++govt
++ govt path :: country/postcode
This is the location of the user, usually of the form "country/zip".
++sect
++ sect ?(%black %blue %red %orange %white) :: banner
XXX
++name
++ name ,[p=@t q=(unit ,@t) r=(unit ,@t) s=@t] :: first mid/nick last
This is the given name, possible middle name/initial, possible nickname, and surname of a user.