Commit Graph

31 Commits

Author SHA1 Message Date
Fintan Halpenny 82dc3fe20c radicle/cob/identity: Enforce sibling-accept invariant for `RevisionAccept` in `action`
On `Action::RevisionAccept` use `has_active_sibling_accept` to check for a sibling accept.
If one is found, the `RevisionAccept` is logged and skipped.

This preserves the invariant of a single accept per delegate on active, sibling
revisions without throwing an error on existing histories.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 8e677a9d90 radicle/cob/identity: Add has_active_sibling_accept helper
Add a helper method to check if a delegate has already voted on a sibling
revision.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 62f64744ad radicle/cob/identity: Test accept-before-redact convergence
Companion to the redact-vs-accept test, with reversed timestamp ordering so
accept wins.
2026-07-03 15:22:13 +01:00
Fintan Halpenny f720526fc9 radicle/cob/identity: Test concurrent redact-vs-accept convergence
Verifies that when Alice redacts a revision and Bob concurrently
accepts it, both nodes converge after sync. The CRDT evaluation
order (timestamp-based) determines which operation takes effect.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 21700e4962 radicle/cob/identity: Fix cascading_rejections test to test actual cascading
The test previously had B2 as a sibling of B1 (both children of A1),
because update() always uses self.current as parent. This meant
the test was testing multi-sibling rejection, not cascading.

