Handle errors correctly in storage

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-10-10 14:46:19 +02:00
parent d652df7c30
commit fde0af09f7
No known key found for this signature in database
7 changed files with 148 additions and 115 deletions

View File

@ -851,7 +851,7 @@ pub trait ServiceState {
/// Get the current inventory.
fn inventory(&self) -> Result<Inventory, storage::Error>;
/// Get a project from storage, using the local node's key.
fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, storage::Error>;
fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, storage::ProjectError>;
/// Get the clock.
fn clock(&self) -> &RefClock;
/// Get service configuration.
@ -874,7 +874,7 @@ where
self.storage.inventory()
}
fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, storage::Error> {
fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, storage::ProjectError> {
self.storage.get(&self.node_id(), proj)
}
@ -944,6 +944,8 @@ pub enum LookupError {
Storage(#[from] storage::Error),
#[error(transparent)]
Routing(#[from] routing::Error),
#[error(transparent)]
Project(#[from] storage::ProjectError),
}
/// Information on a peer, that we may or may not be connected to.

View File

@ -17,6 +17,7 @@ use crate::crypto;
use crate::crypto::{Signature, Unverified, Verified};
use crate::git;
use crate::identity::Did;
use crate::storage;
use crate::storage::git::trailers;
use crate::storage::{BranchName, ReadRepository, RemoteId, WriteRepository, WriteStorage};
@ -36,7 +37,7 @@ pub const MAX_STRING_LENGTH: usize = 255;
pub const MAX_DELEGATES: usize = 255;
#[derive(Error, Debug)]
pub enum Error {
pub enum DocError {
#[error("json: {0}")]
Json(#[from] serde_json::Error),
#[error("i/o: {0}")]
@ -47,6 +48,21 @@ pub enum Error {
Git(#[from] git::Error),
#[error("git: {0}")]
RawGit(#[from] git2::Error),
#[error("storage: {0}")]
Storage(#[from] storage::Error),
#[error("git: reference `{0}` was not found")]
NotFound(git::RefString),
}
impl DocError {
/// Whether this error is caused by the document not being found.
pub fn is_not_found(&self) -> bool {
match self {
Self::NotFound(_) => true,
Self::Git(git::Error::NotFound(_)) => true,
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -93,7 +109,7 @@ pub struct Doc<V> {
}
impl Doc<Verified> {
pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), Error> {
pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), DocError> {
let mut buf = Vec::new();
let mut serializer =
serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new());
@ -118,7 +134,7 @@ impl Doc<Verified> {
false
}
pub fn sign<G: crypto::Signer>(&self, signer: G) -> Result<(git::Oid, Signature), Error> {
pub fn sign<G: crypto::Signer>(&self, signer: G) -> Result<(git::Oid, Signature), DocError> {
let (oid, bytes) = self.encode()?;
let sig = signer.sign(&bytes);
@ -130,7 +146,7 @@ impl Doc<Verified> {
remote: &RemoteId,
msg: &str,
storage: &S,
) -> Result<(Id, git::Oid, S::Repository), Error> {
) -> Result<(Id, git::Oid, S::Repository), DocError> {
// You can checkout this branch in your working copy with:
//
// git fetch rad
@ -138,7 +154,7 @@ impl Doc<Verified> {
//
let (doc_oid, doc) = self.encode()?;
let id = Id::from(doc_oid);
let repo = storage.repository(id).unwrap();
let repo = storage.repository(id)?;
let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
let oid = Doc::commit(remote, &tree, msg, &[], repo.raw())?;
@ -153,7 +169,7 @@ impl Doc<Verified> {
msg: &str,
signatures: &[(&PublicKey, Signature)],
repo: &R,
) -> Result<git::Oid, Error> {
) -> Result<git::Oid, DocError> {
let mut msg = format!("{msg}\n\n");
for (key, sig) in signatures {
writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER)
@ -175,7 +191,7 @@ impl Doc<Verified> {
msg: &str,
parents: &[&git2::Commit],
repo: &git2::Repository,
) -> Result<git::Oid, Error> {
) -> Result<git::Oid, DocError> {
let sig = repo
.signature()
.or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
@ -320,48 +336,30 @@ impl Doc<Unverified> {
})
}
pub fn blob_at<R: ReadRepository>(
commit: Oid,
repo: &R,
) -> Result<Option<git2::Blob>, git::Error> {
match repo.blob_at(commit, Path::new(&*PATH)) {
Err(git::ext::Error::NotFound(_)) => Ok(None),
Err(e) => Err(e),
Ok(blob) => Ok(Some(blob)),
}
pub fn blob_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<git2::Blob, DocError> {
repo.blob_at(commit, Path::new(&*PATH))
.map_err(DocError::from)
}
pub fn load_at<R: ReadRepository>(
commit: Oid,
repo: &R,
) -> Result<Option<(Self, Oid)>, git::Error> {
if let Some(blob) = Self::blob_at(commit, repo)? {
let doc = Doc::from_json(blob.content()).unwrap();
return Ok(Some((doc, blob.id().into())));
}
Ok(None)
pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<(Self, Oid), DocError> {
let blob = Self::blob_at(commit, repo)?;
let doc = Doc::from_json(blob.content())?;
Ok((doc, blob.id().into()))
}
pub fn load<R: ReadRepository>(
remote: &RemoteId,
repo: &R,
) -> Result<Option<(Self, Oid)>, git::Error> {
if let Some(oid) = Self::head(remote, repo)? {
Self::load_at(oid, repo)
} else {
Ok(None)
}
pub fn load<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<(Self, Oid), DocError> {
let oid = Self::head(remote, repo)?;
Self::load_at(oid, repo)
}
}
impl<V> Doc<V> {
pub fn head<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<Option<Oid>, git::Error> {
pub fn head<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<Oid, DocError> {
let head = &git::refname!("heads").join(&*git::refs::IDENTITY_BRANCH);
if let Some(oid) = repo.reference_oid(remote, head)? {
Ok(Some(oid))
} else {
Ok(None)
}
repo.reference_oid(remote, head)?
.ok_or_else(|| DocError::NotFound(head.to_owned()))
}
}
@ -379,8 +377,8 @@ pub enum IdentityError {
InvalidSignature(PublicKey, crypto::Error),
#[error("quorum not reached: {0} signatures for a threshold of {1}")]
QuorumNotReached(usize, usize),
#[error("the identity branch was not found")]
NotFound,
#[error("identity document error: {0}")]
Doc(#[from] DocError),
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -424,60 +422,58 @@ impl Identity<Untrusted> {
remote: &RemoteId,
repo: &R,
) -> Result<Identity<Oid>, IdentityError> {
if let Some(head) = Doc::<Untrusted>::head(remote, repo)? {
let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
let head = Doc::<Untrusted>::head(remote, repo)?;
let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
// Retrieve root document.
let root_oid = history.pop().unwrap()?.into();
let root_blob = Doc::blob_at(root_oid, repo)?.unwrap();
let root: git::Oid = root_blob.id().into();
let trusted = Doc::from_json(root_blob.content()).unwrap();
let revision = history.len() as u32;
// Retrieve root document.
let root_oid = history.pop().unwrap()?.into();
let root_blob = Doc::blob_at(root_oid, repo)?;
let root: git::Oid = root_blob.id().into();
let trusted = Doc::from_json(root_blob.content()).unwrap();
let revision = history.len() as u32;
let mut trusted = trusted.verified()?;
let mut current = root;
let mut signatures = Vec::new();
let mut trusted = trusted.verified()?;
let mut current = root;
let mut signatures = Vec::new();
// Traverse the history chronologically.
for oid in history.into_iter().rev() {
let oid = oid?;
let blob = Doc::blob_at(oid.into(), repo)?.unwrap();
let untrusted = Doc::from_json(blob.content()).unwrap();
let untrusted = untrusted.verified()?;
let commit = repo.commit(oid.into())?.unwrap();
let msg = commit.message_raw().unwrap();
// Traverse the history chronologically.
for oid in history.into_iter().rev() {
let oid = oid?;
let blob = Doc::blob_at(oid.into(), repo)?;
let untrusted = Doc::from_json(blob.content()).map_err(DocError::from)?;
let untrusted = untrusted.verified()?;
let commit = repo.commit(oid.into())?.unwrap();
let msg = commit.message_raw().unwrap();
// Keys that signed the *current* document version.
signatures = trailers::parse_signatures(msg).unwrap();
for (pk, sig) in &signatures {
if let Err(err) = pk.verify(blob.content(), sig) {
return Err(IdentityError::InvalidSignature(*pk, err));
}
// Keys that signed the *current* document version.
signatures = trailers::parse_signatures(msg).unwrap();
for (pk, sig) in &signatures {
if let Err(err) = pk.verify(blob.content(), sig) {
return Err(IdentityError::InvalidSignature(*pk, err));
}
// Check that enough delegates signed this next version.
let quorum = signatures
.iter()
.filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key)))
.count();
if quorum < trusted.threshold {
return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
}
trusted = untrusted;
current = blob.id().into();
}
return Ok(Identity {
root,
head,
current,
revision,
doc: trusted,
signatures: signatures.into_iter().collect(),
});
// Check that enough delegates signed this next version.
let quorum = signatures
.iter()
.filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key)))
.count();
if quorum < trusted.threshold {
return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
}
trusted = untrusted;
current = blob.id().into();
}
Err(IdentityError::NotFound)
Ok(Identity {
root,
head,
current,
revision,
doc: trusted,
signatures: signatures.into_iter().collect(),
})
}
}

View File

@ -1,3 +1,4 @@
#![allow(clippy::match_like_matches_macro)]
pub mod collections;
pub mod crypto;
pub mod git;

View File

@ -8,8 +8,10 @@ use thiserror::Error;
use crate::crypto::{Signer, Verified};
use crate::git;
use crate::identity::project::DocError;
use crate::identity::Id;
use crate::node;
use crate::storage::git::ProjectError;
use crate::storage::refs::SignedRefs;
use crate::storage::{BranchName, ReadRepository as _, RemoteId, WriteRepository as _};
use crate::{identity, storage};
@ -19,7 +21,7 @@ pub static REMOTE_NAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("rad")
#[derive(Error, Debug)]
pub enum InitError {
#[error("doc: {0}")]
Doc(#[from] identity::project::Error),
Doc(#[from] identity::project::DocError),
#[error("doc: {0}")]
DocVerification(#[from] identity::project::VerificationError),
#[error("git: {0}")]
@ -95,6 +97,8 @@ pub enum ForkError {
NotFound(Id),
#[error("project identity error: {0}")]
InvalidIdentity(#[from] storage::git::ProjectError),
#[error("project identity document error: {0}")]
Doc(#[from] DocError),
#[error("git: invalid reference")]
InvalidReference,
}
@ -206,6 +210,8 @@ pub enum CloneError {
Fork(#[from] ForkError),
#[error("checkout: {0}")]
Checkout(#[from] CheckoutError),
#[error("identity document error: {0}")]
Doc(#[from] DocError),
}
pub fn clone<P: AsRef<Path>, G: Signer, S: storage::WriteStorage, H: node::Handle>(
@ -258,6 +264,8 @@ pub enum CheckoutError {
Storage(#[from] storage::Error),
#[error("project `{0}` was not found in storage")]
NotFound(Id),
#[error("project error: {0}")]
Project(#[from] ProjectError),
}
/// Checkout a project from storage as a working copy.

View File

@ -9,7 +9,7 @@ use std::{fmt, io};
use thiserror::Error;
pub use git::VerifyError;
pub use git::{ProjectError, VerifyError};
pub use radicle_git_ext::Oid;
use crate::collections::HashMap;
@ -20,7 +20,6 @@ use crate::git::Url;
use crate::git::{RefError, RefStr, RefString};
use crate::identity;
use crate::identity::{Id, IdError};
use crate::storage::git::ProjectError;
use crate::storage::refs::Refs;
use self::refs::SignedRefs;
@ -43,8 +42,6 @@ pub enum Error {
Id(#[from] IdError),
#[error("i/o: {0}")]
Io(#[from] io::Error),
#[error("doc: {0}")]
Doc(#[from] identity::project::Error),
#[error("invalid repository head")]
InvalidHead,
}
@ -59,6 +56,8 @@ pub enum FetchError {
Io(#[from] io::Error),
#[error("verify: {0}")]
Verify(#[from] git::VerifyError),
#[error(transparent)]
Storage(#[from] Error),
}
pub type RemoteId = PublicKey;
@ -219,7 +218,11 @@ impl Remote<Verified> {
pub trait ReadStorage {
fn path(&self) -> &Path;
fn url(&self, proj: &Id) -> Url;
fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<identity::Doc<Verified>>, Error>;
fn get(
&self,
remote: &RemoteId,
proj: Id,
) -> Result<Option<identity::Doc<Verified>>, ProjectError>;
fn inventory(&self) -> Result<Inventory, Error>;
}
@ -281,7 +284,11 @@ where
self.deref().inventory()
}
fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<identity::Doc<Verified>>, Error> {
fn get(
&self,
remote: &RemoteId,
proj: Id,
) -> Result<Option<identity::Doc<Verified>>, ProjectError> {
self.deref().get(remote, proj)
}
}

View File

@ -28,12 +28,19 @@ pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));
// FIXME: Should this be here?
#[derive(Error, Debug)]
pub enum ProjectError {
#[error("identity branches diverge from each other")]
BranchesDiverge,
#[error("identity branches are in an invalid state")]
InvalidState,
#[error("storage error: {0}")]
Storage(#[from] Error),
#[error("identity document error: {0}")]
Doc(#[from] identity::project::DocError),
#[error("identity verification error: {0}")]
Verify(#[from] identity::project::VerificationError),
#[error("git: {0}")]
Git(#[from] git2::Error),
#[error("git: {0}")]
@ -42,6 +49,16 @@ pub enum ProjectError {
Refs(#[from] refs::Error),
}
impl ProjectError {
/// Whether this error is caused by the project not being found.
pub fn is_not_found(&self) -> bool {
match self {
Self::Doc(doc) => doc.is_not_found(),
_ => false,
}
}
}
pub struct Storage {
path: PathBuf,
}
@ -68,12 +85,15 @@ impl ReadStorage for Storage {
}
}
fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, Error> {
fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, ProjectError> {
// TODO: Don't create a repo here if it doesn't exist?
// Perhaps for checking we could have a `contains` method?
self.repository(proj)?
.project_of(remote)
.map_err(Error::from)
match self.repository(proj)?.project_of(remote) {
Ok(doc) => Ok(Some(doc)),
Err(err) if err.is_not_found() => Ok(None),
Err(err) => Err(err),
}
}
fn inventory(&self) -> Result<Inventory, Error> {
@ -97,7 +117,7 @@ impl WriteStorage for Storage {
}
fn fetch(&self, proj_id: Id, remote: &Url) -> Result<Vec<RefUpdate>, FetchError> {
let mut repo = self.repository(proj_id).unwrap();
let mut repo = self.repository(proj_id)?;
let mut path = remote.path.clone();
path.push(b'/');
@ -272,15 +292,11 @@ impl Repository {
Identity::load(remote, self)
}
pub fn project_of(
&self,
remote: &RemoteId,
) -> Result<Option<identity::Doc<Verified>>, refs::Error> {
if let Some((doc, _)) = identity::Doc::load(remote, self)? {
Ok(Some(doc.verified().unwrap()))
} else {
Ok(None)
}
pub fn project_of(&self, remote: &RemoteId) -> Result<identity::Doc<Verified>, ProjectError> {
let (doc, _) = identity::Doc::load(remote, self)?;
let verified = doc.verified()?;
Ok(verified)
}
/// Return the canonical identity [`git::Oid`] and document.
@ -288,7 +304,7 @@ impl Repository {
let mut heads = Vec::new();
for remote in self.remote_ids()? {
let remote = remote?;
let oid = Doc::<Unverified>::head(&remote, self)?.unwrap();
let oid = Doc::<Unverified>::head(&remote, self)?;
heads.push(oid.into());
}
@ -329,8 +345,7 @@ impl Repository {
}
}
Doc::load_at(longest.into(), self)?
.ok_or(refs::Error::NotFound)
Doc::load_at(longest.into(), self)
.map(|(doc, _)| (longest.into(), doc))
.map_err(ProjectError::from)
}

View File

@ -44,7 +44,11 @@ impl ReadStorage for MockStorage {
}
}
fn get(&self, _remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, Error> {
fn get(
&self,
_remote: &RemoteId,
proj: Id,
) -> Result<Option<Doc<Verified>>, git::ProjectError> {
Ok(self.inventory.get(&proj).cloned())
}