Introduce a property testing harness for asserting a set of properties expected
of the repository identity.
The harness uses the `Network` fixture for providing four nodes that can
interact with the repository identity. The state of the harness is advanced by
providing an actor and operation on the identity document. Invariants are then
asserted about the identity.
The current invariants are:
- The `current` revision is the one and only `Accepted` revision.
- The chain of revisions is valid. That is, `Active` revisions only have a
parent that is `Accepted`, and not `Rejected` or `Redacted`.
- `Active` revisions do not contain a majority approval.
- `Accepted` revisions contain a majority approval.
- `Rejected` revisions do not contain a majority approval.
- For each revision, at most one child is `Accepted`.
- For each revision, if any of its children is `Accepted`, all other
children are `Rejected`.
- A revision which is `Rejected(RejectedBy::Parent)` has a parent that
is `Rejected`.
- A revision which is `Redacted(RedactedBy::Parent)` has a parent that
is `Redacted`.
- For each revision that is `Rejected` or `Redacted`, none of its
children is `Active`.
- A sibling or ancestor revision's rejected state applies to its sibling or
descendant.
- The repository identity documents converge when all nodes have applied all
operations.
When a revision reaches qourum and is adopted, the state all competing sibling
revisions as `Rejected`.
However, the test suite lacked coverage for deeper proposal branches,
specifically testing that this rejection correctly cascades to the
children of those rejected siblings.
Introduces a test to simulate the scenario where a delegate eagerly
proposes a chain of revisions (a child branching off a sibling) while
others concurrently accept a competing branch.
The identity evaluation rewrite changed how revision redactions are
handled. Instead of removing the revision entirely, it explicitly
transitions the revision to `State::Redacted`.
Add a test to cover two redaction scenarios:
- Attempting to redact a revision that has already been `Accepted`.
- Attempting to redact a revision that has already been `Rejected`.
The new identity evaluation logic was updated to ignore `Accept` or `Reject`
actions if the revision has already reached a terminal state (`Accepted` or
`Rejected`), however this handling was not explicitly covered by the test suite.
This test ensures that the state machine correctly short-circuits the
late-arriving vote, proving that the new logic succesfully ensures
terminal states remain immutable under concurrent network conditions.
Introduces support for Gerrit-style magic push references via
`refs/for/<branch>`. Pushing to this ref automatically extracts the
target branch and opens a patch against it, bypassing the need for the
push option `patch.target`. Example:
```
$ git push rad HEAD:refs/for/accepted
```
Will open a patch with its `patch.target` set to `refs/heads/accepted`.
The remote helper now supports the push option `patch.target`. This
allows users to explicitly specify a target canonical reference when
opening or updating a patch. For example, to open a patch that targets
the branch "backport", use:
```
git push -o patch.target=refs/heads/backport
```
Furthermore, strict merge and revert isolation is now enforced:
patches are only marked as merged or reverted if the commits are
pushed to the target branch of the patch explicitly.
Extend `enum MergeTarget` to include a new variant, `Branch`.
The intended use of this new variant is to allow a `Patch` to have a
target branch other than the default branch.
The `Branch` variant holds a `TargetBranch` which, in turn, is ensured
to be a `Qualified` reference that begins with `refs/heads`, i.e. a
Git branch.
The `rad_cob_update` test accidentally use the `rad-cob-log` file
instead of `rad-cob-log`.
This change fixes that, and updates the `rad-cob-update` example file
to the latest patch show format.
When a connection drops and the node attempts to reconnect within the
same second, the `last_attempt` timestamp written to the database is
identitcal to the `last_success` timestamp due to the Unix time (seconds)
precision loss.
The `maintain_connections` logic filters eligible peers using the
condition `last_success >= last_attempt`. Because the truncated
timestamps are equal, this evaluates to true, causing the node to
bypass the `CONNECTION_RETRY_DELTA` backoff.
If the OS instantly rejects the dial attempt (e.g. `EHOSTUNREACH`),
the node synchronously queues another connection attempt. This traps
the reactor in an infinite `while` loop.
Changing the condition to `last_success > last_attempt` ensures that a
failed attempt in the same second correctly evaluates to false, forcing
the node to respect the backoff timer and breaking the loop.
Move logging-related module `radicle::logging` into its own crate.
While at it, remove the "logger" feature flag from `radicle`.
Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Introduce the `network.cue` topology definition for creating a set of
Radicle nodes and deploying them with the `justfile`.
A set of predefined bootstrap keys are provided with helpers to generate
connection strings inside Node config. Node config can be overwritten
directly inside the CUE config.
Nodes versions are taken from quay.io.
Talos is the cluster manager.
K8s hosts the pods.
Timoni aplpys the CUE configs to the K8s cluster.
Adds a `radicle-node` particle CUE module to simulation. Partial for
brevity in review as the `cue.mod` files found in the NEXT commit are
less relevant and autogenerated by Timoni + CUE.
Creates a set of roles: `peer`, `seed`, `bootstrap` that can be arranged
into toplogies for deployment in a k8s system. Images are provided by
Radicle Garden via quay.io.
Previously `test_fetch_emits_canonical_ref_update` was passing by
catching a leftover `CanonicalRefUpdated` event from the initial
repository fetch. Furthermore, the new commit was written to the
un-namespaced root. It can be proven to be the case by commenting out
the `alice.commit_to(...)` line and watching the test still pass.
Fix the test by committing to the namespaced default branch which will
trigger the `CanonicalRefUpdated` that is expected by Bob. Drains events
so we don't catch the wrong event, and explicitly announces.
Previously a rule like `refs/heads/releases-*` would be rewritten
incorrectly to `refs/heads/releases-**/*` matching the 'trailing
asterisk' rule.
Simplify the rewrite rule so that a trailing `*` becomes a prefix
matching rule. Add tests.
Fixes an intermittent failure caused by a race condition with background
gossip messages.
Previously Eve's `RefsAnnounced` message could be processed by Bob
after the test updated the identity document to include Alice-Laptop
as a delegate. This caused Bob to unexpectedly fetch the issue in the
background, failing the `!has_issue` assertion.
We now explicitly wait for Bob to receive Eve's announcement before
updating the identity document.
Avoid relying on a thread sleep to wait for the nodes to connect.
Instead rely on the connection condition being established by using a polling loop.
The loop is controlled by a number of iterations, panicking if the
number of iterations is exceeded.
Addresses intermittent failures in the `test_connection_crossing` e2e test,
which were particularly prevalent on slower CI environments (such as
`rust:trixie`).
Previously, the test spawned two threads to make Alice and Bob dial each
other concurrently and strictly asserted that the "preferred" peer (the
one with the higher Node ID) would always win the `Outbound` link direction.
However, this assumption is flawed in real-world, OS-level network execution
due to two race conditions:
1. Thread Scheduling: One thread could execute and fully establish a
connection before the other thread even began processing its dial command.
2. Reactor Event Ordering: A node's reactor might wake up and process an
incoming TCP connection from its peer *before* it processes the `Connect`
command sent by the test. When it finally processes the `Connect` command,
it sees a session already exists and skips dialing entirely.
In both scenarios, a true "simultaneous crossing" never occurs. Instead, a
standard sequential connection happens, meaning the link direction is dictated
by whoever dialed first, not by the "preferred" peer logic. This caused the
strict `left: Outbound, right: Inbound` assertions to panic.
To fix this, two changes were made:
- Introduced a `std::sync::Barrier` to synchronize the two test threads.
This forces both threads to wait for each other before calling `.connect()`,
maximizing the probability of a true simultaneous dial.
- Relaxed the final assertions. Because OS-level TCP handshakes and reactor
polling can never guarantee perfect simultaneity, we no longer assert
*which* peer gets the `Outbound` link. Instead, we assert the core invariant:
that exactly one connection is established between the nodes, and that their
link directions are opposite (`s1.link != s2.link`).
Copies the template script into the git hooks DIR to prevent a potential
attacker overwriting its contents. Also introduces a check before
running the hook against sensitive files from `master`, if there are
changes between the branch and `master`, asks user to confirm
continuation of hook execution.
Introduce the `simulation` directory in anticipation of migrating the
`radicle simulation network` repository. Introduce a loose plan and
explainer of how it will work.
A new module to model the domain of protocol lines and commands
being exchanged is introduced.
This is to increase readability and to pave the way for a
future sans I/O version of the binary crate.
Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
The service learns to consult the policies database to check if a
`NodeId` is blocked.
When a node is blocked the service will prevent any inbound or
outbound connection.
In the case of an inbound connection, a disconnect must be submitted
to the reactor.
In the case of an outbound connection, nodes are filtered out from any
connection lists, and prevented from submitting a connect to the
reactor.
Introduces the ability to explicitly block a peer via the node control
socket. Previously, the node only exposed follow and unfollow commands.
While the underlying policy database schema supported a Block variant,
there was no mechanism to trigger this state via the client handle.
The new block command:
1. Updates the node's follow policy to Block.
2. Immediately disconnects the peer if a session is active.
To prepare for future changes, warn users if their config file currently
does not explicitly specify a default seeding policy.
Because there are now potentially multiple warnings generated that all
relate to the configuration file, group these together and adjust the
wording to be more uniform.
The `SeedingPolicy` type now requires a `Scope`, however existing
configurations allow it to not be specified.
Introduce a `radicle::node::config::Scope` type that allows for an
optional `policy::Scope`. The default value resolves to
`policy::Scope::All`. This preserves backwards compatibility for
existing configurations.
As part of the work to change the default scope, one of the tests
surfaced an issue where the topology was Alice <--> Bob <--> Eve,
however because of the previous 'all' scope Alice could receive Eve
references via Bob, but since the scope was changed to 'follow' Alice
would need to have a direct connection to Eve. TODO left here for future
consideration.
The previous implementation used 'all' as the default for scope, this
could lead to surprising behaviour where a user would fetch all
references for cloned and seeded repositories. Instead have a progressive,
safe by default value - where it fetches only 'followed' references. Later
a user can decide to set the scope to 'all'. NOTE: the default policy
scope was not changed from 'all' and is intended to be changed at a
later date.
Previous implementation used the default scope when a scope wasn't
present. Scope is not present when the policy is block, because block
doesn't have a scope. Instead use an empty string when printing the
block policy.
The previous resolution would favour one `Oid` over another, due to
the call to `Option::or`.
This was found to result in scenarios where an identity document being
advertised was older than the identity document that the fetching node
already has, and would not fetch namespaces by newly added delegates.
The resolution logic is updated to handle all cases for these `Oid`s,
and when they are both present, uses a commit graph check to see which
one is the latest.
An e2e test is added to check ensure behaviour works correctly.
When a non-delegate attempts to update the identity document, their
action will be rejected.
Provide a better error, along with a hint, to the non-delegate.
Changes the `Error::WithHint::hint` field to use a `String` type instead of
`&'static str`. This allows the caller to provide extra hint
information.
To make it easier to use an `Error::with_hint` constructor is added.
The previous error `UnexpectedState` was opaque, and ended up in a
confusing message when a user would try to edit the identity document
while not being a deleagte themselves.
Instead, provide a more specific error that mentions the `Did`
performing the action, and the action itself.
Note that the `UnexpectedState` variant was matched on during
evaluation, meaning that operations would still appear in the timeline
of the COB. With the change to the other variant, the timeline does
not record the operation anymore.
Instead of changing `impl From<SystemTime> for LocalTime` to `TryFrom`,
the implementation was removed, as there are not many callsites, and
they are well served by `LocalTime::now`.
radicle-localtime: drop TryFrom SystemTime
radicle-node: switch SystemTime usage with LocalTime