Now B2 is a child of B1 via a manual transaction, so when B1 is
rejected (sibling E1 accepted), B2 is rejected via RejectedBy::Parent
(cascade), which is what the test name promises.
2026-07-03 15:22:13 +01:00
Lorenz Leutgeb ebf0f417f7 radicle/cob/identity: Deprecate `Identity::id`
In the heartwood workspace, there is just a single use of this function.
Also, the repository ID can be computed on demand, saving memory.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 97f279fe41 radicle/cob/identity: Test redacting accepted revisions
Add a test to ensure that a previously accepted revision cannot be redacted.
2026-07-03 15:22:13 +01:00
Fintan Halpenny e0e231813f radicle/cob/identity: Test `authorization_based_on_parent_not_current`
To reflect that autorization is governed by the causal parent of a
revision, not the currently accepted revision.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 108686e299 radicle/cob/identity: Test queued children with delegate change
Ensure that adopting queued children takes into account that the delegate set
can change.
2026-07-03 15:22:13 +01:00
Adrian Duke da2c523d41 radicle/cob/identity: Add Property Tests
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.
2026-07-03 15:22:13 +01:00
Adrian Duke 94a24a7283 radicle/cob/identity: Fix comments in test `reject_concurrent`
This incorrectly listed the number of votes and delegates.
2026-07-03 15:22:13 +01:00
Adrian Duke 2768fe79d2 radicle/cob/identity: Test evaluating children of accepted revision
Add a test case to show that when a revision becomes accepted that its children,
which reach a majority vote, are also marked as accepted.
2026-07-03 15:22:13 +01:00
Adrian Duke c174efccb9 radicle/cob/identity: Test cascading of redaction
Add a test case to show that when a revision is redacted that its children also
become redacted, tracking `RedactedBy::Parent`.
2026-07-03 15:22:13 +01:00
Adrian Duke 2349eebf5d radicle/cob/identity: Test cascading rejection
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.
2026-07-03 15:22:13 +01:00
Adrian Duke 39972b0c46 radicle/cob/identity: Test terminal states
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`.
2026-07-03 15:22:13 +01:00
Adrian Duke 489360d0e5 radicle/cob/identity: Add concurrent terminal state test
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.
2026-07-03 15:22:13 +01:00
Lorenz Leutgeb ba4a79af90 radicle/cob/identity: Test rejected sibling
Add a test case to show that when Bob creates two sibling changes `b1` and `b2`,
that the acceptance of one means the other gets rejected.
2026-07-03 15:22:13 +01:00
Lorenz Leutgeb d8c14222a2 radicle/cob/identity: Rewrite Evaluation
The evaluation of the reposioty identity is rewritten to better handle cases
where there are child and sibling revisions that are active at the same time.

In particular, `fn Identity::action` is now free of any references to
`self.current`.

This is achieved through two main improvements. The first is that the `State`
of revisions changes to improve clarity:
 1. `Active` and `Accepted` remain, and their meanings also remain the same.
 2. `Stale` is removed entirely.
 3. `Rejected` is improved to also contain `RejectedBy`, to keep track of the
    reason for rejection.
 4. `Redacted` is promoted to a state. Similarly, the reason for
    redaction is tracked by `RedactedBy`.

The transition of a revision from `Active` to one of the other states now
influences siblings and children.

If a revision transitions to `Accepted`, then the sibling revisions can no
longer transition to `Accepted`. They are considered `Rejected` where the reason
is `Sibling`. This cascades: Children of siblings are `Rejected`
recursively, tracking `RejectedBy::Parent`.
Any children of the `Accepted` revision are also evaluated to see if they can be
similarly transitioned to the `Accepted` state; since they may have already been
voted on.

If a revision was rejected by a majority, then the it transitions to `Rejected`
with `RejectedBy::Vote`. Dually to acceptance, the children of this revision
are also rejected with the reason of `Ancestor`.

Finally, when the author of a revision redacts a revision, it transitions
to `Redacted`, tracking `RedactedBy::Author`, and its children are redacted
tracking `RedactedBy::Parent`.

The test is adjusted to `remove_delegate_concurrent` reflect that concurrently
proposed revisions are retained in the timeline and explicitly marked as rejected,
rather than being dropped entirely (as before).
2026-07-02 09:56:22 +01:00
Lorenz Leutgeb b145b95ee4 radicle/cob/identity: Use verdicts to count votes
Evaluation of this COB is implemented under the assumption that
delegates accept at most one revision.

This can lead to issues, if some delegates create new revisions eagerly,
without waiting for the others to vote on earlier revisions. As the
eager delegates start accepting newer and newer revisions, this shadows
their acceptance of earlier revisions, which leads to failure to
recognize that these earlier revisions were actually accepted by a
majority.

To avoid such situations, remove `heads`, and always count the number of
"accept" verdicts explicitly.
2026-07-02 09:56:22 +01:00
Lorenz Leutgeb 442df34be7 radicle/cob/identity: Strengthen test assumptions
Not only assert that the revision is active, but
also its parent.
2026-07-02 09:56:22 +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 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
Fintan Halpenny 1566646762 radcile/cob/identity: mark ApplyError as non_exhaustive 2026-02-19 21:06:43 +01:00
Adrian Duke 67a4a712e4 radicle: add new error type for unauthorized non-delegates
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.
2026-01-22 10:24:26 +00:00
Lorenz Leutgeb b0beef4391
workspace/rust/clippy: Fix all warnings
Fix clippy warnings following the upgrade of the Rust toolchain to
version 1.90. Almost all are "mismatched_lifetime_syntaxes".
2025-10-17 12:56:43 +02:00
Lorenz Leutgeb b7cfcfff75
workspace: A Little Less `git2`
Make `git2` an *optional* dependency of `radicle-cob`, and refactor
`radicle` to depend on crates that in turn do not depend on `git2`
*non-optionally*

The main offending dependency of `radicle-cob` is `radicle-git-ext`
from the `radicle-git` workspace in repository
(rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt) which *non-optionally* depends on
`git2`.

So, to achieve removal of this dependency:

 1. The crate is refactored to depend on the new crates
    `radicle-git-ref-format` `radicle-git-metadata`, and
    `radicle-oid` introduced in the previous commits, instead of
    `radicle-git-ext`.
 2. Some code from the `radicle-git-ext` crate in the `radicle-git`
    workspace in repository (rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt) is
    copied. See `crates/radicle-cob/src/backend/git/commit.rs`.

This cascades to `radicle` and its dependents.

  Firstly, the there is an
`impl Deref<Target=git2::Oid> for radicle_git_ext::Oid`. This made
it very convenient to just deref to obtain the wrapped `git2::Oid`,
so there are many expressions of the shape `*oid` in `radicle` and
its dependents. However `radicle-oid` does not provide
`impl Deref<Target=git2::Oid> for radicle_oid::Oid`, as notably,
`Target` is an associated type, and not a type parameter, so an
implementation of `Deref` would tie the new `Oid` too tightly to a
particular implementation.
Instead, work with `impl From<radicle_oid::Oid> for git2::Oid`
(which can be enabled using the feature flag `radicle-oid/git2`).
This explains the changes from `*oid` to `oid.into()` at every
transition to "`git2` land".

  Secondly, `radicle` and its dependents are refactored to also depend
on `radicle-git-ref-format` and (much less prominently)
`radicle-git-metadata` instead of `radicle-git-ext`
This is to avoid pulling in `git2` via these dependencies.

  Thirdly, as the re-exports in `crates/radicle/src/git.rs` change,
they are at the same time also cleaned up. Notably, the types from
`radicle-git-ref-format` are re-exported under `fmt` only, not "twice".

While overall this obviously is very much a breaking change, these
changes should mostly amount to changing from `Deref` to `Into`, i.e.,
`*oid` → `oid.into()` and rewriting imports for `radicle::git::fmt`.
This is indicated by the mostly mechanical nature of the changes to
`crates/radicle-{cli,node,remote-helper}`.
2025-10-13 16:14:23 +02:00
Lorenz Leutgeb 3780f908a2 radicle/identity: Stabilize tests
The following tests generated an unstable object ID, thus linearizing
history in a different order, causing failures. Fix this by using the
`stable-commits` feature of `radicle-cob`.

    thread 'cob::identity::test::test_identity_redact_revision'
    panicked at crates\radicle\src\cob\identity.rs:1338:9:
    assertion `left == right` failed
      left: [Oid(b4307ded046befba374bf8cd9fd787592ceb615c),
             Oid(a04165e3d3717c5a1413ec78e852bd8e1bb049d4),
             Oid(820d3faf5b507888173b26ccd1a2a81666bd2573),
             Oid(30ca6f078a401bf542049594fbb1c8d2371c9819),
             Oid(9e9c46971014b123a31cd1195078f95c2e319419)]
     right: [Oid(b4307ded046befba374bf8cd9fd787592ceb615c),
             Oid(a04165e3d3717c5a1413ec78e852bd8e1bb049d4),
             Oid(820d3faf5b507888173b26ccd1a2a81666bd2573),
             Oid(9e9c46971014b123a31cd1195078f95c2e319419),
             Oid(30ca6f078a401bf542049594fbb1c8d2371c9819)]
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

    thread 'cob::identity::test::test_identity_reject_concurrent'
    panicked at crates\radicle\src\cob\identity.rs:1494:9:
    assertion `left == right` failed
      left: [Oid(b4307ded046befba374bf8cd9fd787592ceb615c),
             Oid(bce4233f6a09d6e9c021ba436e3c6923a1a78f31),
             Oid(7cacb23be7079c16c8ad1b2458ae5bac95a1295b),
             Oid(12b00e2e871b699c84c20427a64e029934c15ec4),
             Oid(07431c8d80c11492ef248e07126370e874de3435),
             Oid(98b42450159a359c2f482652c66d79b05d0af624)]
     right: [Oid(b4307ded046befba374bf8cd9fd787592ceb615c),
             Oid(bce4233f6a09d6e9c021ba436e3c6923a1a78f31),
             Oid(7cacb23be7079c16c8ad1b2458ae5bac95a1295b),
             Oid(98b42450159a359c2f482652c66d79b05d0af624),
             Oid(12b00e2e871b699c84c20427a64e029934c15ec4),
             Oid(07431c8d80c11492ef248e07126370e874de3435)]
2025-10-13 08:49:24 +01:00
Lorenz Leutgeb 880634acd4
radicle/git/raw: Capture all `git2` re-exports
A refactoring internal to the `radicle` crate, with the goal of
making dependency on `git2` clearer and more controlled.
`radicle::git::raw` is changed from a complete re-export of `git2`
to a module that selectively re-exports (many) members of `git2`.
required to build the workspace (potentially breaking dependents
outside the workspace, but given just how many types are
re-exported, this seems unlikely).

In the future, no more `use git2::…` statements should be added
outside of `crates/radicle/src/git/raw.rs`. This is in an effort to
decrease dependence on `git2` in the future.
2025-10-10 11:29:24 +02:00
Sebastian Martinez 1d7478cd90 radicle: introduce `cob::common::Title`
This patch adds the `cob::common::Title` struct, this allows instead of
using `std::str::String` or generics and traits for it, to define more
precisely what a title should be.
It trims the provided string and makes sure it contains no carriage
return or new line characters.

Where a `std::str::String` makes more sense so it's easier to mutate it,
we keep the code as is.

Co-authored-by: Fintan Halpenny <fintan.halpenny@gmail.com>
2025-08-19 17:05:33 +02:00
Fintan Halpenny 044ff8adde radicle: introduce specialised Stream types
Add type aliases to specialise the `Stream` type for each of `Patch`, `Issue`,
and `Identity`.

An `init` constructor is also added for each to make it easier to construct each
stream.
2025-08-19 14:49:51 +01:00
Lorenz Leutgeb 5bc2dc677b repo: Move workspace crates into `crates` subdirectory
This keeps the top level of the repo cleaner, e.g.
<https://github.com/radicle-dev/heartwood> scrolls less and
<https://app.radicle.xyz/nodes/rosa.radicle.xyz/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/>
might show more files at the top level.

This way we also don't have to explicitly name all crates in
`Cargo.toml`.
2025-06-09 15:09:21 +02:00
Renamed from radicle/src/cob/identity.rs (Browse further)