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

View File

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

View File

@ -9,7 +9,7 @@ use std::{fmt, io};
use thiserror::Error; use thiserror::Error;
pub use git::VerifyError; pub use git::{ProjectError, VerifyError};
pub use radicle_git_ext::Oid; pub use radicle_git_ext::Oid;
use crate::collections::HashMap; use crate::collections::HashMap;
@ -20,7 +20,6 @@ use crate::git::Url;
use crate::git::{RefError, RefStr, RefString}; use crate::git::{RefError, RefStr, RefString};
use crate::identity; use crate::identity;
use crate::identity::{Id, IdError}; use crate::identity::{Id, IdError};
use crate::storage::git::ProjectError;
use crate::storage::refs::Refs; use crate::storage::refs::Refs;
use self::refs::SignedRefs; use self::refs::SignedRefs;
@ -43,8 +42,6 @@ pub enum Error {
Id(#[from] IdError), Id(#[from] IdError),
#[error("i/o: {0}")] #[error("i/o: {0}")]
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("doc: {0}")]
Doc(#[from] identity::project::Error),
#[error("invalid repository head")] #[error("invalid repository head")]
InvalidHead, InvalidHead,
} }
@ -59,6 +56,8 @@ pub enum FetchError {
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("verify: {0}")] #[error("verify: {0}")]
Verify(#[from] git::VerifyError), Verify(#[from] git::VerifyError),
#[error(transparent)]
Storage(#[from] Error),
} }
pub type RemoteId = PublicKey; pub type RemoteId = PublicKey;
@ -219,7 +218,11 @@ impl Remote<Verified> {
pub trait ReadStorage { pub trait ReadStorage {
fn path(&self) -> &Path; fn path(&self) -> &Path;
fn url(&self, proj: &Id) -> Url; 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>; fn inventory(&self) -> Result<Inventory, Error>;
} }
@ -281,7 +284,11 @@ where
self.deref().inventory() 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) 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> = pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature")); Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));
// FIXME: Should this be here?
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum ProjectError { pub enum ProjectError {
#[error("identity branches diverge from each other")] #[error("identity branches diverge from each other")]
BranchesDiverge, BranchesDiverge,
#[error("identity branches are in an invalid state")] #[error("identity branches are in an invalid state")]
InvalidState, 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}")] #[error("git: {0}")]
Git(#[from] git2::Error), Git(#[from] git2::Error),
#[error("git: {0}")] #[error("git: {0}")]
@ -42,6 +49,16 @@ pub enum ProjectError {
Refs(#[from] refs::Error), 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 { pub struct Storage {
path: PathBuf, 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? // TODO: Don't create a repo here if it doesn't exist?
// Perhaps for checking we could have a `contains` method? // Perhaps for checking we could have a `contains` method?
self.repository(proj)? match self.repository(proj)?.project_of(remote) {
.project_of(remote) Ok(doc) => Ok(Some(doc)),
.map_err(Error::from)
Err(err) if err.is_not_found() => Ok(None),
Err(err) => Err(err),
}
} }
fn inventory(&self) -> Result<Inventory, Error> { 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> { 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(); let mut path = remote.path.clone();
path.push(b'/'); path.push(b'/');
@ -272,15 +292,11 @@ impl Repository {
Identity::load(remote, self) Identity::load(remote, self)
} }
pub fn project_of( pub fn project_of(&self, remote: &RemoteId) -> Result<identity::Doc<Verified>, ProjectError> {
&self, let (doc, _) = identity::Doc::load(remote, self)?;
remote: &RemoteId, let verified = doc.verified()?;
) -> Result<Option<identity::Doc<Verified>>, refs::Error> {
if let Some((doc, _)) = identity::Doc::load(remote, self)? { Ok(verified)
Ok(Some(doc.verified().unwrap()))
} else {
Ok(None)
}
} }
/// Return the canonical identity [`git::Oid`] and document. /// Return the canonical identity [`git::Oid`] and document.
@ -288,7 +304,7 @@ impl Repository {
let mut heads = Vec::new(); let mut heads = Vec::new();
for remote in self.remote_ids()? { for remote in self.remote_ids()? {
let remote = remote?; let remote = remote?;
let oid = Doc::<Unverified>::head(&remote, self)?.unwrap(); let oid = Doc::<Unverified>::head(&remote, self)?;
heads.push(oid.into()); heads.push(oid.into());
} }
@ -329,8 +345,7 @@ impl Repository {
} }
} }
Doc::load_at(longest.into(), self)? Doc::load_at(longest.into(), self)
.ok_or(refs::Error::NotFound)
.map(|(doc, _)| (longest.into(), doc)) .map(|(doc, _)| (longest.into(), doc))
.map_err(ProjectError::from) .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()) Ok(self.inventory.get(&proj).cloned())
} }