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
This commit is contained in:
Fintan Halpenny 2024-07-23 11:27:58 +01:00 committed by cloudhead
parent dc34eafdf4
commit 81a3bc3d1b
No known key found for this signature in database
4 changed files with 116 additions and 35 deletions

View File

@ -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 we 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
```

View File

@ -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..
✗ 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
```

View File

@ -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..",
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))
}

View File

@ -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();