diff options
| author | tv <tv@krebsco.de> | 2026-11-12 16:30:07 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-12 18:45:49 +0100 |
| commit | 98b90e33a0ece7e9eab2f75a100bd61b79adae8c (patch) | |
| tree | ce442dfbc97f67044895545069dd2809caba9263 /docs/federation.md | |
| parent | e31d962378215d90d8d1409de90830d7fe9a66a1 (diff) | |
docs: federation: voters named at a remake, versions, what is uncovered
What the maintainer asked of the design:
* Who votes is named by the remake's proposal: all users at its base,
as recommended, or a list. So those who vote say who votes next,
and nobody votes by a rule of who created it. What that costs is
said with it: every voter named counts toward what a decision
takes, and all users is whoever brought users in.
* What a policy no longer covers is uncovered at once where the
decision holds: shown aside by every node, taken away by its user
only, changed by nobody, and dropped once it has been seen there
for `uncovered` (a number; 0: at once). Within an epoch a rule
moves nothing in the document; the remake moves it.
* Being seen takes nothing but a node that is up: its stamp is
renewed about daily whatever else is written. A node left running
keeps its voter present; naming the voters deals with that.
* A release of a vote is best effort.
* Continuum changing under a swarm: versions agree by what they
compute, not by a number. Every stamp carries the digest of the
snapshot its node would build there; a remake is taken up by the
ID each node built; a node checks itself against the swarm before
it writes with a new version; a change of the rules is a change of
policy, and a node knows a policy by everything in it; recorded
vectors every version passes. What can't change, what changes
freely, and when it is a fork.
* Who may connect is outside the design; the swarm key is a note.
The attacks are 84.
future-work.md: several roots, present by the user's own signature, a
guide to moving to another swarm.
Diffstat (limited to 'docs/federation.md')
| -rw-r--r-- | docs/federation.md | 343 |
1 files changed, 216 insertions, 127 deletions
diff --git a/docs/federation.md b/docs/federation.md index f53828d..5f86028 100644 --- a/docs/federation.md +++ b/docs/federation.md @@ -81,7 +81,7 @@ but for what the voters can do to a user, said with each and below): And for what the swarm decides together, with the fractions the design recommends and fewer than a third of the voters present malicious, then or later (the voters are -the users an epoch starts with, of those the root brought in): +the users an epoch's root change names): 7. **Decisions by two thirds.** A proposal (a remake, a new root, first keys for a user that lost its own, the names of a user that is out, a number, the policy) is @@ -161,9 +161,8 @@ its voters' votes. A federated swarm's policy is `{"version": 2, "values": "signed", "users": "vouched"}`, with what the rules read beside it in `_continuum/access`: the kinds of names (`claims`), the paths in namespaces whose values are signed (`userSigned`, as today), -what becomes of what a later policy no longer covers (`uncovered`), and the -**numbers**. All of it is in the swarm's root, its genesis change, with the root -user, its keys and its nodes: +and the **numbers**. All of it is in the swarm's root, its genesis change, with the +root user, its keys and its nodes: * Nothing is ever written before the policy, so nothing is judged by none. * Every node is given a root and takes no other: the genesis root (`--genesis-root`), @@ -184,6 +183,7 @@ value: | `window` | 7 days | how long a vote runs | | `absent` | 30 days | after how long seen silent a voter is absent | | `gone` | 30 days | after how long seen out a user is gone; none: its names wait until a vouch or a decision | +| `uncovered` | 90 days | how long what a policy no longer covers stays aside before it is dropped; 0: at once; none: until its user takes it away | | `archiveRetention` | 90 days | how long a node answers for an epoch it left, at least | | `baseAge` | 10 minutes | how much of what a voter's node holds a remake's base may lack | | `witness` | 1 minute | how long the root's node lets a claim wait before it writes | @@ -191,7 +191,7 @@ value: | `limits.nodes` | 64 | nodes carrying for a user at once | | `limits.users` | 16 | users a user created that stand; the root: no limit | | `limits.vouches`, `limits.bans` | 64 | vouches, and bans, of a user that stand | -| `limits.proposals` | 8 | proposals of a voter that are open | +| `limits.proposals` | 8 | proposals of a user that are open | | `limits.name` | 255 | bytes of a name | | a kind's `limit` | none | names of the kind a user holds | | `fractions.decide` | 2/3 | a decision takes more than this share of the voters present | @@ -226,7 +226,7 @@ grows. A statement is signed by one user and says one thing: | `node` | the user | a node that carries for it; or one that stops, with a cut | | `release` | the user | a claim, a vote, a proposal or an acceptance of its own given up; or itself | | `ban` | the user | a user that doesn't count for it, with a cut (Bans) | -| `propose`, `vote` | a voter (a fork: a first user) | Decisions | +| `propose`, `vote` | a voter (a fork: any user) | Decisions | * **A user is its creation.** The `create` statement's digest is the **user ID**, and every envelope the user signs names it, with the name, the swarm and the epoch it @@ -280,14 +280,14 @@ say about its names: nobody gets one by vouching unasked. Leaving a line takes b nothing its nodes had shown by then: what they committed counts as far as the user's own nodes, or the root's, had seen it without the release. -Bringing someone in means handing over, out of the document, what a party needs: the -swarm key, the root to start from, some seeds; and writing the `create`. Who vouched -for whom says nothing about who connects to whom (Reaching the others). +Bringing someone in means handing over, out of the document, what a party needs to +connect and to start (the root, some seeds, the swarm key where there is one), and +writing the `create`. Who vouched for whom says nothing about who connects to whom +(Reaching the others). -**What the root is.** A user like any other, but for three things. It is in the root -change, so nobody vouched for it, and its name can't be created again. It is on every -line, so its nodes' changes are what settles claims. And the users it creates are the -**first users**, who come to vote for the swarm (Decisions). It can't write in +**What the root is.** A user like any other, but for two things. It is in the root +change, so nobody vouched for it, and its name can't be created again. And it is on +every line, so its nodes' changes are what settles claims. It can't write in another's namespace, nor take a settled name. It can contest any name, in the open. Who the root is can be decided anew (The root's seat). @@ -303,6 +303,11 @@ the voters (`fractions.seen`; the voters: Decisions) each have a node with two a stamps that far apart or more, the thing so at both and at every stamp of that node between. It is read from the changes alone. +A node that is up renews its stamp about daily, whatever else is written or isn't +*(docs)*. So nobody does anything to be seen, and there are always stamps to read +time from. While no node writes at all, no time passes: nothing is seen for longer +by it. + No clock is compared with another. A third, because decisions are argued against fewer than a third of the voters being malicious: so few show nothing by themselves, whatever they stamp. And no more than a third, because everyone it takes must be @@ -363,8 +368,8 @@ liked, and for good: every line passes it. it, and no decision changes that. Against a user that keeps within the rules and harms all the same (squats names up to its limits, gets others' names contested) there is: each party's ban, which changes what that party reads (Bans); its vouchers -unvouching it; and the root unvouching the first user above it, with all below, who -then need other vouches. A vouch from anyone brings it back. +unvouching it; and the root unvouching the user it created above it, with all below, +who then need other vouches. A vouch from anyone brings it back. A user gives itself up by a `release` of its own creation: from that release on it is gone, and its names are free at once. @@ -542,14 +547,18 @@ voters, when to compact. They are decided by votes of users, signed by them, ove days. **Who votes.** The **voters** are the users the epoch's root change names. A swarm's -first names the root alone. A remake's names the **first users** that count at its -base and aren't absent there: the root, those who were the root, and the users each -created as the root. So a first user votes from the next remake on, one that left -is no voter after it, and until the first remake the root decides alone. A root -can't fill the votes with users of its own: those who vote see who would, before -they vote for the remake, and can seat another root first. Users further down the -lines don't vote: whoever brought them in speaks for them, and a vote a user would -be a vote a puppet. +first names the root alone. A remake names those of the next epoch: its proposal +says who, all users that count at its base (recommended), or those of a list. So +who votes is decided by those who vote, with each remake; a user brought in since +votes once a remake names it; and until the first remake the root decides alone. + +What to weigh in it. Every voter named counts toward what a decision takes: one that +never votes stands in the way like one voting against, until it is absent, and +toward the half of all even then. A swarm of many users of whom few take part gets +little decided with all of them named. And all users is whoever brought users in: a +user that created sixteen has seventeen votes in the next epoch. The voters see the +list a remake would give before they vote for it (`GET /registry/decisions`), and +name one of their own where they don't like it. A change in who votes takes a remake because a remake is what puts the votes of before out of reach (below). @@ -561,6 +570,10 @@ have in its history. It neither votes nor counts, until a node of it writes agai The voters **present** are those not absent. Fewer than a third of the voters show nobody silent, whatever they stamp; and those who left hold up nothing. +To be present a voter needs a node that is up, its own or one that carries for it; +the user does nothing. So a node left running keeps its voter present though nobody +votes for it any more. Naming the voters at a remake is what deals with those. + A decision also takes more than half of all the epoch's voters (`fractions.all`), absent or not. So of the sides of a partition, however long it lasts, one at most decides. Where half or fewer are left for good, they go on by a fork that names who @@ -575,7 +588,9 @@ votes (Going apart). its matter is decided. 2. Voters **vote** for it: each a statement signed by the user, saying when the vote runs out, `window` from signing unless the user says sooner. A voter has one vote - on a matter at a time; `release` takes one back. + on a matter at a time; `release` takes one back, as far as it gets there first: + a node that holds the vote without its release counts it, and a proposal may be + decided with a vote whose release was on its way. A vote is meant when signed. 3. A voter's node **takes the proposal up** once it holds votes for it by more than two thirds of the voters present (`fractions.decide`) and more than half of all, none released, each still running by the node's clock. It records so, once, under @@ -655,23 +670,21 @@ key off. The root is whoever holds the seat: at first the user in the root change. A decision gives the seat to any user. Where it holds: -* the new root is on every line, so its nodes' changes settle claims, and the users - it creates are first users. Like them, it votes from the next remake on if it - didn't before; -* the old root is a first user like the others, and votes on. It keeps its name, its - namespace and what it holds, and stays on the lines of those it brought in; +* the new root is on every line, so its nodes' changes settle claims. It votes if + the epoch names it, like anyone; +* the old root is a user like the others, and votes on if it did. It keeps its name, + its namespace and what it holds, and stays on the lines of those it brought in; * what a root signed as the root is the seat's: the new root may unvouch the users the old one created, and the old root itself, which goes alone (those it brought in stay, their lines running through the seat). The old root no longer may. -What the old root did that the proposal's history doesn't hold is an ordinary -user's: users it created while away, or crafts later with old dependencies, are not -first users. +What the old root signs that the proposal's history doesn't hold is an ordinary +user's, whenever it shows up: a user it creates then is below it like any user's. So a root that disappears is replaced by two thirds of the voters present, and so is one that turns on them. (Before the first remake there is nobody to do it: a swarm -remakes once its root brought the first of them in.) A root that comes back finds -itself a first user; it may be voted in again. While the seat's holder is away +remakes once its root brought the first users in.) A root that comes back finds +itself a user like the others; it may be voted in again. While the seat's holder is away nothing new is settled: claims count, and can be contested, until there is a root again. @@ -681,20 +694,22 @@ A federated swarm compacts by a remake its voters decide. A remake throws histor away, and does nothing else: what the next epoch holds isn't voted on. It is the registry as it resolves, which every node works out itself. -* **The base.** A remake is proposed and voted on like anything else. Its - **coordinator** is a node of the proposer, the first it named. Once it holds the - votes it names the base, in its record of the proposal: the base is that record's - history. +* **The base.** A remake is proposed and voted on like anything else; its proposal + names who votes in the next epoch. Its **coordinator** is a node of the proposer, + the first it named. Once it holds the votes it names the base, in its record of + the proposal: the base is that record's history. * **Taking it up.** A voter's node takes a remake up with its base: once the votes are there, and the base holds all the node holds but the last `baseAge`, so that a - remake can't leave out what the swarm has. One base a proposal, the first to reach - the node; and one remake at a time, another only when the first's votes ran out - with it undecided. A coordinator that names two bases, or none, gets no remake: it - is proposed again. + remake can't leave out what the swarm has. It builds the snapshot first, and + records the ID of the epoch it got: the remake is decided for the ID that many got + (Continuum changing under a swarm). One base a proposal, the first to reach the + node; and one remake at a time, another only when the first's votes ran out with + it undecided. A coordinator that names two bases, or none, gets no remake: it is + proposed again. * **Every node builds the snapshot** itself once the remake is decided, from the base as it holds it, and switches after the switch delay, as today. The snapshot holds the registry as it resolves at the base: the policy that holds, with its numbers; - who the root is, and who votes (the first users that count and aren't absent); the + who the root is, and who votes (as the proposal says); the users that count, with their names (or that they are contested), IDs, keys, nodes, creators, vouchers and the vouches they accepted; the names held or contested, each marked settled or not; @@ -710,8 +725,8 @@ registry as it resolves, which every node works out itself. released a thousand times leaves nothing. Within an epoch nothing is removed: a release is a statement like any other, signed, and a document keeps its history. The remake is what deletes. -* **What the policy no longer covers** goes aside or is dropped, as the policy says - (The policy changes by a decision). +* **What the policy no longer covers** is moved aside, with how long it has been + seen there, or left out once it was dropped (The policy changes by a decision). * **Carried over.** A node writes again, as they are, the statements of its user that the base lacks. An envelope names the epoch it was signed in. A claim signed in an earlier epoch is in sight of the claims the snapshot holds settled, and is @@ -771,8 +786,8 @@ Either way, what follows: and names: the node carries for it as before, and writes its member entry anew. Or out, its names waiting. Or gone, and so not in the snapshot: then no node takes the node's changes, it shows why, and its user comes in again as anyone does. -* **As a voter** the user was absent, is no voter after a remake it missed, and is - one again after the next. A root that was away is still the root, unless a decision +* **As a voter** the user was absent, and is one in the epoch it arrives in if that + names it. A root that was away is still the root, unless a decision gave the seat to another. * **Its old document** goes to the archive, as today. @@ -782,27 +797,35 @@ word. ### The policy changes by a decision The policy but for its numbers is one matter. A proposal carries it whole: the -version, the kinds with their rules, `userSigned`, `uncovered`. Like every decision -it holds for what is made in sight of it: a change is judged by the policy that holds -in its history. No remake comes with it. A remake throws history away, a change of -policy changes the rules, and neither needs the other. +version, the kinds with their rules, `userSigned`. Like every decision it holds for +what is made in sight of it: a change is judged by the policy that holds in its +history. No remake comes with it. A remake throws history away, a change of policy +changes the rules, and neither needs the other. -* **A version a node doesn't know.** Such a node can't take the proposal up, so a - policy is decided only once the nodes of two thirds of the voters know it. A node - that doesn't know a policy that holds stops syncing, as under any policy it doesn't - know, says why, and goes on once it is upgraded. Or its operator refuses the - decision (below). -* **What the policy no longer covers**: a claim that no rule lets its user make any - more (the kind is gone, or the rule, or the name is now kept for another or left - to the voters), a value at a path `userSigned` no longer lists. Where the decision - holds, it doesn't count, and nothing more is written there. It stays in the - document, as everything does within an epoch, and counts again under a later - policy that covers it. -* **At the next remake**, `uncovered` says what becomes of it. `"aside"`, as - recommended: the snapshot holds it under `_continuum/uncovered/<user>/`, at the - path it had and as it was: read by all, written by none, counting for nothing. Its - user takes from there what it wants to write again under the rules that hold, and - the remake after that leaves it out. `"drop"`: left out at once. +* **A policy a node doesn't know.** A node knows a policy if it knows everything in + it (Continuum changing under a swarm). One that doesn't can't take the proposal + up, so a policy is decided only once the nodes of two thirds of the voters know + it. A node that doesn't know a policy that holds stops syncing, as under any + policy it doesn't know, says why, and goes on once it is upgraded. Or its operator + refuses the decision (below). +* **What the policy no longer covers** is **uncovered** where the decision holds: a + claim that no rule lets its user make any more (the kind is gone, or the rule, or + the name is now kept for another or left to the voters), a value at a path + `userSigned` no longer lists. It doesn't count. Every node shows it aside, under + `_continuum/uncovered/<user>/` at the path it had, and no longer where it was. +* **Only its user takes it away**, and nobody changes it: a value by an envelope + that puts nothing in its place (`null`), a claim by its `release`. Nothing else + is written there. +* **For as long as `uncovered` says.** Once it has been seen uncovered for that long + (Lengths of time) it is **dropped**: no node shows it any more, and nothing brings + it back. At 0 it is dropped at once. Until then, a later policy that covers it + again has it count again, where it was. +* **Where it is.** Within an epoch a rule moves nothing: a move is a change someone + has to write, and every node would have to write the same one. So what is + uncovered stays in the document where it was written, shown aside by every node + from where the decision holds. A remake moves it for good: the snapshot holds what + is uncovered and not dropped under `_continuum/uncovered/<user>/`, with how long + it has been seen there, and leaves out what was dropped. * **Whole, not merged.** Two proposals of a policy are two policies, and the later holds: what one changed isn't worked into the other. @@ -813,10 +836,9 @@ same: with a root that went before the first remake, with half the voters or few left, with two thirds of them turned against the rest, or for a policy the others won't have. A fork is proposed, and binds nobody: -* A first user proposes it: the policy to go on with (the one that holds, or - another), and, if it likes, who votes there. Where it names none, those vote who - would after a remake at the base. Voters may vote for it: that shows who would go, - and decides nothing. +* Any user proposes it: the policy to go on with (the one that holds, or another), + and, if it likes, who votes there. Where it names none, all users at the base + vote. Voters may vote for it: that shows who would go, and decides nothing. * The proposer's node names the base, as a remake's coordinator does, when its user says so, votes or none. * A node follows if its operator says so (`POST /admin/fork`, naming the proposal and @@ -851,8 +873,8 @@ What refusing does: its member entry, so the voters see who would go (`GET /registry/decisions`). * **The epoch without it.** A node that refuses a decision moves to the epoch built at the history of the proposal: the registry as it resolves there, the policy and - the voters as after a remake at that base, and the digest of the proposal refused, - which makes it no other epoch. Every node that refuses the same decision builds + the voters as they are there, and the digest of the proposal refused, which makes + it no other epoch. Every node that refuses the same decision builds the same epoch, whenever it does. So those who think alike meet there without arranging it. * **After**, as after any fork: the same swarm ID and the same history up to the @@ -902,9 +924,11 @@ A change is valid if, in the document at its dependencies: 6. A `claim`: the kind is declared, a rule lets the signer, the signer is within the kind's limit, and no claim of that name stands there. 7. In a namespace: at a listed depth, an envelope by a user of that name, replacing - what is there (`prev`); nothing within one, nothing replacing what holds one. -8. `propose`, `vote`: by a voter present there (a fork: proposed by a first user), - signed in this epoch; a proposal within its voter's limit; a vote where the user + what is there (`prev`); nothing within one, nothing replacing what holds one. At + a place the policy no longer covers: only its user's envelope that puts nothing + there. +8. `propose`, `vote`: by a voter present there (a fork: proposed by any user), + signed in this epoch; a proposal within its user's limit; a vote where the user has none on that matter that isn't released. 9. `_continuum/access`, the swarm ID, the epochs: never. @@ -972,6 +996,8 @@ name), and answers from it: and absent; what is proposed, who voted, until when, which nodes took it up and which would refuse it; what is decided, which decision of each matter holds, and what this node holds back or refuses. +* `GET /registry/uncovered/<user>`: what a policy no longer covers of that user's, + at the path it had, and how long it still stays. * `GET /store`, as today, with a value that doesn't count marked, and the one that counts in its place. @@ -1056,9 +1082,10 @@ others' sight, and is contested or late like any such. * **Reading**: every party reads everything. The registry holds public keys, names and addresses, nothing secret; a value only some should read is sealed for them, as today. -* **The swarm key** stays a secret all nodes share: it keeps those who aren't parties - from connecting and reading, as far as every party keeps it. It is no boundary - between the parties. +* **Who may connect** is outside this design. Today it is the swarm key, a secret all + nodes share: it keeps those who aren't parties from connecting and reading, as far + as every party keeps it, and is no boundary between the parties. Nothing here + rests on it. ## Why it holds @@ -1162,8 +1189,8 @@ third malicious. node holds and judged itself. The coordinator chooses the base, and an honest voter's node takes up none that leaves out what it holds, and one a proposal: two bases named for one remake each need more than two thirds, so neither or one is - decided. Who votes next is in the snapshot: the first users present at the base, - for all to see before they vote for it. + decided. Who votes next is in the remake's proposal, for all to see before they + vote for it. * *Nobody is moved.* A node refuses any decision by its operator's word or bounds, and follows a fork by its operator's word alone. * *With other fractions.* Write d, a and s for `fractions.decide`, `fractions.all` @@ -1196,9 +1223,10 @@ back, so the point only moves forward as more of them show up. holder, but a senior user whose line kept silent takes a junior's name by backdating, and a root that lies takes any. Contested, with `release` to give a name up, fails safer: a name is blocked in the open, not taken. -* **Votes by nodes, or by every user**: a party runs as many nodes, and brings in as - many users, as it likes. Only whom the root brought in itself can't be multiplied - by anyone but the root. +* **Votes by nodes**: a party runs as many nodes as it likes. +* **Voters by a rule** (the users the root itself created, whom nobody else can + multiply): who takes part and who doesn't is known to people, not to a rule. So + those who vote say who votes next. * **Recommending two thirds of all the voters**, absent or not: then nothing is decided once a third left for good. Leaving the absent out, down to half, lets those who stay decide; the price is a malicious voter on both sides of a long @@ -1232,8 +1260,8 @@ back, so the point only moves forward as more of them show up. voters from deciding badly by rules none of them chose. Every number is the swarm's; whoever won't bear a decision refuses it, and goes on with those who think alike. -* **First users voting as soon as they are created**: the root creates as many as it - likes, carries every decision, and is never replaced against its will. +* **Users voting as soon as they are created**: whoever creates users carries every + decision. * **Voters let in by a decision, without a remake**: nobody fills the votes, and nobody waits for a remake. But the voters of before make a decision "of then" later (the root alone, for the time it was the only one), which only nodes that @@ -1298,7 +1326,7 @@ against the resolved registry, when it writes the files out. So are a node kept an archive of every epoch, and whatever brings a vote from a person to a node. More policies later are more data where a rule has a parameter, and a new rule in -continuum, with a policy version, where it hasn't; either way a decision. +continuum, with a policy of its own, where it hasn't; either way a decision. What changes in continuum besides the rules, under the federated policy only: policy version 2; the envelope's payload names the user ID and the epoch; a node is given its @@ -1308,16 +1336,72 @@ neighbours, answers for an epoch it left while it keeps it, takes no remake but its voters decided, rejoins no epoch because peers advertise it, and refuses what its bounds say. No message changes. [`trust.md`](trust.md), [`access.md`](access.md), [`remakes.md`](remakes.md) and the glossary get the federated case: party, statement, -line, first user, voter, claim, cut, late, contested, settled, out, gone, seen for, +line, voter, claim, cut, late, contested, settled, out, gone, uncovered, seen for, taking a proposal up, a decision that holds, bounds, and a ban of a user beside a ban of a node. +## Continuum changing under a swarm + +Parties upgrade when they like, so a federated swarm always runs several versions of +continuum. Given the same changes they must agree on three things: which are valid, +what the registry is, and, byte for byte, the snapshot a remake builds. Whether two +versions do can't be told by looking at them, and a version's number says what its +author remembered to say. So the design goes by what nodes compute: + +* **Every stamp says what its node computes.** With its alive stamp a node records + the digest of the snapshot it would build at that change's history. A node that + holds exactly those changes builds its own and compares. Two digests for one + history prove that two nodes resolve differently, signed by both, at the first + input where they do: it shows (`GET /peers`, the dashboard) long before a remake + would have sent them to two epochs. In a quiet swarm nodes hold the same changes + most of the time. +* **A remake is taken up by its result.** A voter's node builds the snapshot from the + base before it takes the remake up, and records the ID of the epoch it got. The + remake is decided for the ID that the nodes of two thirds of the voters got. A + node that got another stays where it is, and says so. +* **A node checks itself before it writes.** Started with a version it didn't run + before, a node judges and resolves again what it holds *(docs: a node told that + policies disagree does so today)*, builds at the histories of the latest stamps it + holds of other parties' nodes, and compares. Where it differs it writes nothing, + and shows it, until its operator says to go on. +* **A change of the rules is a change of policy.** What a version judges or resolves + differently on purpose, it does under a policy of its own (a part of the policy + that versions before don't know), which the swarm decides. A change is judged by + the policy that holds in its history, so a version judges what was written before + as it was judged then, and carries the rules of every policy that held in an + epoch it can still walk. A node knows a policy if it knows everything in it: it + goes by what the policy holds, not by its number, and takes up none that has a + part it doesn't know. +* **Vectors.** Continuum carries recorded inputs with what must come of them: changes + and whether they are valid, sets of changes and the snapshot they resolve to. Each + test named under Attacks leaves some. A release adds vectors and changes none, + and every version passes all of them: a rule changed without a policy fails there, + before it is released. + +What can't change under a swarm, whatever is decided: how an envelope is read and +what its signature covers (a snapshot carries envelopes as they were signed, from +epoch to epoch); a statement's digest; a change's hash, and what Automerge makes of +the same changes; a snapshot's bytes for the same base and policy. A new form of any +of them comes beside the old one, under a policy. + +What changes freely: everything a node does for itself. Its API, its storage, how it +syncs and with whom. The wire is between two nodes, not the swarm's: a version +speaks its own and the one before, so that parties upgrade one after another +*(today every wire change parts the versions: `transport::wire::VERSION`)*. + +And a fork it is when nodes that hold the same changes don't come to the same, and +neither gives way. It shows by a change one takes and the other refuses, by two +digests for one history, or by two IDs for one base: by what was computed, never by +a number. Which of the two is the swarm's no rule says. The parties see how many of +them say which, and upgrade, drop, or go apart. + ## Attacks Each with what honest nodes do, the promise it touches, and what shows it: `unit` in the module named (`access::statements`, `access::claims`, `access::resolve`, `access::decisions` are new), `sim` a simulation in `src/simtest/federation.rs`, -`argued` where no test can. +`argued` where no test can. Each test leaves vectors (Continuum changing under a +swarm). Statements and users: @@ -1390,8 +1474,8 @@ Decisions, the root, remakes: | # | The adversary | Honest nodes | Promise | Shown by | |---|---|---|---|---| | 42 | Forges a vote; votes as a user that isn't a voter, or one absent | Refuse | 7 | unit, decisions: `a_vote_is_a_voters` | -| 43 | Brings in many users to vote | They don't: only those a root created come to vote | 7 | unit: `who_votes` | -| 44 | The root creates many first users to carry a vote | They vote from the next remake on, which takes two thirds of the voters there are; these see who would vote, and can replace the root first | 7 | unit: `voters_are_the_epochs` | +| 43 | Brings in many users to vote | They vote only if a remake names them, which those who vote decide, seeing the list | 7 | unit: `who_votes` | +| 44 | A remake that says all users, after one user brought in many | They vote in the next epoch: all users is whoever brought users in. The voters see the list before they vote, and name one of their own instead | not promised | unit: `voters_are_the_remakes` | | 45 | Records taking a proposal up without the votes, or with released ones | Its record is its own: honest nodes record what they hold, and it takes two thirds | 7 | unit: `taken_up_with_the_votes` | | 46 | Keeps votes back until they ran out, or crafts them with old dependencies, then shows them | No honest node takes the proposal up: it isn't decided. One decided by its makers' own records, long after (two thirds of their keys stolen), is held back by every node that was up, and shown | 7 | unit: `taken_up_while_the_votes_run`; sim: `a_decision_from_the_past` | | 47 | Two proposals of one matter both decided: voters changed sides, or a third voted both ways | Every node acts on the later, the same from the same changes; the other shows as replaced | 7 | unit: `the_last_holds`; sim: `two_decided_one_holds` | @@ -1401,7 +1485,7 @@ Decisions, the root, remakes: | 51 | Puts a voter's vote into a change of its own node; keeps one back; offers one again | Taken: the signature is the voter's. It counts while it runs; the register holds it once | 7 | unit: `a_vote_by_any_node` | | 52 | A decision lowers a limit below what users have, or sets a number that makes no sense | Nothing is taken away, and no more is taken until below; the number has the result it has | 6 | unit: `a_lowered_limit_takes_nothing` | | 53 | Opens proposals without end | Refuse past `limits.proposals` | 6 | unit: `limits_on_proposals` | -| 54 | The voters decide a policy that covers a user's names no more | They don't count where it holds; the next remake keeps them aside for an epoch, or drops them, as the policy says | not promised | unit: `what_a_policy_no_longer_covers` | +| 54 | The voters decide a policy that covers a user's names no more; another user writes over what is uncovered | It doesn't count where the policy holds, and shows aside. Only its user takes it away, nobody changes it; it is dropped once seen there for `uncovered` | not promised | unit: `what_a_policy_no_longer_covers`, `uncovered_is_only_taken_away` | | 55 | Names a base for a remake that leaves out what the swarm holds | No honest voter's node takes it up | 7 | sim: `a_stale_base_is_not_taken_up` | | 56 | A remake's coordinator names two bases, or none | A node takes the remake up with one base: one is decided, or neither. Then the remake is proposed again | 7 | sim: `two_bases_no_remake` | | 57 | Runs most of the nodes; serves a snapshot of its own | Nothing reads a count of nodes; every node builds the snapshot itself | 7 | sim: `every_node_builds_the_snapshot` | @@ -1409,38 +1493,39 @@ Decisions, the root, remakes: | 59 | A thief's statements are in the base of a remake | They stand: a remake makes final. The vote's days are the time to cut the key off | not promised | unit: `a_cut_reaches_to_the_snapshot` | | 60 | Tells a node that was away of an epoch that isn't the swarm's | The node rejoins no epoch that peers advertise: it finishes the old one from archives and builds the next itself, or takes the ID its operator gave | 1 | sim: `away_over_a_remake`, `back_by_an_id` | | 61 | A node back after several remakes offers what its user signed epochs ago | Refuse: an envelope is taken in its epoch and the next. Quarantined, for the user to sign again | 3 | unit: `carried_over_one_epoch_only` | -| 62 | The old root comes back after its seat went; crafts users "from before" | It is a first user; what the decision's history doesn't hold of it is an ordinary user's | 7 | unit: `the_old_root_comes_back` | -| 63 | Two thirds of the voters turn on the rest | They remake, seat a root, set any number (a share of the voters that lets one decide for all), change the policy, and give a user's first keys to another, or the names of a user that is out: a name taken, in the open. They forge no signature and move nobody to other rules; the rest ban, refuse, or fork | not promised | argued (Going apart, Refusing a decision) | -| 64 | Offers a statement signed on the other side of a fork | Refuse: it names its epoch | 3 | unit: `signed_for_its_user_id_and_epoch` | -| 65 | A fork naming its proposer the only voter | A node follows only by its operator's word, which names the proposal; what that says shows | 7 | sim: `a_fork_by_choice` | -| 66 | A decision breaks a node's bounds: the voters let one of them decide for all | The node refuses it and moves to the epoch built without it, the same for every node that refuses: they go on together | 7 | unit: `bounds_on_the_numbers`; sim: `those_who_refuse_meet` | -| 67 | Says it refuses a decision, or hands out the ID of an epoch without it, to draw nodes there | A node goes there by its own bounds or its operator's word only | 1 | unit: `refusing_is_ones_own` | +| 62 | A version builds another snapshot from a remake's base | Its node records another ID. The remake is decided for the ID two thirds got; the node stays, and says so | 7 | sim: `a_remake_by_its_result` | +| 63 | The old root comes back after its seat went; crafts users "from before" | It is a user like the others: what the proposal's history doesn't hold of it is an ordinary user's | 7 | unit: `the_old_root_comes_back` | +| 64 | Two thirds of the voters turn on the rest | They remake, seat a root, set any number (a share of the voters that lets one decide for all), change the policy, and give a user's first keys to another, or the names of a user that is out: a name taken, in the open. They forge no signature and move nobody to other rules; the rest ban, refuse, or fork | not promised | argued (Going apart, Refusing a decision) | +| 65 | Offers a statement signed on the other side of a fork | Refuse: it names its epoch | 3 | unit: `signed_for_its_user_id_and_epoch` | +| 66 | A fork naming its proposer the only voter | A node follows only by its operator's word, which names the proposal; what that says shows | 7 | sim: `a_fork_by_choice` | +| 67 | A decision breaks a node's bounds: the voters let one of them decide for all | The node refuses it and moves to the epoch built without it, the same for every node that refuses: they go on together | 7 | unit: `bounds_on_the_numbers`; sim: `those_who_refuse_meet` | +| 68 | Says it refuses a decision, or hands out the ID of an epoch without it, to draw nodes there | A node goes there by its own bounds or its operator's word only | 1 | unit: `refusing_is_ones_own` | Nodes and syncing: | # | The adversary | Honest nodes | Promise | Shown by | |---|---|---|---|---| -| 68 | Sends different changes to different peers, each under its own number | Take all: changes made at once | 2 | sim: `two_histories_merge` | -| 69 | Forks its node's actor | The nodes that took each split; one offered both holds proof and refuses the node; a side drops by its operator's choice | not promised | sim: `a_forked_actor_is_proven`, `a_side_drops_a_fork` | -| 70 | Holds changes back, or passes on some | They come by other nodes; a joiner caught up with it syncs on | 6 | sim: `withheld_changes_arrive` | -| 71 | Places its nodes around a victim by choosing their keys | The victim's node keeps its seeds and its user's other nodes as neighbours; a claim not settled for long shows. It costs time, not names | 5, 6 | sim: `around_a_victim` | -| 72 | Hands a joining node another root, or changes from before the policy | Refuse the handshake; there is no before | 1 | sim: `a_joiner_takes_its_root_only` | -| 73 | Signs a token for any user with its node's key | Not taken by another party's node | 3 | unit: `a_token_of_ones_party` | -| 74 | Lies in its member entry (ready, reached, stable) | Sync and trees bend; nothing gets in that way | 6 | argued (Reaching the others) | -| 75 | Sends envelopes that don't check, without end | Refuse each (a signature costs about 30 µs to check *(docs)*); the node is refused as a whole after the first of its own | 6 | sim: `a_refused_node_costs_little` | -| 76 | Writes its own member entry without end; fills the waiting changes | Take them no faster than a budget per party: put off, never refused; a share of the waiting each | 6 | sim: `a_party_past_its_budget_waits` | -| 77 | Crafts a change Automerge won't take, or that writes in an object nowhere to be found | Refuse *(checked: the rules refuse the second today)*; only the message that brought it fails | 1 | unit: `a_change_writing_nowhere` | +| 69 | Sends different changes to different peers, each under its own number | Take all: changes made at once | 2 | sim: `two_histories_merge` | +| 70 | Forks its node's actor | The nodes that took each split; one offered both holds proof and refuses the node; a side drops by its operator's choice | not promised | sim: `a_forked_actor_is_proven`, `a_side_drops_a_fork` | +| 71 | Holds changes back, or passes on some | They come by other nodes; a joiner caught up with it syncs on | 6 | sim: `withheld_changes_arrive` | +| 72 | Places its nodes around a victim by choosing their keys | The victim's node keeps its seeds and its user's other nodes as neighbours; a claim not settled for long shows. It costs time, not names | 5, 6 | sim: `around_a_victim` | +| 73 | Hands a joining node another root, or changes from before the policy | Refuse the handshake; there is no before | 1 | sim: `a_joiner_takes_its_root_only` | +| 74 | Signs a token for any user with its node's key | Not taken by another party's node | 3 | unit: `a_token_of_ones_party` | +| 75 | Lies in its member entry (ready, reached, stable) | Sync and trees bend; nothing gets in that way | 6 | argued (Reaching the others) | +| 76 | Sends envelopes that don't check, without end | Refuse each (a signature costs about 30 µs to check *(docs)*); the node is refused as a whole after the first of its own | 6 | sim: `a_refused_node_costs_little` | +| 77 | Writes its own member entry without end; fills the waiting changes | Take them no faster than a budget per party: put off, never refused; a share of the waiting each | 6 | sim: `a_party_past_its_budget_waits` | +| 78 | Crafts a change Automerge won't take, or that writes in an object nowhere to be found | Refuse *(checked: the rules refuse the second today)*; only the message that brought it fails | 1 | unit: `a_change_writing_nowhere` | Rules, and the swarm as it is: | # | The adversary | Honest nodes | Promise | Shown by | |---|---|---|---|---| -| 78 | A rule differs between versions (a bug) | Each side refuses, logs that the policies disagree, shows the peer refused; one side upgrades and drops | not promised | sim: `a_side_drops_and_comes_back` | -| 79 | A policy version a node doesn't know: in the root, or decided | It doesn't sync, and takes no such proposal up: a policy is decided only with two thirds of the voters' nodes knowing it | 1 | unit: `no_sync_under_an_unknown_policy`, `an_unknown_policy_isnt_taken_up` | -| 80 | A root that lies and isn't replaced; a user nobody unvouches | The parties ban it from a cut on and read on without it | not promised | unit: `a_ban_for_those_who_follow` | -| 81 | A banned user's name claimed by one who bans it | The node refuses its own user's write: the others would | 1 | unit: `no_write_by_ones_bans_alone` | -| 82 | A partition that outlasts the absence period | One side at most decides: a decision takes the nodes of more than half of all the voters. With a malicious voter on both sides of an even split, both may: every node then acts on the later; two remakes are two epochs | 7, not promised | sim: `one_side_decides`; unit: `the_last_holds` | -| 83 | None: a swarm that isn't federated | Everything as today | Optional | the existing tests, untouched; `bench_screening` | +| 79 | A rule differs between versions (a bug) | Each side refuses what the other takes, or their stamps name two digests for one history: each logs and shows it. A node that finds it at its first start with a version writes nothing. One side upgrades and drops | not promised | sim: `a_side_drops_and_comes_back`, `two_digests_one_history`; unit: `a_node_checks_itself` | +| 80 | A policy version a node doesn't know: in the root, or decided | It doesn't sync, and takes no such proposal up: a policy is decided only with two thirds of the voters' nodes knowing it | 1 | unit: `no_sync_under_an_unknown_policy`, `an_unknown_policy_isnt_taken_up` | +| 81 | A root that lies and isn't replaced; a user nobody unvouches | The parties ban it from a cut on and read on without it | not promised | unit: `a_ban_for_those_who_follow` | +| 82 | A banned user's name claimed by one who bans it | The node refuses its own user's write: the others would | 1 | unit: `no_write_by_ones_bans_alone` | +| 83 | A partition that outlasts the absence period | One side at most decides: a decision takes the nodes of more than half of all the voters. With a malicious voter on both sides of an even split, both may: every node then acts on the later; two remakes are two epochs | 7, not promised | sim: `one_side_decides`; unit: `the_last_holds` | +| 84 | None: a swarm that isn't federated | Everything as today | Optional | the existing tests, untouched; `bench_screening` | ## The registry on it @@ -1472,8 +1557,8 @@ over a namespace that is there. The root is trusted as it is (every party is giv hash), so what it holds counts without signatures: a host from the root counts until its user signs over it, and a claim from the root is held by whoever has its name. The maintainer then creates each user as it comes with a key; from then on the user -signs its own, and is a first user: it votes from the next remake on. A namespace -whose user never comes stays as the root has it. +signs its own, and votes from the next remake that names it. A namespace whose user +never comes stays as the root has it. The limits in the genesis policy: 64 keys, 64 nodes and 256 addresses a user for a start. The import may set keys and addresses to twice the most any user has in the @@ -1487,13 +1572,14 @@ change of the document (the store's stream), it replaces the merge button. ## Stages -Each leaves the system working, and is a few patches with its tests. At every stage -the existing tests pass untouched (83). +Each leaves the system working, and is a few patches with its tests, which leave +their vectors. At every stage +the existing tests pass untouched (84). 1. **A federated swarm's frame.** `--genesis-policy` (a file, written into the genesis change with the root, its keys and nodes); a federated node is given its root, syncs under no policy it doesn't know, takes no remake yet, takes tokens - from its party only, keeps ended sessions to itself. Tests: 72, 73, 79, 83, and + from its party only, keeps ended sessions to itself. Tests: 73, 74, 80, 84, and the policy's existing tests under a policy from the root. 2. **Statements.** The users' registers (`create`, `key`, `node`, `vouch`, `accept`, `release`), the user ID and the epoch in envelopes, nodes carrying for users and @@ -1510,7 +1596,7 @@ the existing tests pass untouched (83). claim reaches it; `GET /registry/claims` with what makes a claim late; values marked in `GET /store`. A node keeps its seeds and its user's other nodes as neighbours, and shows a claim long unsettled. Tests: 8, 9, 12, 24, 26, 27, 31, - 32, 68, 70, 71. + 32, 69, 71, 72. 6. **Cuts.** `retire` and the keys above, a node that stops, what doesn't count left out, the value before read again, the client listing what stopped counting. Tests: 33 to 37, 39, 40, 41. @@ -1519,8 +1605,8 @@ the existing tests pass untouched (83). nothing taken from an unvouched user's nodes. Tests: 13 to 18. 8. **Coming back.** The drop an operator asks for, a forked actor proven and shown with what the users on one's lines hold, the budget and the share per party. - Tests: 69, 75 to 78. -9. **Bans** for those who follow. Tests: 80, 81. + Tests: 70, 76 to 79. +9. **Bans** for those who follow. Tests: 81, 82. 10. **The registry.** Import, export, and a federated swarm of the simulations with the registry's kinds and a few hundred hosts. From here it can be used; it only grows. @@ -1530,18 +1616,21 @@ the existing tests pass untouched (83). `GET /registry/decisions`. What is decided: a number, a user's first keys, the names of a user that is out, a name the rules leave to the voters. The simulations start from a root change naming several voters, as a remake's does. - Tests: 19, 20, 38, 42 to 49, 51, 52, 53, 82. -12. **The root's seat.** Tests: 62. + Tests: 19, 20, 38, 42 to 49, 51, 52, 53, 83. +12. **The root's seat.** Tests: 63. 13. **Remakes.** The coordinator and the base, taking a remake up, the snapshot every node builds and what it leaves out, carrying over from the epoch before and no further, what becomes final; peers answering for the epochs they keep, and saying which; a node walking them, or rejoining by an ID its operator gave, - and by none that peers advertise. Tests: 55 to 61. + and by none that peers advertise. Who votes next, as the proposal says. The + digest with every stamp, a node checking itself before it writes, a remake + taken up by its result. Tests: 55 to 62, 79. 14. **The policy by a decision.** The policy as a matter, a node that doesn't know - it, what it no longer covers, kept aside or dropped at a remake. Tests: 54, 79. + it, what it no longer covers: shown aside, taken away by its user only, dropped + after a time, moved at a remake. Tests: 54, 80. 15. **Going apart, and refusing.** A fork followed by an operator's word, with who votes on its side. Refusing a decision: a node's bounds, the epoch without the - decision. Tests: 64 to 67. + decision. Tests: 65 to 68. ## What was checked, and what is assumed |
