Commit Graph

167 Commits

Author SHA1 Message Date
Daniel Norman d973a548b9 node/wire: Test connected node removes active fetch
- Assert that with the peer still `Connected`, `worker_result` clears
  `active[rid]` as expected
- Confirm the disconnect test is exercising the discard path rather than
  passing vacuously
2026-06-22 18:02:57 +01:00
Daniel Norman 3459ad57cc node/wire: Test disconnected node remove active fetch
- Drive `worker_result` through the real wire layer with the peer in
  `Disconnecting` state, mirroring a fetch that ends mid-disconnect
- Assert `active[rid]` survives, proving the result is discarded without
  calling `service.fetched`, which is what orphans the entry
- Add helpers to wrap a service with an active fetch in a `Wire`
2026-06-22 18:02:57 +01:00
Daniel Norman 665e640fe4 node/test: Reproduce orphaned fetch at service layer
- Drive the real cancel-skip path: a connection conflict makes
  `disconnected` early-return on a link mismatch, leaving the fetch
  uncancelled
- Combined with an undelivered fetch result, the `active[rid]` entry is
  orphaned and the repo can no longer be fetched from any node
- Confirm delivering the missing result clears the entry and unblocks
  the queued fetch, showing what the fix must guarantee
2026-06-22 18:02:57 +01:00
Daniel Norman 7b910476c3 node/wire: Always report fetch results to the service
A fetch result was discarded whenever the peer was no longer `Connected`
by the time the worker reported, and queued fetches were dropped silently
when the peer disconnected before the `Io::Fetch` was processed. In both
cases the fetcher's `active` entry for the repo was never cleared. Since
that entry is keyed by repo, it blocked the repository from being fetched
from any node until the process restarted.

- Report the result in `worker_result` even when the peer is no longer
  connected, instead of returning early
- Report a failed fetch when an `Io::Fetch` is dropped for a disconnected
  peer, so the active entry is cleared
- Flip the wire regression test to assert the entry is now cleared on
  disconnect
2026-06-22 18:02:57 +01:00
Adrian Duke 55b4b04002 radicle-node: Replace WAN routeable testing IPs with 198.18.0.0/15 private subnet 2026-06-08 11:23:47 +01:00
Adrian Duke a294e05730 radicle-node: Prevent infinite connection loop on instant dial failure
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.
2026-06-05 18:27:16 +01:00
Fintan Halpenny 9ff0e8b01f just: checking for ellipses
Add custom check for ellipses "...", asking for replacement of "…".

Git ranges and the CLI wildcard matches are ignored.

This change includes fixes to all sites that did not pass the check.
2026-05-28 16:52:02 +01:00
Lorenz Leutgeb cf108311dc node: Heap Profiling with DHAT 2026-05-27 12:07:52 +01:00
Lorenz Leutgeb 09b1f1f77c
node: Fix handling absence of systemd credential
In case `radicle_systemd::credential::path` returns `Ok(None)`, a
misleading warning is logged.

Handle this case separately and do not emit a warning.
2026-05-20 09:36:22 +02:00
Fintan Halpenny 1f40b32b6a protocol: provide more information for invalid timestamps
The invalid timestamp error will only tell the timestamp of the
offending node's message.

Currently, there are issues where that error is occurring but the
timestamp seems legitimate. This patch adds the running node's
timestamp to help debug the issue.
2026-05-18 18:06:26 +02:00
Adrian Duke caee776c38 log: New crate for logger implementations
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>
2026-05-11 16:44:37 +01:00
Lorenz Leutgeb 420af3b710 workspace/rust: 1.90 → 1.95
The update to `flake.lock` is a simple

    nix flake update rust-overlay

to be able to reach 1.95.
2026-05-11 12:09:04 +01:00
Lorenz Leutgeb 9ea040ccd0 rust/msrv: 1.85.0 → 1.88.0
`cargo check` fails because `human-panic` and `sysinfo` require at least
1.88.0.

    +++ command cargo check --release --locked --all-targets
    error: rustc 1.85.0 is not supported by the following packages:
      human-panic@2.0.6 requires rustc 1.88
      sysinfo@0.37.2 requires rustc 1.88

Bump MSRV to fix this.

1.88.0 introduced [let chains], which in turn has `clippy` warn about
nested if statements. All of these sites are fixed in this change.

1.87.0 introduced [`is_multiple_of`], which is a more readable version
of `x % y == 0`.

