rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodab0c3f5d81cdcc63c13634d00447f740f95d4975
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "d783d6584193575cfc4a550bf925aa38cac0ce4f",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"title": "cli: `rad id` allow missing default branch",
"state": {
"status": "merged",
"conflicts": []
},
"before": "a04381f4e5fdff04a2f9bfa15a16d2addbcbe8a7",
"after": "ab0c3f5d81cdcc63c13634d00447f740f95d4975",
"commits": [
"ab0c3f5d81cdcc63c13634d00447f740f95d4975"
],
"target": "3fc92f83fe089263c470b328414d923227cf0e4a",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "d783d6584193575cfc4a550bf925aa38cac0ce4f",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "If a peer had created any references but was also missing the\ncanonical branch, the `rad id update` command will fail when adding\nthat peer as a delegate.\n\nInstead, the verification errors are tracked but only returned if\nthere is not enough delegates for a threshold.",
"base": "a04381f4e5fdff04a2f9bfa15a16d2addbcbe8a7",
"oid": "9d259c555d236b7fbb9d0547dacef35749cb929f",
"timestamp": 1721743867
},
{
"id": "e0f34e9e73af33a8e327e1babe1d3e7715b7c576",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Changes:\n- Simplify `VerificationError` type and the verification logic",
"base": "a04381f4e5fdff04a2f9bfa15a16d2addbcbe8a7",
"oid": "ab0c3f5d81cdcc63c13634d00447f740f95d4975",
"timestamp": 1721749018
},
{
"id": "199c9c9113aefb9158dd5d6371e1ec6a8367da0b",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Rebase and minor output fix.",
"base": "dc34eafdf4677247b539ed5c0f83199e372ef594",
"oid": "5b251fd9ea7f2124392d78d1a1582f18bd6f1cfc",
"timestamp": 1721751956
},
{
"id": "cc4289f7511edbcb86ade5a1440cf6fbb5d37c24",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Hotfix.",
"base": "dc34eafdf4677247b539ed5c0f83199e372ef594",
"oid": "81a3bc3d1b24841b57bfb7c8c205c4959cb92670",
"timestamp": 1721832233
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "34d9188a-9484-4cc3-9812-085dacfde500"
},
"info_url": "https://cci.rad.levitte.org//34d9188a-9484-4cc3-9812-085dacfde500.html"
}
Started at: 2026-09-08 04:01:03.884061+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/34d9188a-9484-4cc3-9812-085dacfde500/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 38 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout d783d6584193575cfc4a550bf925aa38cac0ce4f
✓ Switched to branch patch/d783d65 at revision e0f34e9
✓ Branch patch/d783d65 setup to track rad/patches/d783d6584193575cfc4a550bf925aa38cac0ce4f
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout ab0c3f5d81cdcc63c13634d00447f740f95d4975
HEAD is now at ab0c3f5d cli: `rad id` allow missing default branch
Exit code: 0
$ rad patch show d783d6584193575cfc4a550bf925aa38cac0ce4f -p
╭────────────────────────────────────────────────────────────────────────────────╮
│ Title cli: `rad id` allow missing default branch │
│ Patch d783d6584193575cfc4a550bf925aa38cac0ce4f │
│ Author fintohaps z6Mkire…SQZ3voM │
│ Head ab0c3f5d81cdcc63c13634d00447f740f95d4975 │
│ Base a04381f4e5fdff04a2f9bfa15a16d2addbcbe8a7 │
│ Target master │
│ Branches patch/d783d65 │
│ Commits ahead 1, behind 1241 │
│ Status merged │
│ │
│ If a peer had created any references but was also missing the │
│ canonical branch, the `rad id update` command will fail when adding │
│ that peer as a delegate. │
│ │
│ Instead, the verification errors are tracked but only returned if │
│ there is not enough delegates for a threshold. │
├────────────────────────────────────────────────────────────────────────────────┤
│ ab0c3f5 cli: `rad id` allow missing default branch │
├────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision d783d65 @ a04381f..9d259c5 by fintohaps z6Mkire…SQZ3voM 2 years ago │
│ ↑ Revision e0f34e9 @ a04381f..ab0c3f5 by fintohaps z6Mkire…SQZ3voM 2 years ago │
│ ↑ Revision 199c9c9 @ dc34eaf..5b251fd by cloudhead z6MksFq…bS9wzpT 2 years ago │
│ ↑ Revision cc4289f @ dc34eaf..81a3bc3 by cloudhead z6MksFq…bS9wzpT 2 years ago │
│ └─ ✓ merged by cloudhead z6MksFq…bS9wzpT │
╰────────────────────────────────────────────────────────────────────────────────╯
commit ab0c3f5d81cdcc63c13634d00447f740f95d4975
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Tue Jul 23 11:27:58 2024 +0100
cli: `rad id` allow missing default branch
If a peer had created any references but was also missing the
canonical branch, the `rad id update` command will fail when adding
that peer as a delegate.
Instead, missing delegates and missing default branches are tracked
during verification. If the number of missing delegates means the
`threshold` cannot be met, then the errors are returned.
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
diff --git a/radicle-cli/examples/rad-id-threshold-soft-fork.md b/radicle-cli/examples/rad-id-threshold-soft-fork.md
new file mode 100644
index 000000000..2724bb3da
--- /dev/null
+++ b/radicle-cli/examples/rad-id-threshold-soft-fork.md
@@ -0,0 +1,48 @@
+In some cases, a peer can create references, which includes `rad/sigrefs`,
+without having pushed the canonical default branch. For example, Bob can create
+an issue in the repository:
+
+``` ~bob
+$ rad issue open --title "Add Bob as a delegate" --description "We agreed to add me as a delegate, so I am creating an issue to track that work" --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+╭──────────────────────────────────────────────────────────────╮
+│ Title Add Bob as a delegate │
+│ Issue f12d512c51d30429f7916db038ae0360e2e938c2 │
+│ Author bob (you) │
+│ Status open │
+│ │
+│ We agreed to add me as a delegate, so I am creating an issue │
+│ to track that work │
+╰──────────────────────────────────────────────────────────────╯
+✓ Synced with 1 node(s)
+```
+
+and if inspect Alice's references, then we will see the following:
+
+``` ~alice
+$ rad inspect --refs
+z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+└── refs
+ ├── cobs
+ │ └── xyz.radicle.id
+ │ └── 0656c217f917c3e06234771e9ecae53aba5e173e
+ ├── heads
+ │ └── master
+ └── rad
+ ├── id
+ └── sigrefs
+z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+└── refs
+ ├── cobs
+ │ └── xyz.radicle.issue
+ │ └── f12d512c51d30429f7916db038ae0360e2e938c2
+ └── rad
+ └── sigrefs
+```
+
+Despite not having the canonical branch, Alice should still be able to add Bob
+as a delegate, since a threshold of 1 can still be reached:
+
+``` ~alice
+$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q
+7be665f9fccba97abb21b2fa85a6fd3181c72858
+```
diff --git a/radicle-cli/examples/rad-id-threshold.md b/radicle-cli/examples/rad-id-threshold.md
index 9682ce0e4..ec3d338f3 100644
--- a/radicle-cli/examples/rad-id-threshold.md
+++ b/radicle-cli/examples/rad-id-threshold.md
@@ -8,9 +8,9 @@ can be updated.
``` ~alice (fail)
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --threshold 2
✗ Error: failed to verify delegates for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-✗ Error: a threshold of 2 delegates cannot be met, found 1 delegate(s) and the following delegates are missing [did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk]
+✗ Error: the threshold of 2 delegates cannot be met due to the following errors
+✗ Error: the delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk is missing
✗ Hint: run `rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk` to follow this missing peer
-✗ Hint: run `rad sync -f` to attempt to fetch the newly followed peers
✗ Error: fatal: refusing to update identity document
```
diff --git a/radicle-cli/src/commands/id.rs b/radicle-cli/src/commands/id.rs
index 617a55446..2bddd60b6 100644
--- a/radicle-cli/src/commands/id.rs
+++ b/radicle-cli/src/commands/id.rs
@@ -441,6 +441,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
if let Some(errs) = verify_delegates(&proposal, &repo)? {
term::error(format!("failed to verify delegates for {rid}"));
+ term::error(format!(
+ "the threshold of {} delegates cannot be met due to the following errors",
+ proposal.threshold
+ ));
for e in errs {
e.print();
}
@@ -761,15 +765,14 @@ fn print_diff(
Ok(())
}
+#[derive(Clone)]
enum VerificationError {
MissingDefaultBranch {
branch: radicle::git::RefString,
did: Did,
},
- InsufficientDelegates {
- threshold: usize,
- met: usize,
- missing: Vec<Did>,
+ MissingDelegate {
+ did: Did,
},
}
@@ -781,21 +784,11 @@ impl VerificationError {
term::format::secondary(branch),
term::format::did(did)
)),
- VerificationError::InsufficientDelegates {
- threshold,
- met,
- missing,
- } => {
- term::error(format!(
- "a threshold of {threshold} delegates cannot be met, found {met} delegate(s) and the following delegates are missing [{}]",
- missing.iter().map(|did| did.to_string()).collect::<Vec<_>>().join(","),
+ VerificationError::MissingDelegate { did } => {
+ term::error(format!("the delegate {did} is missing"));
+ term::hint(format!(
+ "run `rad follow {did}` to follow this missing peer"
));
- for did in missing {
- term::hint(format!(
- "run `rad follow {did}` to follow this missing peer"
- ));
- }
- term::hint("run `rad sync -f` to attempt to fetch the newly followed peers")
}
}
}
@@ -811,33 +804,23 @@ where
let dids = &proposal.delegates;
let threshold = proposal.threshold;
let (canonical, _) = repo.canonical_head()?;
- let mut errors = Vec::with_capacity(dids.len());
- let mut local_delegates = 0;
let mut missing = Vec::with_capacity(dids.len());
+
for did in dids {
match refs::SignedRefsAt::load((*did).into(), repo)? {
None => {
- missing.push(*did);
+ missing.push(VerificationError::MissingDelegate { did: *did });
}
Some(refs::SignedRefsAt { sigrefs, .. }) => {
if sigrefs.get(&canonical).is_none() {
- errors.push(VerificationError::MissingDefaultBranch {
+ missing.push(VerificationError::MissingDefaultBranch {
branch: canonical.to_ref_string(),
did: *did,
- })
- } else {
- local_delegates += 1;
+ });
}
}
}
}
- if local_delegates < threshold {
- errors.push(VerificationError::InsufficientDelegates {
- threshold,
- met: local_delegates,
- missing,
- });
- }
- Ok((!errors.is_empty()).then_some(errors))
+ Ok((dids.len() - missing.len() < threshold).then_some(missing))
}
diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs
index 1eddcdd40..3943d3982 100644
--- a/radicle-cli/tests/commands.rs
+++ b/radicle-cli/tests/commands.rs
@@ -452,6 +452,56 @@ fn rad_id_threshold() {
.unwrap();
}
+#[test]
+fn rad_id_threshold_soft_fork() {
+ let mut environment = Environment::new();
+ let alice = environment.node(config::node("alice"));
+ let bob = environment.node(config::node("bob"));
+ let working = tempfile::tempdir().unwrap();
+ let working = working.path();
+ let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
+ // Setup a test repository.
+ fixtures::repository(working.join("alice"));
+
+ test(
+ "examples/rad-init.md",
+ working.join("alice"),
+ Some(&alice.home),
+ [],
+ )
+ .unwrap();
+
+ let mut alice = alice.spawn();
+ let mut bob = bob.spawn();
+
+ let events = bob.handle.events();
+ bob.handle.seed(acme, Scope::All).unwrap();
+ alice.connect(&bob).converge([&bob]);
+
+ events
+ .wait(
+ |e| matches!(e, Event::RefsFetched { .. }).then_some(()),
+ time::Duration::from_secs(6),
+ )
+ .unwrap();
+
+ formula(&environment.tmp(), "examples/rad-id-threshold-soft-fork.md")
+ .unwrap()
+ .home(
+ "alice",
+ working.join("alice"),
+ [("RAD_HOME", alice.home.path().display())],
+ )
+ .home(
+ "bob",
+ working.join("bob"),
+ [("RAD_HOME", bob.home.path().display())],
+ )
+ .run()
+ .unwrap();
+}
+
#[test]
fn rad_id_update_delete_field() {
let mut environment = Environment::new();
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny clippy::all cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 34d9188a-9484-4cc3-9812-085dacfde500 -v /opt/radcis/ci.rad.levitte.org/cci/state/34d9188a-9484-4cc3-9812-085dacfde500/s:/34d9188a-9484-4cc3-9812-085dacfde500/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/34d9188a-9484-4cc3-9812-085dacfde500/w:/34d9188a-9484-4cc3-9812-085dacfde500/w -w /34d9188a-9484-4cc3-9812-085dacfde500/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /34d9188a-9484-4cc3-9812-085dacfde500/s/script.sh
+ cargo --version
info: syncing channel updates for '1.77-x86_64-unknown-linux-gnu'
info: latest update on 2024-04-09, rust version 1.77.2 (25ef9e3d8 2024-04-09)
info: downloading component 'cargo'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: installing component 'cargo'
info: installing component 'rust-std'
info: installing component 'rustc'
cargo 1.77.2 (e52e36006 2024-03-26)
+ rustc --version
rustc 1.77.2 (25ef9e3d8 2024-04-09)
+ cargo fmt --check
error: 'cargo-fmt' is not installed for the toolchain '1.77-x86_64-unknown-linux-gnu'.
To install, run `rustup component add --toolchain 1.77-x86_64-unknown-linux-gnu rustfmt`
Exit code: 1
{
"response": "finished",
"result": "failure"
}