Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: `rad id` allow missing default branch
Merged fintohaps opened 1 year ago

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.

4 files changed +116 -35 dc34eafd 81a3bc3d
added 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
+
```
modified 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
```

modified 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))
}
modified radicle-cli/tests/commands.rs
@@ -453,6 +453,56 @@ fn rad_id_threshold() {
}

#[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();
    let alice = environment.node(config::node("alice"));