[let chains]: https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/#let-chains
[`is_multiple_of`]: https://doc.rust-lang.org/std/primitive.usize.html#method.is_multiple_of
2026-05-11 11:23:18 +01:00
Lorenz Leutgeb 0a0e70b1d1
node/systemd: Fix reading credentials
In `cb57e6560fe7767e79b87473a11261fbb82ce84c` the loading of systemd
credentials was refactored. The idea was to only change the accepted IDs
slightly after switching from radicle.xyz to radicle.dev. Unfortunately,
this was done in a sloppy way. The two cases for passphrase and secret
actually differ in ways that was overlooked.

Rectify these mistakes and restore the old behavior:
 1. The logic to read the passphrase from the file that contains the
    credential is moved out of `load_credential` since it only applies
    to the passphrase, not the secret.
 2. The warnings printed are now generic, and will not mention the
    passphrase in case reading the path for the secret fails.
2026-05-07 22:53:57 +02:00
Daniel Norman 6b460c4429 logger: Respect config file log level
- Fixes a bug where the log level set in the config file was
  ignored: `Logger` and `StderrLogger` captured the level in a
  `self.level` field at construction time and checked it in
  `Log::enabled`. After config was loaded, the global
  `log::set_max_level` was updated but `self.level` was not,
  so verbose messages were dropped by the per-instance filter
  even when the global filter allowed them.
- Make `log::set_max_level` the single source of truth: remove
  the `level` field and have `Log::enabled` defer to
  `log::max_level()` so the two filters can no longer drift.
- Update call sites to construct loggers without a level.
- Disable the structured logger's internal filter (set to "trace") so
  that it also falls back to `log::set_max_level`.
2026-05-07 16:23:28 +01:00
Fintan Halpenny 342b4d963a node: Release 0.20.0 2026-05-06 21:00:14 +01:00
Lorenz Leutgeb 3f81e83d36 radicle/crefs: Support Symbolic References
Canonical references can not be used to model symbolic references.

Relax this restriction by adding another member to the payload, named
"symbolic". Its key-value/name-target pairs then translate directly to
canonical symbolic references.

Care is taken to not allow circular references, and that there always
is a rule that could generate the target of a symbolic reference
(or a chain of symbolic references). Still, a symbolic reference may
dangle (for example when its target reference cannot be computed
because of divergence), but at least it can be prevented that a
symref *always* dangles, because there is no rule that would produce
its target.

Co-authored-by: Fintan Halpenny <fintan.halpenny@gmail.com>
2026-05-06 20:51:04 +02:00
Adrian Duke 80d30d3b59 e2e: Fix false-positive in canonical ref update test
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.
2026-05-05 14:41:52 +01:00
Adrian Duke ed340f5724 e2e: Add partial glob match cref test 2026-05-05 14:41:52 +01:00
Fintan Halpenny e87af868db node/test: `NodeHandle::commit_to` should namespace
The `commit_to` method should namespace the reference, so that node is
only ever committing the namespace of that particular node.

This prevents the node from committing to another node's namespace or
the canonical namespace.
2026-05-05 14:41:52 +01:00
Josh Soref 5dae2a8b58 treewide: Spelling
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2026-04-30 15:50:29 -04:00
Josh Soref 08dd1dfda7 node/test: Fix Spelling
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2026-04-30 15:49:56 -04:00
Fintan Halpenny 1a31a9f541 cli: Handle broken pipe (SIGPIPE) gracefully
Prevent `rad` from panicking when its stdout is a closed pipe, e.g.
when running `rad config | head`.

The broken pipes are handled at the output boundaries using three
layers of defence:

1. Inner: Route all stdout output through `term::print()` and
   `term::print_inline()`, which use explicit `writeln!`/`write!` to
   a locked stdout and silently ignore write errors. All `println!`
   calls in `radicle-cli` and `radicle-term` are converted.
2. Middle: Catch `BrokenPipe` errors that propagate up via `anyhow`
   from subcommands in `main::run()`, and exit cleanly with code 0.
3. Outer: Install a panic hook that intercepts `println!` panics
   containing "Broken pipe" (from dependencies like clap) and exits
   cleanly, chained in front of human_panic's hook.

To prevent regressions, `#![deny(clippy::print_stdout)]` is added to
`radicle-cli`, requiring all future stdout output to go through the
safe `term::` functions.

Note: `eprintln!` calls (stderr) are not handled, as broken stderr
pipes are extremely rare in practice.
2026-04-29 10:57:24 +02:00
Lorenz Leutgeb f65175397f oid: `const ZERO_SHA1` instead of `fn sha1_zero`
`fn sha1_zero` can be `const fn`, and since it does not take arguments,
it can just be a `const`. There is no good reason to not have a constant.

