mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 10:02:47 +03:00
2347 lines
100 KiB
Plaintext
2347 lines
100 KiB
Plaintext
|
---
|
|||
|
title: Urbit Whitepaper
|
|||
|
sort: 0
|
|||
|
---
|
|||
|
|
|||
|
Urbit: an operating function
|
|||
|
============================
|
|||
|
|
|||
|
<div class="warning">This is Urbit whitepaper DRAFT 41K. Some small details
|
|||
|
remain at variance with the codebase.</div>
|
|||
|
|
|||
|
Abstract
|
|||
|
========
|
|||
|
|
|||
|
Urbit is a clean-slate, full-stack redesign of system software.
|
|||
|
In 25K lines of code, it's a packet protocol, a pure functional
|
|||
|
language, a deterministic OS, an ACID database, a versioned
|
|||
|
filesystem, a web server and a global PKI. Urbit runs on a frozen
|
|||
|
combinator interpreter specified in 200 words; the rest of the
|
|||
|
stack upgrades itself over its own network.
|
|||
|
|
|||
|
Architecturally, Urbit is an opaque computing and communication
|
|||
|
layer above Unix and the Internet. To the user, it's a new
|
|||
|
decentralized network where you own and control your own
|
|||
|
general-purpose personal server, or "planet." A planet is not a
|
|||
|
new way to host your old apps; it's a different experience.
|
|||
|
|
|||
|
Download
|
|||
|
--------
|
|||
|
[As `.md`](https://storage.googleapis.com/urbit-extra/preview-1/whitepaper.md)
|
|||
|
[As `.pdf`](https://storage.googleapis.com/urbit-extra/preview-1/whitepaper.pdf)
|
|||
|
|
|||
|
<toc></toc>
|
|||
|
|
|||
|
<div id="toc">
|
|||
|
|
|||
|
Objective
|
|||
|
=========
|
|||
|
|
|||
|
How can we put users back in control of their own computing?
|
|||
|
|
|||
|
Most people still have a general-purpose home computer, but it's
|
|||
|
atrophying into a client. Their critical data is all in the
|
|||
|
cloud. Technically, of course, that's ideal. Data centers are
|
|||
|
pretty good at being data centers.
|
|||
|
|
|||
|
But in the cloud, all users have is a herd of special-purpose
|
|||
|
appliances, not one of which is a general-purpose computer. Do
|
|||
|
users want their own general-purpose personal cloud computer? If
|
|||
|
so, why don't they have one now? How might we change this?
|
|||
|
|
|||
|
Conventional cloud computing, the way the cloud works now, is
|
|||
|
"1:n". One application, hosted on one logical server by its own
|
|||
|
developer, serves "n" users. Each account on each application is
|
|||
|
one "personal appliance" - a special-purpose computer, completely
|
|||
|
under the developer's control.
|
|||
|
|
|||
|
Personal cloud computing, the way we wish the cloud worked, is
|
|||
|
"n:1": each user has one logical server, which runs "n"
|
|||
|
independent applications. This general-purpose computer is a
|
|||
|
"personal server," completely under the user's control.
|
|||
|
|
|||
|
"Personal server" is a phrase only a marketing department could
|
|||
|
love. We prefer to say: your *planet*. Your planet is your
|
|||
|
digital identity, your network address, your filesystem and your
|
|||
|
application server. Every byte on it is yours; every instruction
|
|||
|
it runs is under your control.
|
|||
|
|
|||
|
Most people should park their planets in the cloud, because the
|
|||
|
cloud works better. But a planet is not a planet unless it's
|
|||
|
independent. A host without contractual guarantees of absolute
|
|||
|
privacy and unconditional migration is not a host, but a trap.
|
|||
|
The paranoid and those with global adversaries should migrate to
|
|||
|
their own closets while home computing remains legal.
|
|||
|
|
|||
|
But wait: isn't "planet" just a fancy word for a self-hosted
|
|||
|
server? Who wants to self-host? Why would anyone want to be their
|
|||
|
own sysadmin?
|
|||
|
|
|||
|
*Managing* your own computing is a cost, not a benefit. Your
|
|||
|
planet should be as easy as possible to manage. (It certainly
|
|||
|
should be easier than herding your "n" personal appliances.) But
|
|||
|
the benefit of "n:1" is *controlling* your own computing.
|
|||
|
|
|||
|
The "n:1" cloud is not a better way to implement your existing
|
|||
|
user experience. It's a different relationship between human and
|
|||
|
computer. An owner is not just another customer. A single-family
|
|||
|
home is not just another motel room. Control matters. A lot.
|
|||
|
|
|||
|
Perhaps this seems abstract. It's hard to imagine the "n:1" world
|
|||
|
before it exists. Let's try a thought-experiment: adding
|
|||
|
impossible levels of ownership to today's "1:n" ecosystem.
|
|||
|
|
|||
|
Take the web apps you use today. Imagine you trust them
|
|||
|
completely. Imagine any app can use any data from any other app,
|
|||
|
just because both accounts are you. Imagine you can "sidegrade"
|
|||
|
any app by moving its data safely to a compatible competitor.
|
|||
|
Imagine all your data is organized into a personal namespace, and
|
|||
|
you can compute your own functions on that namespace.
|
|||
|
|
|||
|
In this world, no app developer has any way to hold your data
|
|||
|
hostage. Forget how this works technically (it doesn't). How does
|
|||
|
it change the apps?
|
|||
|
|
|||
|
Actually, the rules of this thought-experiment world are so
|
|||
|
different that *few of the same apps exist*. Other people's apps
|
|||
|
are fundamentally different from your own apps. They're not
|
|||
|
"yours" because you developed them -- they're your apps because
|
|||
|
you can fire the developer without any pain point. You are not a
|
|||
|
hostage, so the power dynamic changes. Which changes the app.
|
|||
|
|
|||
|
For example: with other people's apps, when you want to shop on
|
|||
|
the Internets, you point your browser at amazon.com or use the
|
|||
|
Google bar as a full-text store. With your own apps, you're more
|
|||
|
likely to point your browser at your own shopping assistant. This
|
|||
|
program, which *works entirely for you and is not slipping anyone
|
|||
|
else a cut*, uses APIs to sync inventory data and send purchase
|
|||
|
orders.
|
|||
|
|
|||
|
Could you write this web app today? Sure. It would be a store.
|
|||
|
The difference between apps you control and apps you don't is the
|
|||
|
difference between a shopping assistant and a store. It would be
|
|||
|
absurd if a shopping assistant paid its developer a percentage of
|
|||
|
all transactions. It would be absurd if a store didn't. The
|
|||
|
general task is the same, but every detail is different.
|
|||
|
|
|||
|
Ultimately, the planet is a different user experience because you
|
|||
|
trust the computer more. A program running on someone else's
|
|||
|
computer can promise it's working only for you. This promise is
|
|||
|
generally false and you can't enforce it. When a program on your
|
|||
|
computer makes the same promise, it's generally true and you can
|
|||
|
enforce it. Control changes the solution because control produces
|
|||
|
trust and trust changes the problem.
|
|||
|
|
|||
|
Could we actually add this level of user sovereignty to the "1:n"
|
|||
|
cloud? It'd take a lot of engineers, a lot of lawyers, and a
|
|||
|
whole lot of standards conferences. Or we could just build
|
|||
|
ourselves some planets.
|
|||
|
|
|||
|
Obstacles
|
|||
|
=========
|
|||
|
|
|||
|
If this is such a great product, why can't you already buy it?
|
|||
|
|
|||
|
In 2015, general-purpose cloud servers are easily available. But
|
|||
|
they are industrial tools, not personal computers. Most users
|
|||
|
(consumer and enterprise) use personal appliances. They choose
|
|||
|
"1:n" over "n:1". Why does "1:n" beat "n:1" in the real world?
|
|||
|
|
|||
|
Perhaps "1:n" is just better. Your herd of developer-hosted
|
|||
|
appliances is just a better product than a self-hosted planet. Or
|
|||
|
at least, this is what the market seems to be telling us.
|
|||
|
|
|||
|
Actually, the market is telling us something more specific. It's
|
|||
|
saying: the appliance herd is a better product than a self-hosted
|
|||
|
*Unix server on the Internet*.
|
|||
|
|
|||
|
The market hasn't invalidated the abstract idea of the planet.
|
|||
|
It's invalidated the concrete product of the planet *we can
|
|||
|
actually build on the system software we actually have*.
|
|||
|
|
|||
|
In 1978, a computer was a VAX. A VAX cost \$50K and was the size
|
|||
|
of a fridge. By 1988, it would cost \$5K and fit on your desk.
|
|||
|
But if a computer is a VAX, however small or cheap, there is no
|
|||
|
such thing as a PC. And if a planet is an AWS box, there is no
|
|||
|
such thing as a planet.
|
|||
|
|
|||
|
The system software stack that 2015 inherited -- two '70s
|
|||
|
designs, Unix and the Internet -- remains a viable platform for
|
|||
|
"1:n" industrial servers. Maybe it's not a viable platform for
|
|||
|
"n:1" personal servers? Just as VAX/VMS was not a viable
|
|||
|
operating system for the PC?
|
|||
|
|
|||
|
But what would a viable platform for a personal server look like?
|
|||
|
What exactly is this stack? If it's not a Unix server on the
|
|||
|
Internet, it's an X server on network Y. What are X and Y? Do
|
|||
|
they exist?
|
|||
|
|
|||
|
Clearly not. So all we have to replace is Unix and the Internet.
|
|||
|
In other words, all we have to replace is everything. Is this an
|
|||
|
obstacle, or an opportunity?
|
|||
|
|
|||
|
A clean-slate redesign seems like the obvious path to the levels
|
|||
|
of simplicity we'll need in a viable planet. Moreover, it's
|
|||
|
actually easier to redesign Unix and the Internet than Unix *or*
|
|||
|
the Internet. Computing and communication are not separate
|
|||
|
concerns; if we design the network and OS as one system, we avoid
|
|||
|
all kinds of duplications and impedance mismatches.
|
|||
|
|
|||
|
And if not now, when? Will there ever be a clean-slate redesign
|
|||
|
of '70s system software? A significant problem demands an
|
|||
|
ambitious solution. Intuitively, clean-slate computing and the
|
|||
|
personal cloud feel like a fit. Let's see if we can make the
|
|||
|
details work. We may never get another chance to do it right.
|
|||
|
|
|||
|
Principles of platform reform
|
|||
|
-----------------------------
|
|||
|
|
|||
|
To review: we don't have planets because our antique system
|
|||
|
software stack, Unix and the Internet, is a lousy platform on
|
|||
|
which to build a planet. If we care about the product, we need to
|
|||
|
start with a new platform.
|
|||
|
|
|||
|
How can we replace Unix and the Internet? We can't. But we can
|
|||
|
tile over them, like cheap bathroom contractors. We can use the
|
|||
|
Unix/Internet, or "classical layer," as an implementation
|
|||
|
substrate for our new layer. A platform on top of a platform.
|
|||
|
Under your Brazilian slate, there's pink Color Tile from 1976.
|
|||
|
|
|||
|
"Tiling over" is the normal way we replace system software. The
|
|||
|
Internet originally ran over phone circuits; under your
|
|||
|
transatlantic TCP packets, there's an ATM switch from 1996. And
|
|||
|
of course, under your browser there's an OS from 1976.
|
|||
|
|
|||
|
After a good job of tiling over, the obsolete layer can sometimes
|
|||
|
be removed. In some cases it's useless, but would cost too much
|
|||
|
to rip out. In some cases it's still used -- a Mac doesn't (yet)
|
|||
|
boot to Safari. But arguably, that's because the browser platform
|
|||
|
is anything but a perfect tiling job.
|
|||
|
|
|||
|
One property the browser got right was total opacity. The old
|
|||
|
platform implements the new platform, but can't be visible
|
|||
|
through it. If web apps could make Unix system calls or use Unix
|
|||
|
libraries, there would be no such thing as web apps.
|
|||
|
|
|||
|
(In fact, one easy way to think of a planet is as "the browser
|
|||
|
for the server side." The browser is one universal client that
|
|||
|
hosts "n" independent client applications; the planet is one
|
|||
|
universal server that hosts "n" independent server applications.)
|
|||
|
|
|||
|
And the bathroom remains a bathroom. The new platform does the
|
|||
|
same *general* job as the old one. So to justify the reform, it
|
|||
|
has to be *much* better at its new, specific job. For instance,
|
|||
|
the browser is easily two orders of magnitude better than Unix at
|
|||
|
installing untrusted transient applications (ie, web pages).
|
|||
|
|
|||
|
Abstract targets
|
|||
|
----------------
|
|||
|
|
|||
|
Again, we have two problems: Unix and the Internet. What about
|
|||
|
each do we need to fix? What exactly is wrong with the classical
|
|||
|
layer? What qualities should our replacement have?
|
|||
|
|
|||
|
Since we're doing a clean-slate design, it's a mistake to focus
|
|||
|
too much on fixing the flaws of the old platform. The correct
|
|||
|
question is: what is the right way to build a system software
|
|||
|
stack? Besides cosmetic details like character sets, this
|
|||
|
exercise should yield the same results on Mars as Earth.
|
|||
|
|
|||
|
But we come from Earth and will probably find ourselves making
|
|||
|
normal Earth mistakes. So we can at least express our abstract
|
|||
|
design goals in normal Earth terms.
|
|||
|
|
|||
|
### A simpler OS
|
|||
|
|
|||
|
One common reaction to the personal-server proposition: "my
|
|||
|
mother is not a Linux system administrator." Neither is mine. She
|
|||
|
does own an iPhone, however. Which is also a general-purpose
|
|||
|
computer. A usability target: a planet should be as easy to
|
|||
|
manage as an iPhone.
|
|||
|
|
|||
|
(To go into way too much detail: on a planet with the usability
|
|||
|
of iOS, the user as administrator has four system configuration
|
|||
|
tasks for the casual or newbie user: (a) deciding which
|
|||
|
applications to run; (b) checking resource usage; (c) configuring
|
|||
|
API authentication to your existing Web apps (your planet wants
|
|||
|
to access, rip or even sync your silo data); and (d) maintaining
|
|||
|
a reputation database (your friends, enemies, etc). (a) and (b)
|
|||
|
exist even on iOS; (c) and (d) are inherent in any planet; (d) is
|
|||
|
common on the Web, and (c) not that rare.)
|
|||
|
|
|||
|
Could a planet just run iOS? The complexity of managing a
|
|||
|
computer approximates the complexity of its state. iOS is a
|
|||
|
client OS. Its apps are not much more than webpages. There is
|
|||
|
much more state on a server; it is much more valuable, and much
|
|||
|
more intricate, and much more durable.
|
|||
|
|
|||
|
(Moreover, an iOS app is a black box. It's running on your
|
|||
|
physical hardware, but you don't have much more control over it
|
|||
|
than if it was remote. iOS apps are not designed to share data
|
|||
|
with each other or with user-level computing tools; there is no
|
|||
|
visible filesystem, shell, etc. This is not quite the ownership
|
|||
|
experience -- it's easy to get locked in to a black box.)
|
|||
|
|
|||
|
And while an Apple product is a good benchmark for any usability
|
|||
|
goal, it's the exception that proves the rule for architecture.
|
|||
|
iOS is Unix, after all -- Unix with a billion-dollar makeover.
|
|||
|
Unix is not a turd, and Cupertino could probably polish it even
|
|||
|
if it was.
|
|||
|
|
|||
|
Simplicity is the only viable path to usability for a new
|
|||
|
platform. It's not sufficient, but it is necessary. The computer
|
|||
|
feels simple to the user not because it's presenting an illusion
|
|||
|
of simplicity, but because it really is simple.
|
|||
|
|
|||
|
While there is no precise way to measure simplicity, a good proxy
|
|||
|
is lines of code -- or, to be slightly more sophisticated,
|
|||
|
compressed code size. Technical simplicity is not actually
|
|||
|
usability, just a force multiplier in the fight for usability.
|
|||
|
But usability proper can only be assessed once the UI is
|
|||
|
complete, and the UI is the top layer by definition. So we assume
|
|||
|
this equation and target simplicity.
|
|||
|
|
|||
|
### A sane network
|
|||
|
|
|||
|
When we look at the reasons we can't have a nice planet, Unix is
|
|||
|
a small part of the problem. The main problem is the Internet.
|
|||
|
|
|||
|
There's a reason what we call "social networks" on the Internet
|
|||
|
are actually centralized systems -- social *servers*. For a "1:n"
|
|||
|
application, social integration - communication between two users
|
|||
|
of the same application - is trivial. Two users are two rows in
|
|||
|
the same database.
|
|||
|
|
|||
|
When we shift to a "n:1" model, this same integration becomes a
|
|||
|
distributed systems problem. If we're building a tictactoe app in
|
|||
|
a "1:n" design, our game is a single data structure in which
|
|||
|
moves are side effects. If we're building the same app on a
|
|||
|
network of "n:1" model, our game is a distributed system in which
|
|||
|
moves are network messages.
|
|||
|
|
|||
|
Building and managing distributed Internet systems is not easy.
|
|||
|
It's nontrivial to build and manage a centralized API. Deploying
|
|||
|
a new global peer-to-peer protocol is a serious endeavor.
|
|||
|
|
|||
|
But this is what we have to do for our tictactoe app. We have to
|
|||
|
build, for instance, some kind of identity model - because who
|
|||
|
are you playing against, an IP address? To play tictactoe, you
|
|||
|
find yourself building your own distributed social network.
|
|||
|
|
|||
|
While there are certainly asocial apps that a planet can run,
|
|||
|
it's not clear that an asocial planet is a viable product. If the
|
|||
|
cost of building a distributed social service isn't close to the
|
|||
|
cost of a building its centralized equivalent, the design isn't
|
|||
|
really successful.
|
|||
|
|
|||
|
Fortunately, while there are problems in "n:1" services that no
|
|||
|
platform can solve for you (mostly around consistency) there are
|
|||
|
set of problems with "1:n" services that no platform can solve
|
|||
|
for you (mostly around scaling). Scaling problems in "n:1" social
|
|||
|
services only arise when you're Justin Bieber and have a million
|
|||
|
friends, ie, a rare case even in a mature network. Mr. Bieber can
|
|||
|
probably afford a very nice computer.
|
|||
|
|
|||
|
### Concrete requirements
|
|||
|
|
|||
|
Here are some major features we think any adequate planet needs.
|
|||
|
They're obviously all features of Urbit.
|
|||
|
|
|||
|
#### Repeatablе computing
|
|||
|
|
|||
|
Any non-portable planet is locked in to its host. That's bad. You
|
|||
|
can have all the legal guarantees you like of unconditional
|
|||
|
migration. Freedom means nothing if there's nowhere to run to.
|
|||
|
Some of today's silos are happy to give you a tarball of your own
|
|||
|
data, but what would you do with it?
|
|||
|
|
|||
|
The strongest way to ensure portability is a deterministic,
|
|||
|
frozen, non-extensible execution model. Every host runs exactly
|
|||
|
the same computation on the same image and input, for all time.
|
|||
|
When you move that image, the only thing it notices is that its
|
|||
|
IP address has changed.
|
|||
|
|
|||
|
We could imagine a planet with an unfrozen spec, which had some
|
|||
|
kind of backward-compatible upgrade process. But with a frozen
|
|||
|
spec, there is no state outside the planet itself, no input not
|
|||
|
input to the planet itself, and no way of building a planet on
|
|||
|
one host that another host can't compute correctly.
|
|||
|
|
|||
|
Of course every computer is deterministic at the CPU level, but
|
|||
|
CPU-level determinism can't in practice record and replay its
|
|||
|
computation history. A computer which is deterministic at the
|
|||
|
semantic level can. Call it "repeatable computing."
|
|||
|
|
|||
|
#### Orthogonal persistence
|
|||
|
|
|||
|
It's unclear why we'd expose the transience semantics of the
|
|||
|
hardware memory hierarchy to either the programmer or the user.
|
|||
|
When we do so, we develop two different models for managing data:
|
|||
|
"programming languages" and "databases." Mapping between these
|
|||
|
models, eg "ORM," is the quintessence of boilerplate.
|
|||
|
|
|||
|
A simple pattern for orthogonal persistence without a separate
|
|||
|
database is "prevalence": a checkpoint plus a log of events since
|
|||
|
the checkpoint. Every event is an ACID transaction. In fact, most
|
|||
|
databases use this pattern internally, but their state transition
|
|||
|
function is not a general-purpose interpreter.
|
|||
|
|
|||
|
#### Identity
|
|||
|
|
|||
|
In the classical layer, hosts, usernames, IP addresses, domain
|
|||
|
names and public keys are all separate concepts. A planet has one
|
|||
|
routable, memorable, cryptographically independent identity which
|
|||
|
serves all these purposes.
|
|||
|
|
|||
|
The "Zooko's triangle" impossibility result tells us we can't
|
|||
|
build a secure, meaningful, decentralized identity system. Rather
|
|||
|
than surrendering one of these three goals, the planet can
|
|||
|
retreat a little bit on two of them. It can be memorable, but not
|
|||
|
meaningful; it can start as a centralized system, but
|
|||
|
decentralize itself over time. 100-80-70 is often preferable to
|
|||
|
100-100-0.
|
|||
|
|
|||
|
#### A simple typed functional language
|
|||
|
|
|||
|
Given the level of integration we're expecting in this design,
|
|||
|
it's silly to think we could get away without a new language.
|
|||
|
There's no room in the case for glue. Every part has to fit.
|
|||
|
|
|||
|
The main obstacle to functional language adoption is that
|
|||
|
functional programming is math, and most human beings are really
|
|||
|
bad at math. Even most programmers are bad at math. Their
|
|||
|
intuition of computation is mechanical, not mathematical.
|
|||
|
|
|||
|
A pure, higher-order, typed, strict language with mechanical
|
|||
|
intuition and no mathematical roots seems best positioned to
|
|||
|
defeat this obstacle. Its inference algorithm should be almost
|
|||
|
but not quite as strong as Hindley-Milner unification, perhaps
|
|||
|
inferring "forward but not backward."
|
|||
|
|
|||
|
We'd also like two other features from our types. One, a type
|
|||
|
should define a subset of values against a generic data model,
|
|||
|
the way a DTD defines a set of XML values. Two, defining a type
|
|||
|
should mean defining an executable function, whose range is the
|
|||
|
type, that verifies or normalizes a generic value. Why these
|
|||
|
features? See the next section...
|
|||
|
|
|||
|
#### High-level communication
|
|||
|
|
|||
|
A planet could certainly use a network type descriptor that was
|
|||
|
like a MIME type, if a MIME type was an executable specification
|
|||
|
and could validate incoming content automatically. After ORM,
|
|||
|
manual data validation must be the second leading cause of
|
|||
|
boilerplate. If we have a language in which a type is also a
|
|||
|
validator, the automatic validation problem seems solvable. We
|
|||
|
can get to something very like a typed RPC.
|
|||
|
|
|||
|
Exactly-once message delivery semantics are impossible unless the
|
|||
|
endpoints have orthogonal persistence. Then they're easy: use a
|
|||
|
single persistent session with monotonically increasing sequence
|
|||
|
numbers. It's nice not worrying about idempotence.
|
|||
|
|
|||
|
With an integrated OS and protocol, it's possible to fully
|
|||
|
implement the end-to-end principle, and unify the application
|
|||
|
result code with the packet acknowledgment -- rather than having
|
|||
|
three different layers of error code. Not only is every packet a
|
|||
|
transaction at the system level; every message is a transaction
|
|||
|
at the application level. Applications can even withhold an
|
|||
|
acknowledgment to signal internal congestion, sending the other
|
|||
|
end into exponential backoff.
|
|||
|
|
|||
|
It's also nice to support simple higher-level protocol patterns
|
|||
|
like publish/subscribe. We don't want every application to have
|
|||
|
to implement its own subscription queue backpressure. Also, if we
|
|||
|
can automatically validate content types, perhaps we can also
|
|||
|
also diff and patch them, making remote syncing practical.
|
|||
|
|
|||
|
The web will be around for a while. It would be great to have a
|
|||
|
web server that let a browser page with an SSO login authenticate
|
|||
|
itself as another planet, translating messages into JSON. This
|
|||
|
way, any distributed application is also a web application.
|
|||
|
|
|||
|
Finally, a planet is also a great web *client*. There is lots of
|
|||
|
interesting data behind HTTP APIs. A core mission of a planet is
|
|||
|
collecting and maintaining the secrets the user needs to manage
|
|||
|
any off-planet data. (Of course, eventually this data should come
|
|||
|
home, but it may take a while.) The planet's operating system
|
|||
|
should serve as client-side middleware that mediates the API
|
|||
|
authentication process, letting the programmer program against
|
|||
|
the private web as if it was public, using site-specific auth
|
|||
|
drivers with user-configured secrets.
|
|||
|
|
|||
|
#### Global namespace
|
|||
|
|
|||
|
The URL taught us that any global identity scheme is the root of
|
|||
|
a global namespace. But the URL also made a big mistake:
|
|||
|
mutability.
|
|||
|
|
|||
|
A global namespace is cool. An immutable (referentially
|
|||
|
transparent) global namespace, which can use any data in the
|
|||
|
universe as if it was a constant, is really cool. It's even
|
|||
|
cooler if, in case your data hasn't been created yet, your
|
|||
|
calculation can block waiting for it. Coolest of all is when the
|
|||
|
data you get back is typed, and you can use it in your typed
|
|||
|
functional language just like dereferencing a pointer.
|
|||
|
|
|||
|
Of course, an immutable namespace should be a distributed version
|
|||
|
control system. If we want typed data, it needs to be a typed
|
|||
|
DVCS, clearly again with type-specific diff and patch. Also, our
|
|||
|
DVCS (which already needs a subscription mechanism to support
|
|||
|
blocking) should be very good at reactive syncing and mirroring.
|
|||
|
|
|||
|
#### Semantic drivers
|
|||
|
|
|||
|
One unattractive feature of a pure interpreter is that it exacts
|
|||
|
an inescapable performance tax -- since an interpreter is always
|
|||
|
slower than native code. This violates the prime directive of OS
|
|||
|
architecture: the programmer must never pay for any service that
|
|||
|
doesn't get used. Impure interpreters partly solve this problem
|
|||
|
with a foreign-function interface, which lets programmers move
|
|||
|
inner loops into native code and also make system calls. An FFI
|
|||
|
is obviously unacceptable in a deterministic computer.
|
|||
|
|
|||
|
A pure alternative to the FFI is a semantic registry in which
|
|||
|
functions, system or application, can declare their semantics in
|
|||
|
a global namespace. A smart interpreter can recognize these
|
|||
|
hints, match them to a checksum of known good code, and run a
|
|||
|
native driver that executes the function efficiently. This
|
|||
|
separates policy (pure algorithm as executable specification)
|
|||
|
from mechanism (native code or even hardware).
|
|||
|
|
|||
|
#### Repository-driven updates
|
|||
|
|
|||
|
A planet owner is far too busy to manually update applications.
|
|||
|
They have to update themselves.
|
|||
|
|
|||
|
Clearly the right way to execute an application is to run it
|
|||
|
directly from the version-control system, and reload it when a
|
|||
|
new version triggers any build dependencies. But in a planet, the
|
|||
|
user is not normally the developer. To install an app is to
|
|||
|
mirror the developer's repository. The developer's commit starts
|
|||
|
the global update process by updating mirror subscriptions.
|
|||
|
|
|||
|
Of course, data structures may change, requiring the developer to
|
|||
|
include an adapter function that converts the old state. (Given
|
|||
|
orthogonal persistence, rebooting the app is not an option.)
|
|||
|
|
|||
|
Added fun is provided by the fact that apps use the content types
|
|||
|
defined in their own repository, and these types may change.
|
|||
|
Ideally these changes are backward compatible, but an updated
|
|||
|
planet can still find itself sending an un-updated planet
|
|||
|
messages that don't validate. This race condition should block
|
|||
|
until the update has fully propagated, but not throw an error up
|
|||
|
to the user level -- because no user has done anything wrong.
|
|||
|
|
|||
|
### Why not a planet built on JS or JVM?
|
|||
|
|
|||
|
Many programmers might accept our reasoning at the OS level, but
|
|||
|
get stuck on Urbit's decision not to reuse existing languages or
|
|||
|
interpreters. Why not JS, JVM, Scheme, Haskell...? The planet is
|
|||
|
isolated from the old layer, but can't it reuse mature designs?
|
|||
|
|
|||
|
One easy answer is that, if we're going to be replacing Unix and
|
|||
|
the Internet, or at least tiling over them, rewriting a bit of
|
|||
|
code is a small price to pay for doing it right. Even learning a
|
|||
|
new programming language is a small price to pay. And an
|
|||
|
essential aspect of "doing it right" is a system of components
|
|||
|
that fit together perfectly; we need all the simplicity wins we
|
|||
|
can get.
|
|||
|
|
|||
|
But these are big, hand-waving arguments. It takes more than this
|
|||
|
kind of rhetoric to justify reinventing the wheel. Let's look at
|
|||
|
a few details, trying not to get ahead of ourselves.
|
|||
|
|
|||
|
In the list above, only JS and the JVM were ever designed to be
|
|||
|
isolated. The others are tools for making POSIX system calls.
|
|||
|
Isolation in JS and the JVM is a client thing. It is quite far
|
|||
|
from clear what "node.js with browser-style isolation" would even
|
|||
|
mean. And who still uses Java applets?
|
|||
|
|
|||
|
Let's take a closer look at the JS/JVM options - not as the only
|
|||
|
interpreters in the world, just as good examples. Here are some
|
|||
|
problems we'd need them to solve, but they don't solve - not, at
|
|||
|
least, out of the box.
|
|||
|
|
|||
|
First: repeatability. JS and the JVM are not frozen, but warm;
|
|||
|
they release new versions with backward compatibility. This means
|
|||
|
they have "current version" state outside the planet proper. Not
|
|||
|
lethal but not good, either.
|
|||
|
|
|||
|
When pure, JS and then JVM are at least nominally deterministic,
|
|||
|
but they are also used mainly on transient data. It's not clear
|
|||
|
that the the actual implementations and specifications are built
|
|||
|
for the lifecycle of a planet - which must never miscompute a
|
|||
|
single bit. (ECC servers are definitely recommended.)
|
|||
|
|
|||
|
Second: orthogonal persistence. Historically, successful OP
|
|||
|
systems are very rare. Designing the language and OS as one unit
|
|||
|
seems intuitively required.
|
|||
|
|
|||
|
One design decision that helps enormously with OP is an acyclic
|
|||
|
data model. Acyclic data structures are enormously easier to
|
|||
|
serialize, to specify and validate, and of course to
|
|||
|
garbage-collect. Acyclic databases are far more common than
|
|||
|
cyclic ("network" or "object") databases. Cyclic languages are
|
|||
|
more common than acyclic languages -- but pure functional
|
|||
|
languages are acyclic, so we know acyclic programming can work.
|
|||
|
|
|||
|
(It's worth mentioning existing image-oriented execution
|
|||
|
environments - like Smalltalk and its descendants, or even the
|
|||
|
Lisp machine family. These architectures (surely Urbit's closest
|
|||
|
relatives) could in theory be adapted to use as orthogonally
|
|||
|
persistent databases, but in practice are not designed for it.
|
|||
|
For one thing, they're all cyclic. More broadly, the assumption
|
|||
|
that the image is a GUI client in RAM is deeply ingrained.)
|
|||
|
|
|||
|
Third: since a planet is a server and a server is a real OS, its
|
|||
|
interpreter should be able to efficiently virtualize itself.
|
|||
|
There are two kinds of interpreter: the kind that can run an
|
|||
|
instance of itself as a VM, and the kind that can't.
|
|||
|
|
|||
|
JS can almost virtualize itself with `eval`, but `eval` is a toy.
|
|||
|
(One of those fun but dangerous toys -- like lawn darts.) And
|
|||
|
while it's not at all the same thing, the JVM can load applets --
|
|||
|
or at least, in 1997 it could...
|
|||
|
|
|||
|
(To use some Urbit concepts we haven't met yet: with semantic
|
|||
|
drivers (which don't exist in JS or the JVM, although asm.js is a
|
|||
|
sort of substitute), we don't even abandon all the world's JS or
|
|||
|
JVM code by using Urbit. Rather, we can implement the JS or JVM
|
|||
|
specifications in Hoon, then jet-propel them with practical Unix
|
|||
|
JS or JVM engines, letting us run JS or Java libraries.)
|
|||
|
|
|||
|
Could we address any or all of these problems in the context of
|
|||
|
JS, the JVM or any other existing interpreter? We could. This
|
|||
|
does not seem likely to produce results either better or sooner
|
|||
|
than building the right thing from scratch.
|
|||
|
|
|||
|
Definition
|
|||
|
==========
|
|||
|
|
|||
|
An operating function (OF) is a logical computer whose state is a
|
|||
|
fixed function of its input history:
|
|||
|
|
|||
|
V(I) => T
|
|||
|
|
|||
|
where `T` is the state, `V` is the fixed function, `I` is the
|
|||
|
list of input events from first to last.
|
|||
|
|
|||
|
Intuitively, what the computer knows is a function of what it's
|
|||
|
heard. If the `V` function is identical on every computer for all
|
|||
|
time forever, all computers which hear the same events will learn
|
|||
|
the same state from them.
|
|||
|
|
|||
|
Is this function a protocol, an OS, or a database? All three.
|
|||
|
Like an OS, `V` specifies the semantics of a computer. Like a
|
|||
|
protocol, `V` specifies the semantics of its input stream. And
|
|||
|
like a database, `V` interprets a store of persistent state.
|
|||
|
|
|||
|
Advantages
|
|||
|
----------
|
|||
|
|
|||
|
This is a very abstract description, which doesn't make it easy
|
|||
|
to see what an OF is useful for.
|
|||
|
|
|||
|
Two concrete advantages of any practical OF (not just Urbit):
|
|||
|
|
|||
|
### Orthogonal persistencе
|
|||
|
|
|||
|
An OF is inherently a single-level store. `V(I) => T` is an
|
|||
|
equation for the lifecycle of a computer. It doesn't make any
|
|||
|
distinction between transient RAM and persistent disk.
|
|||
|
|
|||
|
The word "database" has always conflated two concepts: data that
|
|||
|
isn't transient; structures specialized for search. A substantial
|
|||
|
percentage of global programmer-hours are spent on translating
|
|||
|
data between memory and disk formats.
|
|||
|
|
|||
|
Every practical modern database computes something like
|
|||
|
`V(I) => T`. `I` is the transaction log. An OF is an ACID
|
|||
|
database whose history function (log to state) is a
|
|||
|
general-purpose computer. A transaction is an event, an event is
|
|||
|
a transaction.
|
|||
|
|
|||
|
(Obviously a practical OF, though still defined as `V(I) => T`,
|
|||
|
does not recompute its entire event history on every event.
|
|||
|
Rather, it computes some incremental iterator `U` that can
|
|||
|
compute `U(E T0) => T1`, where `E` is each event in `I`.
|
|||
|
|
|||
|
In plain English, the next state `T1` is a function `U` of the
|
|||
|
latest event `E` and the current state `T0`. We can assume that
|
|||
|
some kind of incrementality will fall out of any reasonable `V`.)
|
|||
|
|
|||
|
In any reasonable OF, you can write a persistent application by
|
|||
|
leaving your data in the structures you use it from. Of course,
|
|||
|
if you want to use specialized search structures, no one is
|
|||
|
stopping you.
|
|||
|
|
|||
|
### Repeatable computing
|
|||
|
|
|||
|
Every computer is deterministic at the CPU level, in the sense
|
|||
|
that the CPU has a manual which is exactly right. But it is not
|
|||
|
actually practical to check the CPU's work.
|
|||
|
|
|||
|
"Repeatable computing" is high-level determinism. It's actually
|
|||
|
practical to audit the validity of a repeatable computer by
|
|||
|
re-executing its computation history. For an OF, the history is
|
|||
|
the event log.
|
|||
|
|
|||
|
Not every urbit is a bank; not every urbit has to store its full
|
|||
|
log. Still, the precision of a repeatable computer also affects
|
|||
|
the user experience. (It's appalling, for instance, how
|
|||
|
comfortable the modern user is with browser hangs and crashes.)
|
|||
|
|
|||
|
Requirements
|
|||
|
------------
|
|||
|
|
|||
|
`V` can't be updated for the lifecycle of the computer. Or, since
|
|||
|
`V` is also a protocol, for the lifetime of the network.
|
|||
|
|
|||
|
So `V` needs to be perfect, which means it needs to be small.
|
|||
|
It's also easier to eradicate ambiguity in a small definition.
|
|||
|
|
|||
|
But we're designing a general-purpose computer which is
|
|||
|
programmed by humans. Thus `V` is a programming language in some
|
|||
|
sense. Few practical languages are small, simple or perfect.
|
|||
|
|
|||
|
`V` is a practical interpreter and should be reasonably fast. But
|
|||
|
`V` is also a (nonpreemptive) operating system. One universal
|
|||
|
feature of an OS is the power to virtualize user-level code. So
|
|||
|
we need a small, simple, perfect interpreter which can
|
|||
|
efficiently virtualize itself.
|
|||
|
|
|||
|
`V` is also a system of axioms in the mathematical sense. Axioms
|
|||
|
are always stated informally. This statement succeeds iff it
|
|||
|
accurately communicates the same axioms to every competent
|
|||
|
reader. Compressing the document produces a rough metric of
|
|||
|
information content: the complexity of `V`.
|
|||
|
|
|||
|
(It's possible to cheat on this test. For instance, we could
|
|||
|
design a simple `V` that only executes another interpreter, `W`.
|
|||
|
`W`, the only program written in V's language, is simply encoded
|
|||
|
in the first event. Which could be quite a large event.
|
|||
|
|
|||
|
This design achieves the precision of `V`, but not its stability.
|
|||
|
For stability, the true extent of `V` is the semantic kernel that
|
|||
|
the computer can't replace during its lifetime from events in the
|
|||
|
input history. Thus `V` properly includes `W`.)
|
|||
|
|
|||
|
There are no existing interpreters that ace all these tests, so
|
|||
|
Urbit uses its own. Our `V` is defined in 350 bytes gzipped.
|
|||
|
|
|||
|
Nouns
|
|||
|
-----
|
|||
|
|
|||
|
A value in Urbit is a *noun*. A noun is an *atom* or a *cell*. An
|
|||
|
atom is an unsigned integer of any size. A cell is an ordered
|
|||
|
pair of nouns.
|
|||
|
|
|||
|
In the system equation `V(I) => T`, `T` is a noun, `I` is a list
|
|||
|
of nouns - where a list is either `0` or a cell `[item list]`.
|
|||
|
The `V` function is defined below.
|
|||
|
|
|||
|
Nock
|
|||
|
----
|
|||
|
|
|||
|
Nock or `N` is a combinator interpreter on nouns. It's specified
|
|||
|
by these pseudocode reduction rules:
|
|||
|
|
|||
|
Nock(a) *a
|
|||
|
[a b c] [a [b c]]
|
|||
|
|
|||
|
?[a b] 0
|
|||
|
?a 1
|
|||
|
+[a b] +[a b]
|
|||
|
+a 1 + a
|
|||
|
=[a a] 0
|
|||
|
=[a b] 1
|
|||
|
=a =a
|
|||
|
|
|||
|
/[1 a] a
|
|||
|
/[2 a b] a
|
|||
|
/[3 a b] b
|
|||
|
/[(a + a) b] /[2 /[a b]]
|
|||
|
/[(a + a + 1) b] /[3 /[a b]]
|
|||
|
/a /a
|
|||
|
|
|||
|
*[a [b c] d] [*[a b c] *[a d]]
|
|||
|
|
|||
|
*[a 0 b] /[b a]
|
|||
|
*[a 1 b] b
|
|||
|
*[a 2 b c] *[*[a b] *[a c]]
|
|||
|
*[a 3 b] ?*[a b]
|
|||
|
*[a 4 b] +*[a b]
|
|||
|
*[a 5 b] =*[a b]
|
|||
|
|
|||
|
*[a 6 b c d] *[a 2 [0 1] 2 [1 c d] [1 0] 2 [1 2 3] [1 0] 4 4 b]
|
|||
|
*[a 7 b c] *[a 2 b 1 c]
|
|||
|
*[a 8 b c] *[a 7 [[7 [0 1] b] 0 1] c]
|
|||
|
*[a 9 b c] *[a 7 c 2 [0 1] 0 b]
|
|||
|
*[a 10 [b c] d] *[a 8 c 7 [0 3] d]
|
|||
|
*[a 10 b c] *[a c]
|
|||
|
|
|||
|
*a *a
|
|||
|
|
|||
|
Note that operators 6-10 are macros and not formally essential.
|
|||
|
Note also that Nock reduces invalid reductions to self, thus
|
|||
|
specifying nontermination (bottom).
|
|||
|
|
|||
|
A valid Nock reduction takes a cell `[subject formula]`. The
|
|||
|
formula defines a function that operates on the subject.
|
|||
|
|
|||
|
A valid formula is always a cell. If the head of the formula is a
|
|||
|
cell, Nock reduces head and tail separately and produces the cell
|
|||
|
of their products ("autocons"). If the head is an atom, it's an
|
|||
|
operator from `0` to `10`.
|
|||
|
|
|||
|
In operators `3`, `4`, and `5`, the formula's tail is another
|
|||
|
formula, whose product is the input to an axiomatic function. `3`
|
|||
|
tests atom/cell, `4` increments, `5` tests equality.
|
|||
|
|
|||
|
In operator `0`, the tail is a "leg" (subtree) address in the
|
|||
|
subject. Leg `1` is the root, `2n` the left child of `n`, `2n+1`
|
|||
|
the right child.
|
|||
|
|
|||
|
In operator `1`, the tail is produced as a constant. In operator
|
|||
|
`2`, the tail is a formula, producing a `[subject formula]` cell
|
|||
|
which Nock reduces again (rendering the system Turing complete).
|
|||
|
|
|||
|
In the macro department, `6` is if-then-else; `7` is function
|
|||
|
composition; `8` is a stack push; `9` is a function call; `10` is
|
|||
|
a hint.
|
|||
|
|
|||
|
Surprisingly, Nock is quite a practical interpreter, although it
|
|||
|
poses unusual implementation challenges. For instance, the only
|
|||
|
arithmetic operation is increment (see the implementation issues
|
|||
|
section for how this is practical). Another atypical feature is
|
|||
|
that Nock can neither test pointer equality, nor create cycles.
|
|||
|
|
|||
|
The fixed function
|
|||
|
------------------
|
|||
|
|
|||
|
So is `V` just Nock? Almost. Where `N` is Nock, `V` is:
|
|||
|
|
|||
|
V(I) == N(I [2 [0 3] [0 2]])
|
|||
|
|
|||
|
If Nock was not `[subject formula]` but `[formula subject]`, not
|
|||
|
`N(S F)` but `N(F S)`, V would be Nock.
|
|||
|
|
|||
|
In either case, the head `I0` of `I` is the boot formula; the
|
|||
|
tail is the boot subject. Intuitively: to interpret the event
|
|||
|
history, treat the first event as a program; run that program on
|
|||
|
the rest of history.
|
|||
|
|
|||
|
More abstractly: the event sequence `I` begins with a *boot
|
|||
|
sequence* of non-uniform events, which do strange things like
|
|||
|
executing each other. Once this sequence completes, we end up in
|
|||
|
a *main sequence* of uniform events (starting at `I5`), which
|
|||
|
actually look and act like actual input.
|
|||
|
|
|||
|
`I0`: The lifecycle formula
|
|||
|
---------------------------
|
|||
|
|
|||
|
`I0` is special because we run it in theory but not in practice.
|
|||
|
Urbit is both a logical definition and a practical interpreter.
|
|||
|
Sometimes there's tension between these goals. But even in
|
|||
|
practice, we check that `I0` is present and correct.
|
|||
|
|
|||
|
`I0` is a nontrivial Nock formula. Let's skip ahead and write it
|
|||
|
in our high-level language, Hoon:
|
|||
|
|
|||
|
=> [load rest]=.
|
|||
|
=+ [main step]=.*(rest load)
|
|||
|
|- ?~ main step
|
|||
|
$(main +.main, step (step -.main))
|
|||
|
|
|||
|
Or in pseudocode:
|
|||
|
|
|||
|
from pair $load and $rest
|
|||
|
let pair $main and $step be:
|
|||
|
the result of Nock $load run on $rest
|
|||
|
loop
|
|||
|
if $main is empty
|
|||
|
return $step
|
|||
|
else
|
|||
|
continue with
|
|||
|
$main set to tail of $main,
|
|||
|
$step set to:
|
|||
|
call function $step on the head of $main
|
|||
|
|
|||
|
In actual Nock (functional machine code, essentially):
|
|||
|
|
|||
|
[8 [2 [0 3] [0 2]] 8 [ 1 6 [5 [1 0] 0 12] [0 13] 9 2
|
|||
|
[0 2] [[0 25] 8 [0 13] 9 2 [0 4] [0 56] 0 11] 0 7] 9 2 0 1]
|
|||
|
|
|||
|
`I0` is given the sequence of events from `I1` on. It takes `I1`,
|
|||
|
the boot loader, and runs it against the rest of the sequence.
|
|||
|
`I1` consumes `I2`, `I3`, and `I4`, and then it produces the true
|
|||
|
initial state function `step` and the rest of the events from
|
|||
|
`I5` on.
|
|||
|
|
|||
|
`I0` then continues on to the main sequence, iteratively
|
|||
|
computing `V(I)` by calling the `step` function over and over on
|
|||
|
each event. Each call to `step` produces the next state,
|
|||
|
incorporating the changes resulting from the current event.
|
|||
|
|
|||
|
`I1`: the language formula
|
|||
|
--------------------------
|
|||
|
|
|||
|
The language formula, `I1`, is another nontrivial Nock formula.
|
|||
|
Its job is to compile `I3`, the Hoon compiler as source, with
|
|||
|
`I2`, the Hoon compiler as a Nock formula.
|
|||
|
|
|||
|
If `I3`, compiled with `I2`, equals `I2`, `I1` then uses `I2` to
|
|||
|
load `I4`, the Arvo source. The main event sequence begins with
|
|||
|
`I5`. The only events which are Nock formulas are `I0`, `I1`, and
|
|||
|
`I2`; the only events which are Hoon source are `I3` and `I4`.
|
|||
|
|
|||
|
`I1`, in pseudo-Hoon:
|
|||
|
|
|||
|
=> [fab=- src=+< arv=+>- seq=+>+]
|
|||
|
=+ ken=(fab [[%atom %ud] 164] src)
|
|||
|
?> =(+>.fab +.ken)
|
|||
|
[seq (fab ken arv)]
|
|||
|
|
|||
|
In pseudocode:
|
|||
|
|
|||
|
with tuple as [$fab, $src, $arv, $seq], // these are I2 I3 I4 and I5...
|
|||
|
let $ken be:
|
|||
|
call $fab on type-and-value "uint 164" and Hoon $src
|
|||
|
assert environment from $fab is equal to the value from $ken
|
|||
|
return pair of the remaining $seq and:
|
|||
|
call $fab on environment $ken and Hoon $arv
|
|||
|
|
|||
|
In actual Nock:
|
|||
|
|
|||
|
[7 [0 2] 8 [8 [0 2] 9 2 [0 4] [7 [0 3] [1 [1.836.020.833
|
|||
|
25.717] 164] 0 6] 0 11] 6 [5 [0 27] 0 5] [[0 31] 8 [0 6]
|
|||
|
9 2 [0 4] [7 [0 3] [0 2] 0 30] 0 11] 0 0]
|
|||
|
|
|||
|
(`25.717`? Urbit uses the German dot notation for large atoms.)
|
|||
|
|
|||
|
We compile the compiler source, check that it produces the same
|
|||
|
compiler formula, and then compile the OS with it.
|
|||
|
|
|||
|
Besides `I0` and `I1`, there's no Nock code in the boot sequence
|
|||
|
that doesn't ship with Hoon source.
|
|||
|
|
|||
|
`I2` and `I3`: the Hoon compiler
|
|||
|
--------------------------------
|
|||
|
|
|||
|
Again, `I2` is the Hoon compiler (including basic libraries) as a
|
|||
|
Nock formula. `I3` is the same compiler as source.
|
|||
|
|
|||
|
Hoon is a strict, typed, higher-order pure functional language
|
|||
|
which compiles itself to Nock. It avoids category theory and
|
|||
|
aspires to a relatively mechanical, concrete style.
|
|||
|
|
|||
|
The Hoon compiler works with three kinds of noun: `nock`, a Nock
|
|||
|
formula; `twig`, a Hoon AST; and `span`, a Hoon type, which
|
|||
|
defines semantics and constraints on some set of nouns.
|
|||
|
|
|||
|
There are two major parts of the compiler: the front end, which
|
|||
|
parses a source file (as a text atom) to a twig; and the back
|
|||
|
end, `ut`, which accepts a subject span and a twig, and compiles
|
|||
|
them down to a product span and a Nock formula.
|
|||
|
|
|||
|
### Back end and type system
|
|||
|
|
|||
|
The Hoon back end, (`ut`), about 1700 lines of Hoon, performs
|
|||
|
type inference and (Nock) code generation. The main method of
|
|||
|
`ut` is `mint`, with signature `$+([span twig] [span Nock])`
|
|||
|
(i.e., in pseudocode, `mint(span, twig) -> [span Nock]`).
|
|||
|
|
|||
|
Just as a Nock formula executes against a subject noun, a Hoon
|
|||
|
expression is always compiled against a subject span. From this
|
|||
|
`[span twig]`, `mint` produces a cell `[span Nock]` . The Nock
|
|||
|
formula generates a useful product from any noun in the subject
|
|||
|
span. The span describes the set of product nouns.
|
|||
|
|
|||
|
Type inference in Hoon uses only forward tracing, not unification
|
|||
|
(tracing backward) as in Hindley-Milner (Haskell, ML). Hoon needs
|
|||
|
more user annotation than a unification language, but it's easier
|
|||
|
for the programmer to follow what the algorithm is doing - just
|
|||
|
because Hoon's algorithm isn't as smart.
|
|||
|
|
|||
|
But the Hoon type system can solve most of the same problems as
|
|||
|
Haskell's, notably including typeclasses / genericity. For
|
|||
|
instance, it can infer the type of an AST by compiling a grammar
|
|||
|
in the form of an LL combinator parser (like Hoon's own grammar).
|
|||
|
|
|||
|
The Hoon type system, slightly simplified for this overview:
|
|||
|
|
|||
|
++ span $| $? %noun
|
|||
|
%void
|
|||
|
==
|
|||
|
$% [%atom p=cord]
|
|||
|
[%cell p=span q=span]
|
|||
|
[%core p=span q=(map term twig)]
|
|||
|
[%cube p=noun q=span]
|
|||
|
[%face p=term q=span]
|
|||
|
[%fork p=span q=span]
|
|||
|
[%hold p=span q=twig]
|
|||
|
==
|
|||
|
|
|||
|
In pseudocode:
|
|||
|
|
|||
|
define $span as one of:
|
|||
|
'noun'
|
|||
|
'void'
|
|||
|
'atom' with text aura
|
|||
|
'cell' of $span and $span
|
|||
|
'core' of environment $span and map of name to code AST
|
|||
|
'cube' of constant value and $span
|
|||
|
'face' of name wrapping $span
|
|||
|
'fork' of $span and alternate $span
|
|||
|
'hold' of subject $span and continuation AST
|
|||
|
|
|||
|
(The syntax `%string` means a *cord*, or atom with ASCII bytes in
|
|||
|
LSB first order. Eg, `%foo` is also 0x6f.6f66 or 7.303.014.)
|
|||
|
|
|||
|
The simplest spans are `%noun`, the set of all nouns, and
|
|||
|
`%void`, the empty set.
|
|||
|
|
|||
|
Otherwise, a span is one of `%atom`, `%cell`, `%core`, `%cube`,
|
|||
|
`%face`, `%fork` or `%hold`, each of which is parameterized with
|
|||
|
additional data.
|
|||
|
|
|||
|
An `%atom` is any atom, plus an *aura* cord that describes both
|
|||
|
what logical units the atom represents, and how to print it. For
|
|||
|
instance, `%ud` is an unsigned decimal, `%ta` is ASCII text,
|
|||
|
`%da` is a 128-bit Urbit date. `%n` is nil (`~`).
|
|||
|
|
|||
|
A `%cell` is a recursively typed cell. `%cube` is a constant.
|
|||
|
`%face` wraps another span in a name for symbolic access. `%fork`
|
|||
|
is the union of two spans.
|
|||
|
|
|||
|
A `%core` is a combination of code and data - Hoon's version of
|
|||
|
an object. It's a cell `[(map term formula) payload]`, i.e. a set
|
|||
|
of named formulas (essentially "computed attributes") and a
|
|||
|
"payload", which includes the context the core is defined in.
|
|||
|
Each arm uses the whole core as its subject.
|
|||
|
|
|||
|
One common core pattern is the *gate*, Hoon's version of a
|
|||
|
lambda. A gate is a core with a single formula, and whose payload
|
|||
|
is a cell `[arguments context]`. To call the function, replace
|
|||
|
the formal arguments with your actual argument, then apply. The
|
|||
|
context contains any data or code the function may need.
|
|||
|
|
|||
|
(A core is not exactly an object, nor a map of names to formulas
|
|||
|
a vtable. The formulas are computed attributes. Only a formula
|
|||
|
that produces a gate is the equivalent of a method.)
|
|||
|
|
|||
|
### Syntax, text
|
|||
|
|
|||
|
Hoon's front end (`vast`) is, at 1100 lines, quite a gnarly
|
|||
|
grammar. It's probably the most inelegant section of Urbit.
|
|||
|
|
|||
|
Hoon is a keyword-free, ideographic language. All alphabetic
|
|||
|
strings are user text; all punctuation is combinator syntax. The
|
|||
|
*content* of your code is always alphabetic; the *structure* is
|
|||
|
always punctuation. And there are only 80 columns per line.
|
|||
|
|
|||
|
A programming language is a UI for programmers. UIs should be
|
|||
|
actually usable, not apparently usable. Some UI tasks are harder
|
|||
|
than they appear; others are easier. Forming associative memories
|
|||
|
is easier than it looks.
|
|||
|
|
|||
|
Languages do need to be read out loud, and the conventional names
|
|||
|
for punctuation are clumsy. So Hoon replaces them:
|
|||
|
|
|||
|
ace [1 space] dot . pan ]
|
|||
|
bar | fas / pel )
|
|||
|
bis \ gap [>1 space, nl] pid }
|
|||
|
buc $ hax # ran >
|
|||
|
cab _ ket ^ rep '
|
|||
|
cen % lep ( sac ;
|
|||
|
col : lit < tar *
|
|||
|
com , lus + tec `
|
|||
|
das - mat @ tis =
|
|||
|
den " med & wut ?
|
|||
|
dip { nap [ zap !
|
|||
|
|
|||
|
For example, `%=` sounds like "centis" rather than "percent
|
|||
|
equals." Since even a silent reader will subvocalize, the length
|
|||
|
and complexity of the sound is a tax on reading the code.
|
|||
|
|
|||
|
A few digraphs also have irregular sounds:
|
|||
|
|
|||
|
== stet
|
|||
|
-- shed
|
|||
|
++ slus
|
|||
|
-> dart
|
|||
|
-< dusk
|
|||
|
+> lark
|
|||
|
+< lush
|
|||
|
|
|||
|
Hoon defines over 100 digraph ideograms (like `|=`). Ideograms
|
|||
|
(or *runes*) have consistent internal structure; for instance,
|
|||
|
every rune with the `|` prefix produces a core. There are no
|
|||
|
user-defined runes. These facts considerably mitigate the
|
|||
|
memorization task.
|
|||
|
|
|||
|
Finally, although it's not syntax, style merits a few words.
|
|||
|
While names in Hoon code can have any internal convention the
|
|||
|
programmer wants (as long as it's lowercase ASCII plus hyphen),
|
|||
|
one convention we use a lot: face names which are random
|
|||
|
three-letter syllables, arm names which are four-letter random
|
|||
|
Scrabble words.
|
|||
|
|
|||
|
Again, forming associative memories is easier than it looks.
|
|||
|
Random names quickly associate to their meanings, even without
|
|||
|
any mnemonic. A good example is the use of Greek letters in math.
|
|||
|
As with math variables, consistency helps too - use the same name
|
|||
|
for the same concept everywhere.
|
|||
|
|
|||
|
This "lapidary" convention is appropriate for code that's
|
|||
|
nontrivial, but still clean and relatively short. But code can't
|
|||
|
be clean unless it's solving a clean problem. For dirty problems,
|
|||
|
long informative names and/or kebab-case are better. For trivial
|
|||
|
problems (like defining `add`), we use an even more austere
|
|||
|
"hyperlapidary" style with one-letter names.
|
|||
|
|
|||
|
### Twig structure
|
|||
|
|
|||
|
A Hoon expression compiles to a `twig`, or AST node. A twig is
|
|||
|
always a cell.
|
|||
|
|
|||
|
Like Nock formulas, Hoon twigs "autocons." A twig whose head is a
|
|||
|
cell is a pair of twigs, which compiles to a pair of formulas,
|
|||
|
which is a formula producing a pair. ("autocons" is an homage to
|
|||
|
Lisp, whose `(cons a (cons b c))` becomes Urbit's `[a b c]`.)
|
|||
|
|
|||
|
Otherwise, a twig is a tagged union. Its tag is always a rune,
|
|||
|
stored by its phonetic name as a cord. Because it's normal for
|
|||
|
31-bit numbers to be direct (thus, somewhat more efficient), we
|
|||
|
drop the vowels. So `=+` is not `%tislus`, but `%tsls`.
|
|||
|
|
|||
|
There are too many kinds of twig to enumerate here. Most are
|
|||
|
implemented as macros; only about 25 are directly translated to
|
|||
|
Nock. There is nothing sophisticated about twigs - given the
|
|||
|
syntax and type system, probably anyone would design the same
|
|||
|
twig system.
|
|||
|
|
|||
|
### Syntax geometry
|
|||
|
|
|||
|
Just visually, one nice feature of imperative languages is the
|
|||
|
distinction between statements, which want to grow vertically
|
|||
|
down the page, and expressions, which prefer to grow
|
|||
|
horizontally.
|
|||
|
|
|||
|
Most functional languages, inherently lacking the concept of
|
|||
|
statements, have a visual problem: their expressions grow best
|
|||
|
horizontally, and often besiege the right margin.
|
|||
|
|
|||
|
Hoon has two syntax modes: "tall," which grows vertically, and
|
|||
|
"wide," which grows horizontally. Tall code can contain wide
|
|||
|
code, but not vice versa. In general, any twig can be written in
|
|||
|
either mode.
|
|||
|
|
|||
|
Consider the twig `[%tsls a b]`, where `a` and `b` are twigs.
|
|||
|
`=+` defines a variables, roughly equivalent to Lisp's `let`. `a`
|
|||
|
defines the variable for use in `b`. In wide mode it looks like
|
|||
|
an expression: `=+(a b)`. In tall mode, the separator is two
|
|||
|
spaces or a newline, without parentheses:
|
|||
|
|
|||
|
=+ a
|
|||
|
b
|
|||
|
|
|||
|
which is the same code, but looks like a "statement." The
|
|||
|
deindentation is reasonable because even though `b` is, according
|
|||
|
to the AST, "inside" the `=+`, it's more natural to see it
|
|||
|
separated out.
|
|||
|
|
|||
|
For twigs with a constant number of components, like
|
|||
|
`[%tsls a b]`, tall mode relies on the parser to stop itself.
|
|||
|
With a variable fanout we need a `==` terminator:
|
|||
|
|
|||
|
:* a
|
|||
|
b
|
|||
|
==
|
|||
|
|
|||
|
The indentation rules of constant-fanout twigs are unusual.
|
|||
|
Consider the stem `?:`, which happens to have the exact same
|
|||
|
semantics as C `?:` (if-then-else). Where in wide form we'd write
|
|||
|
`?:(a b c)`, in tall form the convention is
|
|||
|
|
|||
|
?: a
|
|||
|
b
|
|||
|
c
|
|||
|
|
|||
|
This is *backstep* indentation. Its motivation: defend the right
|
|||
|
margin. Ideally, `c` above is a longer code block than `a` or
|
|||
|
`b`. And in `c`, we have not lost any margin at all. Ideally, we
|
|||
|
can write an arbitrarily tall and complex twig that always grows
|
|||
|
vertically and never has a margin problem. Backstep indentation
|
|||
|
is tail call optimization for syntax.
|
|||
|
|
|||
|
Finally, the wide form with rune and parentheses (like `=+(a b)`)
|
|||
|
is the *normal* wide form. Hoon has a healthy variety of
|
|||
|
*irregular* wide forms, for which no principles at all apply. But
|
|||
|
using normal forms everywhere would get quite cumbersome.
|
|||
|
|
|||
|
`I4`: Arvo
|
|||
|
----------
|
|||
|
|
|||
|
`I4` is the source code for Arvo: about 600 lines of Hoon. This
|
|||
|
is excessive for what Arvo does and can probably be tightened. Of
|
|||
|
course, it does not include the kernel modules (or *vanes*).
|
|||
|
|
|||
|
`I4` formally produces the kernel step function, `step` for `I0`.
|
|||
|
But in a practical computer, we don't run `I0`, and there are
|
|||
|
other things we want to do besides `step` it. Inside `step` is
|
|||
|
the Arvo core (leg 7, context), which does the actual work.
|
|||
|
|
|||
|
Arvo's arms are `keep`, `peek`, `poke`, `wish`, `load`.
|
|||
|
|
|||
|
`wish` compiles a Hoon string. `keep` asks Arvo when it wants to
|
|||
|
be woken up next. `peek` exposes the Arvo namespace. These three
|
|||
|
are formally unnecessary, but useful in practice for the Unix
|
|||
|
process that runs Arvo.
|
|||
|
|
|||
|
The essential Arvo arm is `poke`, which produces a gate which
|
|||
|
takes as arguments the current time and an event. (Urbit time is
|
|||
|
a 128-bit atom, starting before the universe and ending after it,
|
|||
|
with 64 bits for subseconds and 64 bits for seconds, ignoring any
|
|||
|
post-2015 leap seconds.)
|
|||
|
|
|||
|
The product of the `poke` gate is a cell `[action-list Arvo]`. So
|
|||
|
Arvo, poked with the time and an event, produces a list of
|
|||
|
actions and a new Arvo state.
|
|||
|
|
|||
|
Finally, `load` is used to replace the Arvo core. When we want to
|
|||
|
change Hoon or Arvo proper, we build a new core with our new
|
|||
|
tools, then start it by passing `load` the old core's state.
|
|||
|
(State structures in the new core need not match the old, so the
|
|||
|
new core may need adapter functions.) `load` produces its new
|
|||
|
core wrapped in the canonical outer `step` function.
|
|||
|
|
|||
|
Since the language certainly needs to be able to change, the
|
|||
|
calling convention to the next generation is a Nock convention.
|
|||
|
`load` is leg `54` of the core battery, producing a gate whose
|
|||
|
formal argument at leg `6` is replaced with the Arvo state, then
|
|||
|
applied to the formula at leg `2`. Of course, future cores can
|
|||
|
use a different upgrade convention. Nothing in Arvo or Urbit is
|
|||
|
technically frozen, except for Nock.
|
|||
|
|
|||
|
State
|
|||
|
-----
|
|||
|
|
|||
|
Arvo has three pieces of dynamic state: its network address, or
|
|||
|
*plot*; an entropy pool; the kernel modules, or *vanes*.
|
|||
|
|
|||
|
Configuration overview
|
|||
|
----------------------
|
|||
|
|
|||
|
The first main-sequence event, `I5`, is `[%init plot]`, with some
|
|||
|
unique Urbit address (*plot*) whose secrets this urbit controls.
|
|||
|
The plot, an atom under 2\^128, is both a routing address and a
|
|||
|
cryptographic name. Every Urbit event log starts by setting its
|
|||
|
own unique and permanent plot.
|
|||
|
|
|||
|
From `I6` we begin to install the vanes: kernel modules. Vanes
|
|||
|
are installed (or reloaded) by a `[%veer label code]` event. Vane
|
|||
|
labels by convention are cords with zero or one letter.
|
|||
|
|
|||
|
The vane named `%$` (zero, the empty string) is the standard
|
|||
|
library. This contains library functions that aren't needed in
|
|||
|
the Hoon kernel, but are commonly useful at a higher level.
|
|||
|
|
|||
|
The standard library is compiled with the Hoon kernel (`I2`) as
|
|||
|
subject. The other vanes are compiled with the standard library
|
|||
|
(which now includes the kernel) as subject. Vane `%a` handles
|
|||
|
networking; `%c`, storage; `%f`, build logic; `%g`, application
|
|||
|
lifecycle; `%e`, http interface; `%d`, terminal interface; `%b`,
|
|||
|
timers; and `%j`, storage of secrets.
|
|||
|
|
|||
|
Mechanics
|
|||
|
---------
|
|||
|
|
|||
|
Vanes are stored as a cell of a noun with its span. At least in
|
|||
|
Urbit, there is no such thing as a dynamic type system - only a
|
|||
|
static type system executed at runtime.
|
|||
|
|
|||
|
Storing vanes with their spans has two benefits. One, it lets us
|
|||
|
type-check internal event dispatch. Two, it lets us do typed
|
|||
|
upgrades when we replace a vane. It makes routine dispatch
|
|||
|
operations incur compiler cost, but this caches well.
|
|||
|
|
|||
|
Input and output
|
|||
|
----------------
|
|||
|
|
|||
|
From the Unix process's perspective, Arvo consumes events (which
|
|||
|
Unix generates) and produces actions (which Unix executes). The
|
|||
|
most common event is hearing a UDP packet; the most common action
|
|||
|
is sending a UDP packet. But Arvo also interacts (still in an
|
|||
|
event-driven pattern) with other Unix services, including HTTP,
|
|||
|
the console, and the filesystem.
|
|||
|
|
|||
|
Events and actions share the `ovum` structure. An ovum is a cell
|
|||
|
`[wire card]`. A wire is a path - a list of cords, like
|
|||
|
`[%foo %bar %baz ~]`, usually written with the irregular syntax
|
|||
|
`/foo/bar/baz`. A card is a tagged union of all the possible
|
|||
|
types of events and effects.
|
|||
|
|
|||
|
A wire is a *cause*. For example, if the event is an HTTP request
|
|||
|
`%thus`, the wire will contain (printed to cords) the server port
|
|||
|
that received the request, the IP and port of the caller, and the
|
|||
|
socket file descriptor.
|
|||
|
|
|||
|
The data in the wire is opaque to Urbit. But an Urbit `poke`, in
|
|||
|
response to this event or to a later one, can answer the request
|
|||
|
with an action `%this` - an HTTP response. Unix parses the action
|
|||
|
wire it sent originally and responds on the right socket file
|
|||
|
descriptor.
|
|||
|
|
|||
|
Moves and ducts
|
|||
|
---------------
|
|||
|
|
|||
|
Event systems are renowned for "callback hell". As a purely
|
|||
|
functional environment, Arvo can't use callbacks with shared
|
|||
|
state mutation, a la node. But this is not the only common
|
|||
|
pitfall in the event landscape.
|
|||
|
|
|||
|
A single-threaded, nonpreemptive event dispatcher, like node or
|
|||
|
Arvo, is analogous to a multithreaded preemptive scheduler in
|
|||
|
many ways. In particular, there's a well-known duality between
|
|||
|
event flow and control flow.
|
|||
|
|
|||
|
One disadvantage of many event systems is unstructured event
|
|||
|
flow, often amounting to "event spaghetti". Indeed, the
|
|||
|
control-flow dual of an unstructured event system is `goto`.
|
|||
|
|
|||
|
Arvo is a structured event system whose dual is a call stack. An
|
|||
|
internal Arvo event is called a "move". A move has a duct, which
|
|||
|
is a list a wires, each of which is analagous to a stack frame.
|
|||
|
Moves come in two kinds: a `%pass` move calls upward, pushing a
|
|||
|
frame to the duct, while a `%give` move returns a result
|
|||
|
downward, popping a frame off the duct.
|
|||
|
|
|||
|
`%pass` contains a target vane name; a wire, which is a return
|
|||
|
pointer that defines this move's cause within the source vane;
|
|||
|
and a card, the event data. `%give` contains just the card.
|
|||
|
|
|||
|
The product of a vane event handler is a cell
|
|||
|
`[moves new-state]`, a list of moves and the vane's new state.
|
|||
|
|
|||
|
On receiving a Unix event, Arvo turns it into a `%pass` by
|
|||
|
picking a vane from the Unix wire (which is otherwise opaque),
|
|||
|
then pushes it on a move stack.
|
|||
|
|
|||
|
The Arvo main loop pops a move off the move stack, dispatches it,
|
|||
|
replaces the result vane state, and pushes the new moves on the
|
|||
|
stack. The Unix event terminates when the stack is empty.
|
|||
|
|
|||
|
To dispatch a `%pass` move sent by vane `%x`, to vane `%y`, with
|
|||
|
wire `/foo/bar`, duct `old-duct`, and card `new-card`, we pass
|
|||
|
`[[/x/foo/bar old-duct] new-card]`, a `[duct card]` cell, to the
|
|||
|
`call` method on vane `%y`. In other words, we push the given
|
|||
|
wire (adding the vane name onto the front) on to the old duct to
|
|||
|
create the new duct, and then pass that along with the card to
|
|||
|
the other vane.
|
|||
|
|
|||
|
To dispatch a `%give` move returned by vane `%x`, we check if the
|
|||
|
duct is only one wire deep. If so, return the card as an action
|
|||
|
to Unix. If not, pull the original calling vane from the top wire
|
|||
|
on the duct (by destructuring the duct as
|
|||
|
`[[vane wire] plot-duct]`), and call the `take` method on `vane`
|
|||
|
with `[plot-duct wire card]`.
|
|||
|
|
|||
|
Intuitively, a pass is a service request and a give is a service
|
|||
|
response. The wire contains the information (normalized to a list
|
|||
|
of strings) that the caller needs to route and process the
|
|||
|
service result. The effect always gets back to its cause.
|
|||
|
|
|||
|
A good example of this mechanism is any internal service which is
|
|||
|
asynchronous, responding to a request in a different *system*
|
|||
|
event than its cause - perhaps many seconds later. For instance,
|
|||
|
the `%c` vane can subscribe to future versions of a file.
|
|||
|
|
|||
|
When `%c` gets its service request, it saves it with the duct in
|
|||
|
a table keyed by the subscription path. When the future version
|
|||
|
is saved on this path, the response is sent on the duct it was
|
|||
|
received from. Again, the effect gets back to its cause.
|
|||
|
Depending on the request, there may even be multiple responses to
|
|||
|
the same request, on the same duct.
|
|||
|
|
|||
|
In practice, the structures above are slightly simplified - Arvo
|
|||
|
manages both vanes and moves as vases, `[span noun]` cells. Every
|
|||
|
dispatch is type-checked.
|
|||
|
|
|||
|
One card structure that Arvo detects and automatically unwraps is
|
|||
|
`[%meta vase]` - where the vase is the vase of a card. `%meta`
|
|||
|
can be stacked up indefinitely. The result is that vanes
|
|||
|
themselves can operate internally at the vase level - dynamically
|
|||
|
executing code just as Arvo itself does.
|
|||
|
|
|||
|
The `%g` vane uses `%meta` to expose the vane mechanism to
|
|||
|
user-level applications. The same pattern, a core which is an
|
|||
|
event transceiver, is repeated across four layers: Unix, Arvo,
|
|||
|
the `%g` vane, and the `:dojo` shell application.
|
|||
|
|
|||
|
Event security
|
|||
|
--------------
|
|||
|
|
|||
|
Arvo is a "single-homed" OS which defines one plot, usually with
|
|||
|
the label `our`, as its identity for life. All vanes are fully
|
|||
|
trusted by `our`. But internal security still becomes an issue
|
|||
|
when we execute user-level code which is not fully trusted, or
|
|||
|
work with data which is not trusted.
|
|||
|
|
|||
|
Our security model is causal, like our event system. Every event
|
|||
|
is a cause and an effect. Event security is all about *who/whom*:
|
|||
|
*who* (other than us) caused this event; *whom* (other than us)
|
|||
|
it will affect. `our` is always authorized to do everything and
|
|||
|
hear anything, so strangers alone are tracked.
|
|||
|
|
|||
|
Every event has a security `mask` with two fields `who` and
|
|||
|
`hum`, each a `(unit (set plot))`. (A `unit` is the equivalent of
|
|||
|
Haskell's `Maybe` - `(unit x`) is either `[~ x]` or `~`, where
|
|||
|
`~` is nil.)
|
|||
|
|
|||
|
If `who` is `~`, nil, anyone else could have caused this move --
|
|||
|
in other words, it's completely untrusted. If `who` is `[~ ~]`,
|
|||
|
the empty set, no one else caused this move -- it's completely
|
|||
|
trusted. Otherwise, the move is "tainted" by anyone in the set.
|
|||
|
|
|||
|
If `hum` is `~`, nil, anyone else can be affected by this move --
|
|||
|
in other words, it's completely unfiltered. If `hum` is `[~ ~]`,
|
|||
|
the empty set, no one else can hear the move -- it's completely
|
|||
|
private. Otherwise, the move is permitted to "leak" to anyone in
|
|||
|
the set.
|
|||
|
|
|||
|
Obviously, in most moves the security mask is propagated from
|
|||
|
cause to effect without changes. It's the exceptions that keep
|
|||
|
security exciting.
|
|||
|
|
|||
|
Namespace
|
|||
|
---------
|
|||
|
|
|||
|
Besides `call` and `take`, each vane exports a `scry` gate whose
|
|||
|
argument is a `path` - a list of strings, like a wire.
|
|||
|
|
|||
|
`scry` implements a global monotonic namespace - one that (a)
|
|||
|
never changes its mind, for the lifetime of the urbit; and (b)
|
|||
|
never conflicts across well-behaved urbits.
|
|||
|
|
|||
|
This invariant is semantic - it's not enforced by the type
|
|||
|
system. Getting it right is up to the vane's developer.
|
|||
|
Installing kernel modules is always a high-trust operation.
|
|||
|
|
|||
|
The product of the `scry` gate is `(unit (unit value))`.
|
|||
|
|
|||
|
So `scry` can produce `~`, meaning the path is not yet bound;
|
|||
|
`[~ ~]`, meaning the path is bound to empty; or `[~ ~ value]`, an
|
|||
|
actual value is bound. This value is of the form
|
|||
|
`[mark span noun]`, where `span` is the type of the noun and
|
|||
|
`mark` is a higher-level "type" label best compared to a MIME
|
|||
|
type or filename extension. Marks are discussed under the `%f`
|
|||
|
vane.
|
|||
|
|
|||
|
Vane activation and namespace invariant
|
|||
|
---------------------------------------
|
|||
|
|
|||
|
The vane that Arvo stores is not the core that exports `call`
|
|||
|
etc, but a gate that produces the core. The argument to this gate
|
|||
|
is a cell `[@da $+(path (unit (unit value)))]`. Note that
|
|||
|
`$+(path (unit (unit value)))` is just the function signature of
|
|||
|
the `scry` namespace.
|
|||
|
|
|||
|
So, the head of the argument is the current time; the tail is the
|
|||
|
system namespace. The general namespace is constructed from the
|
|||
|
`scry` method of the vanes. The first letter of the head of the
|
|||
|
path identifies the vane; the rest of the head, with the rest of
|
|||
|
the path, is passed to the vane. The vane core is activated just
|
|||
|
this way for all its methods, including `scry` itself.
|
|||
|
|
|||
|
This answers the question of how to expose dynamic state without
|
|||
|
violating our referential transparency invariants. If we require
|
|||
|
the query to include exactly the current time (which is
|
|||
|
guaranteed to change between events) in the path, then we are
|
|||
|
able to respond with the current state of the dynamic data. If
|
|||
|
the path doesn't contain the current time, then fail with
|
|||
|
`[~ ~]`. This technique is only needed, of course, when you don't
|
|||
|
know what the state was in the past.
|
|||
|
|
|||
|
Additionally, since each vane knows the plot it's on, a `scry`
|
|||
|
query with the plot in the path is guaranteed to be globally
|
|||
|
unique.
|
|||
|
|
|||
|
But vanes do not have to rely on these safe methods to maintain
|
|||
|
monotonicity - for instance, the `%c` revision-control vane binds
|
|||
|
paths around the network and across history.
|
|||
|
|
|||
|
### A tour of the vanes
|
|||
|
|
|||
|
The vanes are separately documented, because that's the entire
|
|||
|
point of vanes. Let's take a quick tour through the system as it
|
|||
|
currently stands, however.
|
|||
|
|
|||
|
### `%a` `%ames`: networking
|
|||
|
|
|||
|
`%a` is the network vane, currently `%ames` (2000 lines). `%ames`
|
|||
|
implements an encrypted P2P network over UDP packets.
|
|||
|
|
|||
|
As a vane, `%ames` provides a simple `%pass` service: send a
|
|||
|
message to another plot. The message is an arbitrary
|
|||
|
`[wire noun]` cell: a message channel and a message body. `%ames`
|
|||
|
will respond with a `%give` that returns either success or
|
|||
|
failure and an error dump.
|
|||
|
|
|||
|
`%ames` messages maintain causal integrity across the network.
|
|||
|
The sender does not send the actual duct that caused the message,
|
|||
|
of course, but an opaque atom mapped to it. This opaque `bone`,
|
|||
|
plus the channel itself, are the "socket" in the RFC sense.
|
|||
|
|
|||
|
Messages are ordered within this socket and delivered exactly
|
|||
|
once. (Yes, as normally defined this is impossible. Urbit can do
|
|||
|
exactly-once because `%ames` messaging is not a tool to build a
|
|||
|
consistency layer, but a tool built on the consistency layer.
|
|||
|
Thus, peers can have sequence numbers that are never reset.)
|
|||
|
|
|||
|
More subtly, local and remote causes treated are exactly the same
|
|||
|
way, improving the sense of network abstraction. CORBA taught us
|
|||
|
that it's not possible to abstract RPC over local function calls
|
|||
|
without producing a leaky abstraction. The Arvo structured event
|
|||
|
model is designed to abstract over messages.
|
|||
|
|
|||
|
One unusual feature is that `%ames` sends end-to-end acks.
|
|||
|
Acknowledging the last packet received in a message acknowledges
|
|||
|
that the message itself has been fully accepted. There is no
|
|||
|
separate message-level result code. Like its vane interface,
|
|||
|
`%ames` acks are binary: a positive ack has no data, a negative
|
|||
|
ack has a dump. Thus, Arvo is transactional at the message level
|
|||
|
as well as the packet/event level.
|
|||
|
|
|||
|
Another `%ames` technique is dropping packets. For instance, it
|
|||
|
is always a mistake to respond to a packet with bad encryption:
|
|||
|
it enables timing attacks. Any packet the receiver doesn't
|
|||
|
understand should be dropped. Either the sender is deranged or
|
|||
|
malicious, or the receiver will receive a future upgrade that
|
|||
|
makes a later retransmission of this packet make sense.
|
|||
|
|
|||
|
Messages are encrypted in a simple PKI (currently RSA, soon
|
|||
|
curve/ed25519) and AES keys are exchanged. Certificate transfer
|
|||
|
and key exchange are aggressively piggybacked, so the first
|
|||
|
packet to a stranger is always signed but not encrypted. The idea
|
|||
|
is that if you've never talked to someone before, probably the
|
|||
|
first thing you say is "hello." Which is not interesting for any
|
|||
|
realistic attacker. In the rare cases where this isn't true, it's
|
|||
|
trivial for the application to work around. (See the network
|
|||
|
architecture section for more about the PKI approach.)
|
|||
|
|
|||
|
(For packet geeks only: `%ames` implements both "STUN" and "TURN"
|
|||
|
styles of peer-to-peer NAT traversal, using parent plots (see
|
|||
|
below) as supernodes. If the sending plot lacks a current IP/port
|
|||
|
(Urbit uses random ports) for the destination, it forwards across
|
|||
|
the parent hierarchy. As we forward a packet, we attach the
|
|||
|
sender's address, in case we have a full-cone NAT that can do
|
|||
|
STUN.
|
|||
|
|
|||
|
When these indirect addresses are received, they are regarded
|
|||
|
with suspicion, and duplicate packets are sent - one forwarded,
|
|||
|
one direct. Once a direct packet is received, the indirect
|
|||
|
address is dropped. Forwarding up the hierarchy always succeeds,
|
|||
|
because galaxies (again, see below) are bound into the DNS at
|
|||
|
`galaxy.urbit.org`.)
|
|||
|
|
|||
|
### `%c` `%clay`: filesystem
|
|||
|
|
|||
|
`%c` is the filesystem, currently `%clay` (3000 lines). `%clay`
|
|||
|
is a sort of simplified, reactive, typed `git`.
|
|||
|
|
|||
|
The `%clay` filesystem uses a uniform inode structure, `ankh`. An
|
|||
|
ankh contains: a Merkle hash of this subtree, a set of named
|
|||
|
children, and possibly file data. If present, the file data is
|
|||
|
typed and marked (again, see the `%f` vane for more about marked
|
|||
|
data).
|
|||
|
|
|||
|
A path within the tree always contains the plot and desk
|
|||
|
(lightweight branch) where the file is located, which version of
|
|||
|
the file to use, and what path within the desk the file is at.
|
|||
|
|
|||
|
Paths can be versioned in three ways: by change number (for
|
|||
|
changes within this desk); by date; or by label.
|
|||
|
|
|||
|
Where `git` has multiple parallel states, `%clay` uses separate
|
|||
|
desks. For instance, where `git` creates a special merge state,
|
|||
|
`%clay` just uses a normal scratch desk. Don't detach your head,
|
|||
|
merge an old version to a scratch desk. Don't have explicit
|
|||
|
commit messages, edit a log file. `%clay` is a RISC `git`.
|
|||
|
|
|||
|
`%clay` is a typed filesystem; not only does it save a type with
|
|||
|
each file, but it uses `mark` services from `%f` to perform typed
|
|||
|
diff and patch operations that match the mark types. Obviously,
|
|||
|
Hunt-McIlroy line diff only works for text files. `%clay` can
|
|||
|
store and revise arbitrary custom data structures, not a
|
|||
|
traditional revision-control feature.
|
|||
|
|
|||
|
The `scry` namespace exported by clay uses the mode feature to
|
|||
|
export different views of the filesystem. For instance,
|
|||
|
`[%cx plot desk version path]`, which passes `%x` as the mode to
|
|||
|
clay's `scry`, produces the file (if any) at that path. `%y`
|
|||
|
produces the directory, essentially - the whole ankh with its
|
|||
|
subtrees trimmed. `%z` produces the whole ankh.
|
|||
|
|
|||
|
`%clay` is reactive; it exports a subscription interface, with
|
|||
|
both local and network access. A simple use is waiting for a file
|
|||
|
or version which has not yet been committed.
|
|||
|
|
|||
|
A more complex use of `%clay` subscription is synchronization,
|
|||
|
which is actually managed at the user level within a `%gall`
|
|||
|
application (`:hood`). It's straightforward to set up complex,
|
|||
|
multistep or even cyclical chains of change propagation.
|
|||
|
Intuitively, `git` only pulls; `%clay` both pulls and pushes.
|
|||
|
|
|||
|
Finally, the easiest way to use `%clay` is to edit files directly
|
|||
|
from Unix. `%clay` can mount and synchronize subtrees in the
|
|||
|
Urbit home directory. There are two kinds of mount: export and
|
|||
|
sync. While an export mount marks the Unix files read-only, sync
|
|||
|
mounts support "dropbox" style direct manipulation. The Unix
|
|||
|
process watches the subtree with `inotify()` or equivalent, and
|
|||
|
generates filesystem change events automagically. A desk which is
|
|||
|
synced with Unix is like your `git` working tree; a merge from
|
|||
|
this working desk to a more stable desk is like a `git` commit.
|
|||
|
|
|||
|
Access control in `%clay` is an attribute of the desk, and is
|
|||
|
either a blacklist or whitelist of plots. Creating and
|
|||
|
synchronizing desks is easy enough that the right way to create
|
|||
|
complex access patterns is to make a desk for the access pattern,
|
|||
|
and sync only what you want to share into it.
|
|||
|
|
|||
|
### `%f` `%ford`: builder
|
|||
|
|
|||
|
`%f` is the functional build system, currently `%ford` (1800
|
|||
|
lines). It might be compared to `make` et al, but loosely.
|
|||
|
|
|||
|
`%ford` builds things. It builds applications, resources, content
|
|||
|
type conversions, filter pipelines, more or less any functional
|
|||
|
computation specified at a high level. Also, it caches repeated
|
|||
|
computations and exports a dependency tracker, so that `%ford`
|
|||
|
users know when they need to rebuild.
|
|||
|
|
|||
|
`%ford` does all its building and execution in the virtual Nock
|
|||
|
interpreter, `mock`. `mock` is a Nock interpreter written in
|
|||
|
Hoon, and of course executed in Nock. (See the implementation
|
|||
|
issues section for how this is practical.)
|
|||
|
|
|||
|
`mock` gives us two extra affordances. One, when code compiled
|
|||
|
with tracing hints crashes deterministically, we get a
|
|||
|
deterministic stack trace. Two, we add a magic operator `11`
|
|||
|
(`.^` in Hoon) which dereferences the global namespace. This is
|
|||
|
of course referentially transparent, and `mock` remains a
|
|||
|
functional superset of Nock.
|
|||
|
|
|||
|
`%ford` is passed one card, `[%exec silk]`, which specifies a
|
|||
|
computation - essentially, a makefile as a noun. The `silk`
|
|||
|
structure is too large to enumerate here, but some highlights:
|
|||
|
|
|||
|
The simplest task of `%ford` is building code, by loading sources
|
|||
|
and resources from `%clay`. For kernel components, a source file
|
|||
|
is just parsed directly into a `twig`. `%ford` can do a lot of
|
|||
|
work before it gets to the `twig`.
|
|||
|
|
|||
|
The Hoon parser for a `%ford` build actually parses an extended
|
|||
|
build language wrapped around Hoon. Keeping the build
|
|||
|
instructions and the source in one file precludes a variety of
|
|||
|
exciting build mishaps.
|
|||
|
|
|||
|
For any interesting piece of code, we want to include structures,
|
|||
|
libraries, and data resources. Some of these requests should be
|
|||
|
in the same `beak` (plot/desk/version) as the code we're
|
|||
|
building; some are external code, locked to an external version.
|
|||
|
|
|||
|
`%ford` loads code from `%clay` with a *role* string that
|
|||
|
specifies its use, and becomes the head of the spur. (Ie, the
|
|||
|
first path item after plot, desk and version.) Roles: structures
|
|||
|
are in `/sur`, libraries in `/lib`, marks in `/mar`, applications
|
|||
|
in `/app`, generators in `/gen`, fabricators in `/fab`, filters
|
|||
|
in `/tip`, and anything not a Hoon file in `/doc`.
|
|||
|
|
|||
|
Data resources are especially interesting, because we often want
|
|||
|
to compile whole directory trees of resources into a single noun,
|
|||
|
a typed constant from the point of view of the programmer. Also,
|
|||
|
requests to `%clay` may block - `%ford` will have to delay giving
|
|||
|
its result, and wait until a subscription for the result returns.
|
|||
|
|
|||
|
Moreover, for any interesting build, the builder needs to track
|
|||
|
dependencies. With every build, `%ford` exports a dependency key
|
|||
|
that its customers can subscribe to, so that they get a
|
|||
|
notification when a dependency changes. For example, `%gall` uses
|
|||
|
this feature to auto-reload applications.
|
|||
|
|
|||
|
Finally, both for loading internal resources in a build and as a
|
|||
|
vane service (notably used by the web vane `%eyre`), `%ford`
|
|||
|
defines a functional namespace which is mapped over the static
|
|||
|
document space in `/doc`.
|
|||
|
|
|||
|
If we request a resource from `%ford` that does not correspond to
|
|||
|
a file in `/doc`, we search in `/fab` for the longest matching
|
|||
|
prefix of the path. The fabricator receives as its sole argument
|
|||
|
that part of the path not part of the matching prefix.
|
|||
|
|
|||
|
Thus, if we search for `/a/b/c/d`, and there's no `/doc/a/b/c/d`,
|
|||
|
then we first check for `/fab/a/b/c/d`, then `/fab/a/b/c`, and so
|
|||
|
on. If we find, for example, a `/fab/a/b`, then we run that
|
|||
|
fabricator with the argument `/c/d`. Thus, a fabricator can
|
|||
|
present an arbitrary virtual document tree.
|
|||
|
|
|||
|
The `%ford` vane also exports its namespace through `scry`, but
|
|||
|
`scry` has no way to block if a computation waits. It will just
|
|||
|
produce `~`, binding unknown.
|
|||
|
|
|||
|
Another major `%ford` feature is the mark system. While the Hoon
|
|||
|
type system works very well, it does not fill the niche that MIME
|
|||
|
types fill on the Internets. Marks are like MIME types, if MIME
|
|||
|
types came with with executable specifications which defined how
|
|||
|
to validate, convert, diff, and patch content objects.
|
|||
|
|
|||
|
The mark `%foo` is simply the core built from `/mar/foo`. If
|
|||
|
there is no `/mar/foo`, all semantics are defaulted - `%foo` is
|
|||
|
treated as `%noun`. If there is a core, it can tell `%ford` how
|
|||
|
to validate/convert from other marks (validation is always
|
|||
|
equivalent to a conversion from `%noun`, and seldom difficult,
|
|||
|
since every Hoon structure is defined as a fixpoint normalizer);
|
|||
|
convert *to* other marks; patch, diff, and merge. Obviously
|
|||
|
`%clay` uses these revision control operations, but anyone can.
|
|||
|
|
|||
|
For complex conversions, `%ford` has a simple graph analyzer that
|
|||
|
can convert, or at least try to convert, any source mark to any
|
|||
|
target mark.
|
|||
|
|
|||
|
There are two ford request modes, `%c` and `%r` - cooked and raw.
|
|||
|
A cooked request, `%fc` in `scry`, has a target mark and converts
|
|||
|
the result to it. A raw request, `%fr`, returns whatever `%ford`
|
|||
|
finds easiest to make.
|
|||
|
|
|||
|
Generators and filters, `/gen` and `/tip`, are actually not used
|
|||
|
by any other vane at present, but only by the user-level `:dojo`
|
|||
|
app, which constructs dataflow calculations in Unix pipe style.
|
|||
|
Applications, `/app`, are used only by the `%g` vane.
|
|||
|
|
|||
|
### `%g` `%gall`: application driver
|
|||
|
|
|||
|
`%g` is the application system, currently `%gall` (1300 lines)
|
|||
|
`%gall` is half process table and half systemd, sort of.
|
|||
|
|
|||
|
`%gall` runs user-level applications much as Arvo runs vanes. Why
|
|||
|
this extra layer? Vanes are kernel components written by kernel
|
|||
|
hackers, for whom expressiveness matters more than convenience. A
|
|||
|
bad kernel module can disable the OS; a bad application has to be
|
|||
|
firewalled.
|
|||
|
|
|||
|
But the basic design of an application and a vane is the same: a
|
|||
|
stateful core that's called with events and produces moves. Only
|
|||
|
the details differ, and there are too many to cover here.
|
|||
|
|
|||
|
One example is that `%gall` apps don't work with Arvo ducts
|
|||
|
directly, but opaque integers that `%gall` maps to ducts; the
|
|||
|
consequences of a bad app making a bad duct would be odd and
|
|||
|
potentially debilitating, so we don't let them touch ducts
|
|||
|
directly. Also, `%gall` does not like to crash, so to execute
|
|||
|
user-level code it uses the virtual interpeter `mock`. At the
|
|||
|
user level, the right way to signal an error is to crash and let
|
|||
|
`%gall` pick up the pieces - in a message handler, for instance,
|
|||
|
this returns the crash dump in a negative ack.
|
|||
|
|
|||
|
`%gall` uses `%ford` to build cores and track dependencies. Like
|
|||
|
vanes, `%gall` applications update when new code is available,
|
|||
|
sometimes using adapter functions to translate old state types to
|
|||
|
new ones. Unlike Arvo, `%gall` triggers updates automatically
|
|||
|
when a dependency changes.
|
|||
|
|
|||
|
Other than convenience and sanity checks, the principal value add
|
|||
|
of `%gall` is its inter-application messaging protocol. `%gall`
|
|||
|
messaging supports two communication patterns: a one-way message
|
|||
|
with a success/error result (`%poke`), and classic publish and
|
|||
|
subscribe (`%peer`).
|
|||
|
|
|||
|
`%peer` is designed for synchronizing state. The subscriber
|
|||
|
specifies a path which defines an application resource, and
|
|||
|
receives a stream of `%full` and `%diff` cards, total and
|
|||
|
incremental updates. (A simple "get" request is a special case in
|
|||
|
which there is a single full and no diffs.) Backpressure breaks
|
|||
|
the subscription if the queue of undelivered updates grows too
|
|||
|
deep. Broken subscriptions should not cause user-level errors;
|
|||
|
the user should see an error only if the subscription can't be
|
|||
|
reopened.
|
|||
|
|
|||
|
Recall that `%ames` routes messages which are arbitrary nouns
|
|||
|
between vanes on different urbits. `%gall` uses these `%ames`
|
|||
|
messages for remote urbits, and direct moves for local IPC. Thus,
|
|||
|
from the app's perspective, messaging an app on another Urbit is
|
|||
|
the same as messaging another app on the same Urbit. The
|
|||
|
abstraction does not leak.
|
|||
|
|
|||
|
Messages are untyped at the `%ames` level, but all `%gall`
|
|||
|
messages carry a mark and are validated by the receiver. Marks
|
|||
|
are not absolute and timeless - `%ford` needs a `beak` (plot,
|
|||
|
desk, version) to load the mark source.
|
|||
|
|
|||
|
When a message fails to validate, the packet is dropped silently.
|
|||
|
This puts the sender into exponential backoff retransmission. The
|
|||
|
general cause of a message that doesn't validate is that the
|
|||
|
sender's mark has received a backward-compatible update (mark
|
|||
|
updates that aren't backward compatible are a bad idea - use a
|
|||
|
new name), and the receiver doesn't have this update yet. The
|
|||
|
retransmitted packet will be processed correctly once the update
|
|||
|
has propagated.
|
|||
|
|
|||
|
With this mechanism, Urbit can update a distributed system (such
|
|||
|
as our own `:talk` network), upgrading both applications and
|
|||
|
protocols, silently without user intervention or notification.
|
|||
|
The general pattern of application distribution is that the app
|
|||
|
executes from a local desk autosynced to a remote urbit, which
|
|||
|
performs the function of an app store or distro. As this server
|
|||
|
fills its subscriptions, a period of network heterogeneity is
|
|||
|
inevitable; and so is transient unavailability, as new versions
|
|||
|
try to talk to old ones. But it resolves without hard errors as
|
|||
|
all clients are updated.
|
|||
|
|
|||
|
### `%e` `%eyre`: web server/client
|
|||
|
|
|||
|
`%e` is the web system, currently `%eyre` (1600 lines).
|
|||
|
|
|||
|
`%eyre` has three purposes. First, it is an HTTP and HTTPS
|
|||
|
client - or rather, it interfaces via actions/events to HTTP
|
|||
|
client handlers in the Unix layer.
|
|||
|
|
|||
|
Second, `%eyre` serves the `%ford` namespace over HTTP. The URL
|
|||
|
extension is parsed as a mark, but the default mark for requests
|
|||
|
is `urb`, which creates HTML and injects an autoupdate script
|
|||
|
that long-polls on the dependencies, reloading the page in the
|
|||
|
background when they change.
|
|||
|
|
|||
|
The normal way of publishing static or functional content in
|
|||
|
Urbit is to rely on `%ford` for format translation. Most static
|
|||
|
content is in markdown, the `%md` mark. Dynamic content is best
|
|||
|
generated with the `sail` syntax subsystem in Hoon, which is is
|
|||
|
essentially an integrated template language that reduces to XML.
|
|||
|
|
|||
|
Third, `%eyre` acts as a client for `%gall` apps, translating the
|
|||
|
`%gall` message flow into JSON over HTTP. `%poke` requests become
|
|||
|
POST requests, `%peer` becomes a long-poll stream. Our `urb.js`
|
|||
|
framework is a client-side wrapper for these requests.
|
|||
|
|
|||
|
The abstraction does not leak. On the server side, all it takes
|
|||
|
to support web clients is ensuring that outbound marks print to
|
|||
|
`%json` and inbound marks parse from `%json`. (The standard
|
|||
|
library includes JSON tools.) The `%gall` application does not
|
|||
|
even know it's dealing with a web client, all it sees are
|
|||
|
messages and subscriptions, just like it would receive from
|
|||
|
another Urbit app.
|
|||
|
|
|||
|
The dataflow pattern of `%gall` subscriptions is ideal for React
|
|||
|
and similar "one-way data binding" client frameworks.
|
|||
|
|
|||
|
Of course, `%gall` apps expect to be talking to an urbit plot, so
|
|||
|
web clients need to (a) identify themselves and (b) talk over
|
|||
|
HTTPS. `%eyre` contains a single sign-on (SSO) flow that
|
|||
|
authenticates an urbit user either to her own server or her
|
|||
|
friends'.
|
|||
|
|
|||
|
Ideally, an urbit named `~plot` is DNSed to `plot.urbit.org`. If
|
|||
|
you use your urbit through the web, you'll have an insecure
|
|||
|
cookie on `*.urbit.org`. Other urbits read this and drive the SSO
|
|||
|
flow; if you're `~tasfyn-partyv`, you can log in as yourself to
|
|||
|
`~talsur-todres.urbit.org`. The SSO confirmation message is sent
|
|||
|
directly as an `%ames` message between the `%eyre` vanes on the
|
|||
|
respective urbits.
|
|||
|
|
|||
|
Urbit also has an nginx configuration and node cache server,
|
|||
|
which (a) let a relatively slow Urbit server drive reasonably
|
|||
|
high request bandwidth, and (b) serve HTTPS by proxy.
|
|||
|
|
|||
|
Note that while external caching helps `%ford` style functional
|
|||
|
publishing, it does not help `%gall` style clients, which use
|
|||
|
server resources directly. Urbit is a personal server; it can
|
|||
|
handle an HN avalanche on your blog, but it's not designed to
|
|||
|
power your new viral startup.
|
|||
|
|
|||
|
### `%j` `%jael`: secret storage
|
|||
|
|
|||
|
`%j`, currently `%jael` (200 lines), saves secrets in a tree.
|
|||
|
Types of secrets that belong in `%jael`: Urbit private keys,
|
|||
|
Urbit symmetric keys, web API user keys and/or passwords, web API
|
|||
|
consumer (application) keys.
|
|||
|
|
|||
|
`%jael` has no fixed schema and is simply a classic tree
|
|||
|
registry. Besides a simpler security and type model, the main
|
|||
|
difference between secrets and ordinary `%clay` data is that
|
|||
|
secrets expire - sometimes automatically, sometimes manually.
|
|||
|
When another vane uses a `%jael` secret, it must register to
|
|||
|
receive an expiration notice.
|
|||
|
|
|||
|
### `%d` `%dill`: console and Unix
|
|||
|
|
|||
|
`%d`, currently `%dill` (450 lines) handles the terminal and
|
|||
|
miscellaneous Unix interfaces, mainly for initialization and
|
|||
|
debugging.
|
|||
|
|
|||
|
The console terminal abstraction, implemented directly with
|
|||
|
`terminfo` in raw mode, gives Urbit random-access control over
|
|||
|
the input line. Keystrokes are not echoed automatically. Output
|
|||
|
lines are write-only and appear above the input line.
|
|||
|
|
|||
|
`%dill` also starts default applications, measures system memory
|
|||
|
consumption, dumps error traces, etc.
|
|||
|
|
|||
|
### `%b` `%behn`: timers
|
|||
|
|
|||
|
`%b`, currently `%behn` (250 lines), is a timer vane that
|
|||
|
provides a simple subscription service to other vanes.
|
|||
|
|
|||
|
Base applications
|
|||
|
-----------------
|
|||
|
|
|||
|
Arvo ships with three major default applications: `:hood` (1600
|
|||
|
lines), `:dojo` (700 lines), and `:talk` (1600 lines).
|
|||
|
|
|||
|
### `:dojo`: a shell
|
|||
|
|
|||
|
`:dojo` is a shell for ad-hoc computation. A dojo command is a
|
|||
|
classic filter pipeline, with sources, filters, and sinks.
|
|||
|
|
|||
|
Sources can be shell variables, `%clay` files, immediate
|
|||
|
expressions, source files in `/gen`, or uploads (Unix files in
|
|||
|
`~/urbit/$plot/.urb/get`). There are three kinds of generator:
|
|||
|
simple expressions, dialog cores, and web scrapers. Generators
|
|||
|
are executed by `%ford` through `mock`, and can read the Urbit
|
|||
|
namespace via virtual operator `11`.
|
|||
|
|
|||
|
Urbit does not use short-lived applications like Unix commands. A
|
|||
|
`%gall` application is a Unix daemon. A generator is not like a
|
|||
|
Unix process at all; it cannot send moves. A dialog generator,
|
|||
|
waiting for user input, does not talk to the console; it tells
|
|||
|
`:dojo` what to say to the console. A web scraper does not talk
|
|||
|
to `%eyre`; it tells `:dojo` what resources it needs (GET only).
|
|||
|
This is POLA (principle of least authority); it makes the command
|
|||
|
line less powerful and thus less scary.
|
|||
|
|
|||
|
`:dojo` filters are immediate expressions or `/tip` source files.
|
|||
|
Sinks are console output, shell variables, `%clay`, or downloads
|
|||
|
(Unix files in `~/urbit/$plot/.urb/put`). (The upload/download
|
|||
|
mechanism is a console feature, not related to `%clay` sync -
|
|||
|
think of browser uploading and downloading.)
|
|||
|
|
|||
|
### `:talk`: a communication bus
|
|||
|
|
|||
|
`:talk` is a distributed application for sharing *telegrams*, or
|
|||
|
typed social messages. Currently we use `:talk` as a simple chat
|
|||
|
service, but telegram distribution is independent of type.
|
|||
|
|
|||
|
Every urbit running `:talk` can create any number of "stations."
|
|||
|
Every telegram has a unique id and a target audience, to which
|
|||
|
its sender uploads it. But `:talk` stations also subscribe to
|
|||
|
each other to construct feeds.
|
|||
|
|
|||
|
Mutually subscribing stations will mirror, though not preserving
|
|||
|
message order. A `:talk` station is not a decentralized entity;
|
|||
|
but urbits can use subscription patterns to federate into
|
|||
|
"global" stations - very much as in NNTP (Usenet).
|
|||
|
|
|||
|
Stations have a flexible admission control model, with a blanket
|
|||
|
policy mode and an exception list, that lets them serve as
|
|||
|
"mailboxes" (public write), "journals" (public read), "channels"
|
|||
|
(public read/write), or "chambers" (private read/write).
|
|||
|
|
|||
|
Subscribers are also notified of presence and configuration
|
|||
|
changes, typing indicator, etc. Also, telegrams contain a
|
|||
|
"flavor", which senders can use to indicate categories of content
|
|||
|
that other readers may prefer not to see.
|
|||
|
|
|||
|
`:talk` is usable both a command-line application and a reactive
|
|||
|
web client.
|
|||
|
|
|||
|
### `:hood`: a system daemon
|
|||
|
|
|||
|
`:hood` is a classic userspace system daemon, like Unix `init`.
|
|||
|
It's a system component, but it's in userspace because it can be.
|
|||
|
|
|||
|
`:hood` is actually a compound application made of three
|
|||
|
libraries, `/helm`, `/drum`, and `/kiln`. `/helm` manages the
|
|||
|
PKI; `/drum` multiplexes the console; `/kiln` controls `%clay`
|
|||
|
sync.
|
|||
|
|
|||
|
`/drum` routes console activity over `%gall` messages, and can
|
|||
|
connect to both local and foreign command-line interfaces - ssh,
|
|||
|
essentially.
|
|||
|
|
|||
|
One pattern in Urbit console input is that an application doesn't
|
|||
|
just parse its input after a line is is entered, but on each
|
|||
|
character. This way, we can reject or correct syntax errors as
|
|||
|
they happen. It's a much more pleasant user experience.
|
|||
|
|
|||
|
But since both sides of a conversation (the user and the
|
|||
|
application) are making changes to a single shared state (the
|
|||
|
input line), we have a reconciliation problem. The Urbit console
|
|||
|
protocol uses operational transformation (like Google Wave or
|
|||
|
`git replot`) for eventual consistency.
|
|||
|
|
|||
|
`/helm` manages public keys and (in future) hosted urbits. See
|
|||
|
the network architecture section below.
|
|||
|
|
|||
|
`/kiln` implements mirroring and synchronization. Any desk can
|
|||
|
mirror any other desk (given permission). Mirrors can form
|
|||
|
cycles -- a two-way mirror is synchronization.
|
|||
|
|
|||
|
Implementation issues
|
|||
|
---------------------
|
|||
|
|
|||
|
We've left a couple of knotty implementation issues unresolved up
|
|||
|
until now. Let's resolve them.
|
|||
|
|
|||
|
### Jets
|
|||
|
|
|||
|
How can an interpreter whose only arithmetic operator is
|
|||
|
increment compute efficiently? For instance, the only way to
|
|||
|
decrement `n` is to count up to `n - 1`, which is O(n).
|
|||
|
|
|||
|
Obviously, the solution is: a sufficiently smart optimizer.
|
|||
|
|
|||
|
A sufficiently smart optimizer doesn't need to optimize every
|
|||
|
Nock formula that could calculate a decrement function. It only
|
|||
|
needs to optimize one: the one we actually run.
|
|||
|
|
|||
|
The only one we run is the one compiled from the decrement
|
|||
|
function `dec` in the Hoon standard library. So there's no sense
|
|||
|
in which our sufficiently smart optimizer needs to *analyze* Nock
|
|||
|
formulas to see if they're decrement formulas. It only needs to
|
|||
|
*recognize* the standard `dec`.
|
|||
|
|
|||
|
The easiest way to do this is for the standard `dec` to declare,
|
|||
|
with a hint (Nock `10`), in some formalized way, that it is a
|
|||
|
decrement formula. The interpreter implementation can check this
|
|||
|
assertion by simple comparison - it knows what formula the
|
|||
|
standard `dec` compiles to. Our sufficiently smart optimizer
|
|||
|
isn't very smart at all!
|
|||
|
|
|||
|
The C module that implements the efficient decrement is called a
|
|||
|
"jet." The jet system should not be confused with an FFI: a jet
|
|||
|
has *no* reason to make system calls, and should never be used to
|
|||
|
produce side effects. Additionally, a jet is incorrect unless it
|
|||
|
accurately duplicates an executable specification (the Hoon
|
|||
|
code). Achieving jet correctness is difficult, but we can
|
|||
|
spot-check it easily by running both soft and hard versions.
|
|||
|
|
|||
|
Jets separate mechanism and policy in Nock execution. Except for
|
|||
|
perceived performance, neither programmer nor user has any
|
|||
|
control over whether any formula is jet-propelled. A jet can be
|
|||
|
seen as a sort of "software device driver," although invisible
|
|||
|
integration of exotic hardware (like FPGAs) is another use case.
|
|||
|
And jets do not have to be correlated with built-in or low-level
|
|||
|
functionality; for instance, Urbit has a markdown parser jet.
|
|||
|
|
|||
|
Jets are of course the main fail vector for both computational
|
|||
|
correctness and security intrusion. Fortunately, jets don't make
|
|||
|
system calls, so sandboxing policy issues are trivial, but the
|
|||
|
sandbox transition needs to be very low-latency. Another approach
|
|||
|
would be a safe systems language, such as Rust.
|
|||
|
|
|||
|
The correctness issue is more interesting, because errors happen.
|
|||
|
They are especially likely to happen early in Urbit's history. A
|
|||
|
common scenario will be that the host audits an urbit by
|
|||
|
re-executing all the events, and produces a different state. In
|
|||
|
this case, the urbit must become a "bastard" - logically
|
|||
|
instantiated at the current state. The event log is logically
|
|||
|
discarded as meaningless. Hosting a bastard urbit is not a huge
|
|||
|
problem, but if you have one you want to know.
|
|||
|
|
|||
|
In the long run, jet correctness is an excellent target problem
|
|||
|
for fuzz testers. A sophisticated implementation might even put
|
|||
|
10% of runtime into automatic jet testing. While it's always hard
|
|||
|
for implementors to match a specification perfectly, it's much
|
|||
|
easier with an executable specification that's only one or two
|
|||
|
orders of magnitude slower.
|
|||
|
|
|||
|
### Event planning
|
|||
|
|
|||
|
How do we actually process Urbit events? If Urbit is a database,
|
|||
|
how does our database execute and maintain consistency in
|
|||
|
practice, either on a personal computer or a normal cloud server?
|
|||
|
How does orthogonal persistence work? Can we use multiple cores?
|
|||
|
|
|||
|
An event is a transaction. A transaction either completes or
|
|||
|
doesn't, and we can't execute its side effects until we have
|
|||
|
committed it. For instance, if an incoming packet causes an
|
|||
|
outgoing packet, we can't send the outgoing packet until we've
|
|||
|
committed the incoming one to durable storage.
|
|||
|
|
|||
|
Unfortunately, saving even a kilobyte of durable storage on a
|
|||
|
modern PC, with full write-through sync, can take 50 to 100ms.
|
|||
|
Solid-state storage improves this, but the whole machine is just
|
|||
|
not designed for low-latency persistence.
|
|||
|
|
|||
|
In the cloud the situation is better. We can treat consensus on a
|
|||
|
nontrivial Raft cluster in a data center as persistence, even
|
|||
|
though the data never leaves RAM. Urbit is highly intolerant of
|
|||
|
computation error, for obvious reasons, and should be run in an
|
|||
|
EMP shielded data center on ECC memory.
|
|||
|
|
|||
|
There are a number of open-source reliable log processors that
|
|||
|
work quite well. We use Apache Kafka.
|
|||
|
|
|||
|
With either logging approach, the physical architecture of an
|
|||
|
Urbit implementation is clear. The urbit is stored in two forms:
|
|||
|
an event log, and a checkpoint image (saved periodically, always
|
|||
|
between events). The log can be pruned at the last reliably
|
|||
|
recorded checkpoint, or not. This design (sometimes called
|
|||
|
"prevalence") is widely used and supported by common tools.
|
|||
|
|
|||
|
The checkpointing process is much easier because Urbit has no
|
|||
|
cycles and needs no tracing garbage collector. Without careful
|
|||
|
tuning, a tracing GC tends to turn all memory available to it
|
|||
|
into randomly allocated memory soup. The Urbit interpreter uses a
|
|||
|
region system with a copy step to deallocate the whole region
|
|||
|
used for processing each event.
|
|||
|
|
|||
|
Events don't always succeed. How does a functional operating
|
|||
|
system deal with an infinite loop? The loop has to be
|
|||
|
interrupted, as always. How this happens depends on the event. If
|
|||
|
the user causes an infinite loop from a local console event, it's
|
|||
|
up to the user to interrupt it with \^C. Network packets have a
|
|||
|
timeout, currently a minute.
|
|||
|
|
|||
|
When execution fails, we want a stack trace. But Urbit is a
|
|||
|
deterministic computer and the stack trace of an interrupt is
|
|||
|
inherently nondeterministic. How do we square this circle?
|
|||
|
|
|||
|
Urbit is deterministic, but it's a function of its input. If an
|
|||
|
event crashes, we don't record the event in `I`. Instead, we
|
|||
|
record a `%crud` card that contains the stack trace and the
|
|||
|
failed event. To Urbit, this is simply another external event.
|
|||
|
|
|||
|
Processing of `%crud` depends on the event that caused it. For
|
|||
|
keyboard input, we print the error to the screen. For a packet,
|
|||
|
we send a negative ack on the packet, with the trace. For
|
|||
|
instance, if you're at the console of urbit `A` and logged in
|
|||
|
over the network to `B`, and you run an infinite loop, the `B`
|
|||
|
event loop will time out; the network console message that `A`
|
|||
|
sent to `B` will return a negative ack; the console application
|
|||
|
on `A` will learn that its message failed, and print the trace.
|
|||
|
|
|||
|
Finally, the possibilities of aggressive optimization in event
|
|||
|
execution haven't been explored. Formally, Urbit is a serial
|
|||
|
computer - but it's probable that a sophisticated, mature
|
|||
|
implementation would find a lot of ways to cheat. As always, a
|
|||
|
logically simple system is the easiest system to hack for speed.
|
|||
|
|
|||
|
Network and PKI architecture
|
|||
|
----------------------------
|
|||
|
|
|||
|
Two more questions we've left unanswered: how you get your urbit
|
|||
|
plot, and how packets get from one plot to another.
|
|||
|
|
|||
|
Again, a plot is both a digital identity and a routing address.
|
|||
|
Imagine IPv4 if you owned your own address, converted it to a
|
|||
|
string that sounded like a foreign name, and used it as your
|
|||
|
Internet handle.
|
|||
|
|
|||
|
Bases are parsed and printed with the `%p` aura, which is
|
|||
|
designed to make them as human-memorable as possible. `%p` uses
|
|||
|
phonemic plot-256 and renders smaller plots as shorter strings:
|
|||
|
|
|||
|
8 bits galaxy ~syd
|
|||
|
16 bits star ~delsym
|
|||
|
32 bits planet ~mighex-forfem
|
|||
|
64 bits moon ~dabnev-nisseb-nomlec-sormug
|
|||
|
128 bits comet ~satnet-rinsyr-silsec-navhut--bacnec-todmeb-sarseb-pagmul
|
|||
|
|
|||
|
Of course, not everyone who thinks he's Napoleon is. For the
|
|||
|
urbit to actually send and receive messages under the plot it
|
|||
|
assigns itself in the `%init` event (`I5`), later events must
|
|||
|
convey the secrets that authenticate it. In most cases this means
|
|||
|
a public key, though some urbits are booted with a symmetric
|
|||
|
session key that only works with the parent plot.
|
|||
|
|
|||
|
How do you get a plot? Comets are hashes of a random public key.
|
|||
|
"Civil" non-comets are issued by their parent plot - the
|
|||
|
numerical prefix in the next rank up: moons by planet, planets by
|
|||
|
star, stars by galaxy. The fingerprints of the initial galactic
|
|||
|
public keys (currently test keys) are hardcoded in `%ames`.
|
|||
|
|
|||
|
The comet network is fully decentralized and "free as in beer,"
|
|||
|
so unlikely to be useful. Any network with disposable identities
|
|||
|
can only be an antisocial network, because disposable identities
|
|||
|
cannot be assigned default positive reputation. Perhaps comets
|
|||
|
with nontrivial hashcash in their plots could be an exception,
|
|||
|
but nonmemorable plots remain a serious handicap.
|
|||
|
|
|||
|
The civil network is "semi-decentralized" - a centralized system
|
|||
|
designed to evolve into a decentralized one. Urbit does not use a
|
|||
|
blockchain - address space is digital land, not digital money.
|
|||
|
|
|||
|
The initial public key of a civil plot is signed by its parent.
|
|||
|
But galaxies, stars and planets are independent once they've been
|
|||
|
created - they sign their own updates. (Moons are dependent;
|
|||
|
their planet signs updates.)
|
|||
|
|
|||
|
The certificate or `will` is a signing chain; only the last key
|
|||
|
is valid; longer wills displace their prefixes. Thus update is
|
|||
|
revocation, and revocation is a pinning race. To securely update
|
|||
|
a key is to ensure that the new will reaches any peer before any
|
|||
|
messages signed by an attacker who has stolen the old key.
|
|||
|
|
|||
|
The engineering details of this race depend on the actual threat
|
|||
|
model, which is hard to anticipate. If two competing parties have
|
|||
|
the same secret, no algorithm can tell who is in the right. Who
|
|||
|
pins first should win, and there will always be a time window
|
|||
|
during which the loser can cause problems. Aggressively flooding
|
|||
|
and expiring certificates reduces this window; caching and lazy
|
|||
|
distribution expands it. There is no tradeoff-free solution.
|
|||
|
|
|||
|
Broadly, the design difference between Urbit and a blockchain
|
|||
|
network is that blockchains are "trust superconductors" - they
|
|||
|
eliminate any dependency on social, political or economic trust.
|
|||
|
Urbit is a "trust conductor" - engineered to minimize, but not
|
|||
|
eliminate, dependencies on trust.
|
|||
|
|
|||
|
For instance, bitcoin prevents double-spending with global mining
|
|||
|
costs in multiple dollars per transaction (as of 2015). Trusted
|
|||
|
transaction intermediation is an easily implemented service whose
|
|||
|
cost is a tiny fraction of this. And the transaction velocity of
|
|||
|
money is high; transactions in land are far more rare.
|
|||
|
|
|||
|
Another trust engineering problem in Urbit is the relationship
|
|||
|
between a plot and its parent hierarchy. The hierarchy provides a
|
|||
|
variety of services, starting with peer-to-peer routing. But
|
|||
|
children need a way to escape from bad parents.
|
|||
|
|
|||
|
Urbit's escape principle: (a) any planet can depend on either its
|
|||
|
star's services, or its galaxy's; (b) any star can migrate to any
|
|||
|
galaxy; (c) stars and galaxies should be independently and
|
|||
|
transparently owned.
|
|||
|
|
|||
|
Intuitively, an ordinary user is a planet, whose governor is its
|
|||
|
star, and whose appeals court is its galaxy. Since a star or
|
|||
|
galaxy should have significant reputation capital, it has an
|
|||
|
economic incentive to follow the rules. The escape system is a
|
|||
|
backup. And the comet network is a backup to the backup.
|
|||
|
|
|||
|
From a privacy perspective, a planet is a personal server; a star
|
|||
|
or galaxy is a public service. Planet ownership should be private
|
|||
|
and secret; star or galaxy ownership should be public and
|
|||
|
transparent. Since, for the foreseeable future, individual
|
|||
|
planets have negligible economic value, Urbit is not a practical
|
|||
|
money-laundering tool. This is a feature, not a bug.
|
|||
|
|
|||
|
Finally, a general principle of both repositories and republics
|
|||
|
is that the easier it is (technically) to fork the system, the
|
|||
|
harder it is (politically) to fork. Anyone could copy Urbit and
|
|||
|
replace the galactic fingerprint block. Anyone can also fork the
|
|||
|
DNS. If the DNS was mismanaged badly enough, someone could and
|
|||
|
would; since it's competently managed, everyone can't and won't.
|
|||
|
Forkability is an automatic governance corrector.
|
|||
|
|
|||
|
From personal server to digital republic
|
|||
|
----------------------------------------
|
|||
|
|
|||
|
Republics? Any global system needs a political design. Urbit is
|
|||
|
designed as a a *digital republic*.
|
|||
|
|
|||
|
The word *republic* is from Latin, "res publica" or "public
|
|||
|
thing." The essence of republican goverment is its
|
|||
|
*constitutional* quality - government by law, not people.
|
|||
|
|
|||
|
A decentralized network defined by a deterministic interpreter
|
|||
|
comes as close to a digital constitution as we can imagine. Urbit
|
|||
|
is not the only member of this set - bitcoin is another; ethereum
|
|||
|
is even a general-purpose computer.
|
|||
|
|
|||
|
The "law" of bitcoin and ethereum is self-enforcing; the "law" of
|
|||
|
Urbit is not. Urbit is not a blockchain, and no urbit can assume
|
|||
|
that any other urbit is computing the Nock function correctly.
|
|||
|
|
|||
|
But at least the distinction between correct and incorrect
|
|||
|
computing is defined. In this sense, Nock is Urbit's
|
|||
|
constitution. It's not self-enforcing like Ethereum, but it's
|
|||
|
exponentially more efficient.
|
|||
|
|
|||
|
Non-self-verifying rules are useful, too. Defining correctness is
|
|||
|
not enforcing correctness. But Urbit doesn't seek to eliminate
|
|||
|
its dependency on conventional trust. Correctness precisely
|
|||
|
defined is easily enforced with social tools: either the
|
|||
|
computation is tampered with or it isn't.
|
|||
|
|
|||
|
Conclusion
|
|||
|
==========
|
|||
|
|
|||
|
On the bottom, Urbit is an equation. In the middle it's an
|
|||
|
operating system. On the top it's a civilization -- or at least,
|
|||
|
a design for one.
|
|||
|
|
|||
|
When we step back and look at that civilization, what we see
|
|||
|
isn't that surprising. It's the present that the past expected.
|
|||
|
The Internet is what the Arpanet of 1985 became; Urbit is what
|
|||
|
the Arpanet of 1985 wanted to become.
|
|||
|
|
|||
|
In 1985 it seemed completely natural and inevitable that, by
|
|||
|
2015, everyone in the world would have a network computer. Our
|
|||
|
files, our programs, our communication would all go through it.
|
|||
|
|
|||
|
When we got a video call, our computer would pick up. When we had
|
|||
|
to pay a bill, our computer would pay it. When we wanted to
|
|||
|
listen to a song, we'd play a music file on our computer. When we
|
|||
|
wanted to share a spreadsheet, our computer would talk to someone
|
|||
|
else's computer. What could be more obvious? How else would it
|
|||
|
work?
|
|||
|
|
|||
|
(We didn't anticipate that this computer would live in a data
|
|||
|
center, not on our desk. But we didn't appreciate how easily a
|
|||
|
fast network can separate the server from its UI.)
|
|||
|
|
|||
|
2015 has better chips, better wires, better screens. We know what
|
|||
|
the personal cloud appliance does with this infrastructure. We
|
|||
|
can imagine our 1985 future ported to it. But the most
|
|||
|
interesting thing about our planets: we don't know what the world
|
|||
|
will do with them.
|
|||
|
|
|||
|
There's a qualitative difference between a personal appliance and
|
|||
|
a personal server; it's the difference between a social "network"
|
|||
|
and a social network. A true planet needs to work very hard to
|
|||
|
make social programming easier. Still, distributed social
|
|||
|
applications and centralized social applications are just
|
|||
|
different. A car is not a horseless carriage.
|
|||
|
|
|||
|
We know one thing about the whole network: by default, a social
|
|||
|
"network" is a monarchy. It has one corporate dictator, its
|
|||
|
developer. By default, a true network is a republic; its users
|
|||
|
govern it. And more important: a distributed community cannot
|
|||
|
coerce its users. Perhaps there are cases where monarchy is more
|
|||
|
efficient and effective -- but freedom is a product people want.
|
|||
|
|
|||
|
But no product is inevitable. Will we even get there? Will Urbit,
|
|||
|
or any planet, or any personal cloud server, actually happen?
|
|||
|
|
|||
|
It depends. The future doesn't just happen. It happens,
|
|||
|
sometimes. When people work together to make it happen.
|
|||
|
Otherwise, it doesn't.
|
|||
|
|
|||
|
The Internet didn't scale into an open, high-trust network of
|
|||
|
personal servers. It scaled into a low-trust network that we use
|
|||
|
as a better modem -- to talk to walled-garden servers that are
|
|||
|
better AOLs. We wish it wasn't this way. It is this way.
|
|||
|
|
|||
|
If we want the network of the future, even the future of 1985,
|
|||
|
someone has to build it. Once someone's built it, someone else
|
|||
|
has to move there. Otherwise, it won't happen.
|
|||
|
|
|||
|
And for those early explorers, the facilities will be rustic. Not
|
|||
|
at all what you're used to. More 1985 than 2015, even. These
|
|||
|
"better AOLs", the modern online services that are our personal
|
|||
|
appliances, are pretty plush in the feature department.
|
|||
|
|
|||
|
Could they just win? Easily. Quite easily. It's a little painful
|
|||
|
herding multiple appliances, but the problem is easily solved by
|
|||
|
a few more corporate mergers. We could all just have one big
|
|||
|
appliance. If nothing changes, we will.
|
|||
|
|
|||
|
To build a new world, we need the right equation and the right
|
|||
|
pioneers. Is Urbit the right equation? We think so. Check our
|
|||
|
work, please. Are you the right pioneer? History isn't over --
|
|||
|
it's barely gotten started.
|
|||
|
|
|||
|
</div>
|
|||
|
|