Justification for changing the order to first specify "zero" and then
"SHA1": In the future we will support multiple object formats. A
function that produces a zero OID would be parameterized by the object
format then. Imagine `fn zero(format: ObjectFormat)` which would be
called as `Oid::zero(ObjectForma::Sha1)`. In anticipation, we change the
ordering to match.
2026-04-28 14:35:38 +02:00
Lorenz Leutgeb cb57e6560f
node: Rename systemd Credentials
Because we changed our main domain from radicle.xyz to radicle.dev, we
should also change our systemd credential identifiers.

Handling is move into a new function, which also checks the old names
for backwards compatibility.
2026-04-27 18:21:26 +02:00
Lorenz Leutgeb 3bc8abdc29
radicle/node/config: Use `IndexSet`
With `connect: std::collections::HashSet`, we suffer unpredictable reordering
of values in our test cases.

Use `connect: indexmap::IndexSet` instead, which preserves insertion
order for iteration.
2026-04-27 17:36:20 +02:00
Adrian Duke bdd991a5f1 e2e: Fix race condition in test_non_fastforward_identity_doc
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.
2026-04-23 08:45:06 +01:00
Adrian Duke 802e4726e3 e2e: Replace the hardcoded 1-second sleep in `test_connection_crossing` with polling loop
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.
2026-04-23 08:42:32 +01:00
Adrian Duke bfb54bf4be e2e: Fix flakiness in `test_connection_crossing` test
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`).
2026-04-23 08:39:03 +01:00
Lorenz Leutgeb 224c03daf1 node: Expand `target_os` condition for keep-alive
`KeepAlive::with_retries` is not just unavailable on Windows, but on a
bunch of other operating systems. Copy the condition over from
`socket2`.
2026-04-22 12:06:12 +01:00
Lorenz Leutgeb 4a81673d0e node: Fix conditions for socket activation
The implementation of socket activation requires not only the `systemd`
feature, but also `socket2`.
2026-04-22 12:03:05 +01:00
Lorenz Leutgeb ac3eba09a1 I2P Support
Co-authored-by: ps
2026-04-15 12:01:50 +01:00
Lorenz Leutgeb 9b9b5ca996 cargo: Update `cyphernet` from 0.5.2 to 0.5.3
The main change that needs adjustment on our side is that
`impl EcPk for ec25519::PublicKey` changed from `Compressed = [u8; 32]`
to `Compressed = amplify::Bytes32`.

Also remove `impl Deref for PublicKey` to avoid dependents of
`radicle-crypto` depend on a particular implementation, and make the
anonymous member private.

See
<f42396139a>.
2026-04-15 11:59:15 +01:00
Lorenz Leutgeb 4d57ba496d
rust: Disallow lint `dead_code` 2026-04-15 10:06:05 +02:00
Lorenz Leutgeb ca732015fb
clippy: Disallow lints `collapsible_*`
The three lints `collapsible_{if,else_if,match}` are disallowed.
2026-04-15 10:06:05 +02:00
Lorenz Leutgeb 44244dc00f
clippy: Disallow lint `type_complexity`
We just need two exceptions.
2026-04-15 10:06:05 +02:00
Lorenz Leutgeb 94f65a71db
cargo: Remove unused dependencies
A number of dependencies were unused, causing confusion. Remove them.
2026-04-15 01:50:14 +02:00
Lorenz Leutgeb 80c1bba76f
radicle: Prevent `RAD_SOCKET` from polluting tests
The value of `RAD_SOCKET` influences the outcome of `cargo test`.

Users that set this environment variable to, e.g. point `rad` at
a socket at a custom location, were surprised that execution of
tests would interact with their node.

Split `Home::socket` into two functions. `Home::socket_default` is
"pure" and only dependent on the path represented by `Home`. On the
other hand, `Home::socket_from_env` interprets the environment.

Most users will want to use `Home::socket_from_env`.
2026-04-09 17:53:49 +02:00
Lorenz Leutgeb aa28567035 radicle/crefs: Refactor `GetCanonicalRefs`
There is only one type that can really construct canonical references,
which is the the identity document. Remove the corresponding method
from the trait and rename it to `GetRawCanonicalRefs` accordingly.
2026-04-08 11:13:28 +01:00
Lorenz Leutgeb ef101d9ab9 radicle/crefs/protect: Module for protected refs
Separate module for the concern of protecting `refs/rad`.

Note that this also fixes a bug, as previously all refs
*starting with* `refs/rad`, such as `refs/radieschen` were protected.
2026-04-08 11:02:25 +01:00
Lorenz Leutgeb bfb2858054 radicle/storage: Split `WriteRepository::set_head`
The method `set_head` does two things:
 1. Compute the canonical head and set the default branch to target.
 2. Set the symbolic reference `HEAD` to target the default branch.

Split these two concerns into:
 1. `set_default_branch_to_canonical_head`
 2. `set_head_to_default_branch`
2026-04-08 11:02:07 +01:00
Lorenz Leutgeb 22b2871f64 radicle: Add Version to User Agent
The main motivation behind this change is to get just a little
telemetry information from nodes on the network, namely the version
of Radicle they are running.

This is achieved by rewriting `impl Default for UserAgent` which now
uses the version information provided by the build script also used
in other crates.

Also, a new configuration option `node.userAgent` is added, which
allows users to override the user agent if they so please, or set
the value `null`, which will in turn send the user agent
`/radicle/`, which is not really helpful, and the default prior to
this commit.

Creations of `UserAgent` in the whole workspace is cleaned up. In
order to do that `UserAgent::test` is introduced.
2026-04-02 16:26:46 +01:00
Lorenz Leutgeb f223afd9d7 radicle: Refactor COB Storage Access
Instead of passing the signer as an argument to many methods on `Store`,
scope the `Store` itself to a signer.

This further allows to differentiate two different access modes on the
store in `radicle::cob::store::access`: `WriteAs` (which requires signer)
and `ReadOnly` (which does not require a signer).

The caches for issues and patches in `radicle::cob::{issue,patch}::Cache`
are concretised by removing the first type parameter, since it was
specific to issues and patches anyway. This was done in this commit as
it touches very similar usage sites.

Make `Device` less prominent, and instead lean more heavily towards
traits from the `signature` crate, such as `Keypair` and `Verifier`
in addition to `Signer`. Trait bounds regarding `Signer` could be
simplified, but this is left for the future.

In `radicle-cli`, the function `term::cob::patches_mut`, which generates
errors with a hint is used instead of the lower-level `Profile::patches_mut`.

Commands `rad issue cache` and `rad patch cache` now construct a
writeable cache on top of a read-only store.

Many knock-on changes are handled as well, to arrive at a clean state.
2026-03-31 17:57:14 +01:00
Lorenz Leutgeb 10a82958ca
node: Explicit default for `AddressConfig`
The intent to drop outgoing connections is modeled as `Option::None`
which is brittle and easy to miss.

Extend `enum AddressConfig` with a variant that is more explicit.
2026-03-31 16:24:22 +02:00
Lorenz Leutgeb 1e132685bc
cargo: Add feature for Tor support 2026-03-31 16:24:22 +02:00
Lorenz Leutgeb 8bac24d6ab
rust/edition/fmt: 2021 → 2024
Reformat code with `rustfmt`, now that the edition changed from
Rust 2021 to Rust 2024.
2026-03-30 16:57:51 +02:00
Lorenz Leutgeb 0494227e75
rust/edition: 2021 → 2024
Switch from Rust 2021 to Rust 2024.

See <https://doc.rust-lang.org/edition-guide/rust-2024/>.

Regarding the added bound `use<>`, see
<https://blog.rust-lang.org/2024/09/05/impl-trait-capture-rules/>.
2026-03-30 16:56:58 +02:00
Fintan Halpenny 78908682df node: Update to 0.19.0 2026-03-26 15:32:43 +00:00
Lorenz Leutgeb ec36d1c3ae radicle/sigrefs: Merge `SignedRefs{,At}`
Since now all callers expect `SignedRefsAt`, merge the two structs
and name the result just `SignedRefs`.
2026-03-25 08:07:49 +00:00
Fintan Halpenny 33db6637b4 radicle: Teach `rad sync` and `rad clone` to accept feature levels
Teach the `rad sync` and `rad clone` commands to accept the
`--signed-refs-feature-level` option. This option configures that
fetch to use the minimum feature level provided when fetching from
other nodes. This overrides the value of
`node.fetch.signedReferences.featureLevel.minimum`, and should only be
used in scenarios where it is necessary to downgrade the security of a
fetch for backwards compatibility.


To achieve this, the `Handle::fetch` method introduces an optional
parameter for threading through the `FeatureLevel`.
2026-03-24 15:07:18 +00:00