Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
radicle/sigrefs: Switch to new implementation
Fintan Halpenny committed 1 month ago
commit d40fa9a34745b4560f0eae1e9e234282c4b16d05
parent d3bc868
11 files changed +272 -241
modified crates/radicle-cli/examples/rad-id-multi-delegate.md
@@ -3,6 +3,16 @@ $ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Bob" --des
069e7d58faa9a7473d27f5510d676af33282796f
```

+
A note for test authors:
+
> The following `rad watch` command will time out if the target given via `-t` changes.
+
> This happens for example when the generation of sigrefs changes.
+
> To recover, temporarily change from `rad watch` to something like
+
>
+
>     $ sleep 5
+
>     $ rad inspect --sigrefs rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
>
+
> And pick out the result for `z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z`.
+

``` ~bob
$ rad watch --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --node z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z -r 'refs/rad/sigrefs' -t c9a828fc2fb01f893d6e6e9e17b9092dea2b3aba -i 500 --timeout 5000ms
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
modified crates/radicle-cli/examples/rad-id-threshold.md
@@ -150,7 +150,7 @@ Similarly, she still does not have Bob's `rad/sigrefs`:

``` ~alice
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ae3c6b77dc1ed51c1c1e6a2772339c2779fa9ba8
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi e0e55994a9a234f0b1cd36d8812e2948e2672b7a
```

And she can still list the project, without any worries:
@@ -198,6 +198,6 @@ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 2 potential s
🌱 Fetched from z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
🌱 Fetched from z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ae3c6b77dc1ed51c1c1e6a2772339c2779fa9ba8
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi e0e55994a9a234f0b1cd36d8812e2948e2672b7a
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk dace6fe948548168a2bb687718949d9b5d9076ee
```
modified crates/radicle-cli/examples/rad-sync.md
@@ -15,7 +15,7 @@ $ rad sync status --sort-by alias
╭───────────────────────────────────────────────────╮
│ Node ID           Alias   ?   SigRefs   Timestamp │
├───────────────────────────────────────────────────┤
-
│ (you)             alice   !   056b1db   [..]      │
+
│ (you)             alice   !   f2dfe80   [..]      │
│ z6Mkt67…v4N1tRk   bob     ✗   99c5497   [..]      │
│ z6Mkux1…nVhib7Z   eve     ✗   99c5497   [..]      │
╰───────────────────────────────────────────────────╯
@@ -37,9 +37,9 @@ $ rad sync status --sort-by alias
╭───────────────────────────────────────────────────╮
│ Node ID           Alias   ?   SigRefs   Timestamp │
├───────────────────────────────────────────────────┤
-
│ (you)             alice   ✓   056b1db   [..]      │
-
│ z6Mkt67…v4N1tRk   bob     ✓   056b1db   [..]      │
-
│ z6Mkux1…nVhib7Z   eve     ✓   056b1db   [..]      │
+
│ (you)             alice   ✓   f2dfe80   [..]      │
+
│ z6Mkt67…v4N1tRk   bob     ✓   f2dfe80   [..]      │
+
│ z6Mkux1…nVhib7Z   eve     ✓   f2dfe80   [..]      │
╰───────────────────────────────────────────────────╯
```

modified crates/radicle-fetch/src/sigrefs.rs
@@ -21,7 +21,7 @@ pub mod error {
        Load(#[from] Load),
    }

-
    pub type Load = radicle::storage::refs::Error;
+
    pub type Load = radicle::storage::refs::sigrefs::read::error::Read;
}

/// A data carrier that associates that data with whether a given
@@ -57,7 +57,10 @@ impl<T> DelegateStatus<T> {
    pub fn load<R, S>(
        self,
        cached: &Cached<R, S>,
-
    ) -> Result<DelegateStatus<Option<SignedRefsAt>>, radicle::storage::refs::Error>
+
    ) -> Result<
+
        DelegateStatus<Option<SignedRefsAt>>,
+
        radicle::storage::refs::sigrefs::read::error::Read,
+
    >
    where
        R: AsRef<Repository>,
    {
modified crates/radicle-fetch/src/state.rs
@@ -68,7 +68,7 @@ pub mod error {
        #[error(transparent)]
        Resolve(#[from] repository::error::Resolve),
        #[error(transparent)]
-
        Refs(#[from] radicle::storage::refs::Error),
+
        Refs(#[from] radicle::storage::refs::sigrefs::read::error::Read),
        #[error(transparent)]
        RemoteRefs(#[from] sigrefs::error::RemoteRefs),
        #[error("failed to get remote namespaces: {0}")]
@@ -683,7 +683,10 @@ where
        self.verified(oid).map(Some).map_err(error::Canonical::from)
    }

-
    pub fn load(&self, remote: &PublicKey) -> Result<Option<SignedRefsAt>, sigrefs::error::Load> {
+
    pub fn load(
+
        &self,
+
        remote: &PublicKey,
+
    ) -> Result<Option<SignedRefsAt>, radicle::storage::refs::sigrefs::read::error::Read> {
        match self.state.sigrefs.get(remote) {
            None => SignedRefsAt::load(*remote, self.handle.repository()),
            Some(tip) => SignedRefsAt::load_at(*tip, *remote, self.handle.repository()).map(Some),
modified crates/radicle/src/identity/doc/update.rs
@@ -8,7 +8,7 @@ use crate::{
    git,
    identity::crefs::GetCanonicalRefs as _,
    prelude::Did,
-
    storage::{refs, ReadRepository, RepositoryError},
+
    storage::{self, refs, ReadRepository, RepositoryError},
};

use super::{Doc, PayloadError, PayloadId, RawDoc, Visibility};
@@ -114,15 +114,12 @@ pub fn privacy_allow_list(
/// # Errors
///
/// This will fail if an operation using the repository fails.
-
pub fn delegates<S>(
+
pub fn delegates(
    mut raw: RawDoc,
    additions: Vec<Did>,
    removals: Vec<Did>,
-
    repo: &S,
-
) -> Result<Result<RawDoc, Vec<error::DelegateVerification>>, RepositoryError>
-
where
-
    S: ReadRepository,
-
{
+
    repo: &storage::git::Repository,
+
) -> Result<Result<RawDoc, Vec<error::DelegateVerification>>, RepositoryError> {
    if additions.is_empty() && removals.is_empty() {
        return Ok(Ok(raw));
    }
@@ -243,20 +240,19 @@ pub fn verify(raw: RawDoc) -> Result<Doc, error::DocVerification> {
    Ok(proposal)
}

-
fn verify_delegates<S>(
+
fn verify_delegates(
    proposal: &RawDoc,
-
    repo: &S,
-
) -> Result<Option<Vec<error::DelegateVerification>>, RepositoryError>
-
where
-
    S: ReadRepository,
-
{
+
    repo: &storage::git::Repository,
+
) -> Result<Option<Vec<error::DelegateVerification>>, RepositoryError> {
    let dids = &proposal.delegates;
    let threshold = proposal.threshold;
    let (canonical, _) = repo.canonical_head()?;
    let mut missing = Vec::with_capacity(dids.len());

    for did in dids {
-
        match refs::SignedRefsAt::load((*did).into(), repo)? {
+
        match refs::SignedRefsAt::load((*did).into(), repo)
+
            .map_err(|err| storage::Error::Refs(storage::refs::Error::Read(err)))?
+
        {
            None => {
                missing.push(error::DelegateVerification::MissingDelegate { did: *did });
            }
modified crates/radicle/src/storage.rs
@@ -410,7 +410,7 @@ impl<V> Deref for Remote<V> {

/// Read-only operations on a storage instance.
pub trait ReadStorage {
-
    type Repository: ReadRepository;
+
    type Repository: ReadRepository + self::refs::sigrefs::git::reference::Reader;

    /// Get user info for this storage.
    fn info(&self) -> &UserInfo;
modified crates/radicle/src/storage/git.rs
@@ -36,7 +36,7 @@ use crate::git::RefError;
use crate::git::UserInfo;
pub use crate::storage::{Error, RepositoryError};

-
use super::refs::RefsAt;
+
use super::refs::{sigrefs, RefsAt};
use super::{RemoteId, RemoteRepository, ValidateRepository};

pub static NAMESPACES_GLOB: LazyLock<PatternString> =
@@ -169,7 +169,8 @@ impl ReadStorage for Storage {
                }
            };
            // Nb. This will be `None` if they were not found.
-
            let refs = refs::SignedRefsAt::load(self.info.key, &repo)?;
+
            let refs = refs::SignedRefsAt::load(self.info.key, &repo)
+
                .map_err(|err| Error::Refs(refs::Error::Read(err)))?;
            let synced_at = refs
                .as_ref()
                .map(|r| node::SyncedAt::new(r.at, &repo))
@@ -202,7 +203,9 @@ impl WriteStorage for Storage {
        let repo = self.repository(rid)?;
        // N.b. we remove the repository if the `local` peer has no
        // `rad/sigrefs`. There's no risk of them corrupting data.
-
        let has_sigrefs = SignedRefsAt::load(self.info.key, &repo)?.is_some();
+
        let has_sigrefs = SignedRefsAt::load(self.info.key, &repo)
+
            .map_err(|err| RepositoryError::Storage(Error::Refs(refs::Error::Read(err))))?
+
            .is_some();
        if has_sigrefs {
            repo.clean(&self.info.key)
        } else {
@@ -256,7 +259,8 @@ impl Storage {
        rids.map(|rid| {
            let repo = self.repository(*rid)?;
            let (_, head) = repo.head()?;
-
            let refs = refs::SignedRefsAt::load(self.info.key, &repo)?;
+
            let refs = refs::SignedRefsAt::load(self.info.key, &repo)
+
                .map_err(|err| RepositoryError::Refs(refs::Error::Read(err)))?;
            let synced_at = refs
                .as_ref()
                .map(|r| SyncedAt::new(r.at, &repo))
@@ -989,15 +993,72 @@ impl SignRepository for Repository {
    ) -> Result<SignedRefs<Verified>, RepositoryError> {
        let remote = signer.public_key();
        // Ensure the root reference is set, which is checked during sigref verification.
-
        if self.identity_root_of(remote).is_err() {
+
        if self
+
            .reference_oid(remote, &git::refs::storage::IDENTITY_ROOT)
+
            .is_err()
+
        {
            self.set_remote_identity_root(remote)?;
        }
-
        let mut refs = self.references_of(remote)?;
-
        refs.remove_sigrefs();
-
        let signed = refs.signed(signer)?.verified(self)?;
-
        signed.save(self)?;

-
        Ok(signed)
+
        let committer = refs::sigrefs::git::committer(remote, &self.backend.signature()?)?;
+
        let signed = self
+
            .references_of(remote)?
+
            .save(*remote, committer, self, signer)?;
+

+
        Ok(signed.sigrefs)
+
    }
+
}
+

+
impl sigrefs::git::object::Reader for Repository {
+
    fn read_commit(
+
        &self,
+
        oid: &Oid,
+
    ) -> Result<Option<Vec<u8>>, sigrefs::git::object::error::ReadCommit> {
+
        self.backend.read_commit(oid)
+
    }
+

+
    fn read_blob(
+
        &self,
+
        commit: &Oid,
+
        path: &Path,
+
    ) -> Result<Option<sigrefs::git::object::Blob>, sigrefs::git::object::error::ReadBlob> {
+
        self.backend.read_blob(commit, path)
+
    }
+
}
+

+
impl sigrefs::git::object::Writer for Repository {
+
    fn write_tree(
+
        &self,
+
        refs: sigrefs::git::object::RefsEntry,
+
        signature: sigrefs::git::object::SignatureEntry,
+
    ) -> Result<Oid, sigrefs::git::object::error::WriteTree> {
+
        self.backend.write_tree(refs, signature)
+
    }
+

+
    fn write_commit(&self, bytes: &[u8]) -> Result<Oid, sigrefs::git::object::error::WriteCommit> {
+
        self.backend.write_commit(bytes)
+
    }
+
}
+

+
impl sigrefs::git::reference::Reader for Repository {
+
    fn find_reference(
+
        &self,
+
        reference: &git::fmt::Namespaced,
+
    ) -> Result<Option<Oid>, sigrefs::git::reference::error::FindReference> {
+
        sigrefs::git::reference::Reader::find_reference(&self.backend, reference)
+
    }
+
}
+

+
impl sigrefs::git::reference::Writer for Repository {
+
    fn write_reference(
+
        &self,
+
        reference: &git::fmt::Namespaced,
+
        commit: Oid,
+
        parent: Option<Oid>,
+
        reflog: String,
+
    ) -> Result<(), sigrefs::git::reference::error::WriteReference> {
+
        self.backend
+
            .write_reference(reference, commit, parent, reflog)
    }
}

modified crates/radicle/src/storage/refs.rs
@@ -9,11 +9,10 @@ use std::io;
use std::io::{BufRead, BufReader};
use std::marker::PhantomData;
use std::ops::Deref;
-
use std::path::Path;
use std::str::FromStr;

-
use crypto::signature::Signer;
-
use crypto::{PublicKey, Signature, Unverified, Verified};
+
use crypto::signature;
+
use crypto::{PublicKey, Signature, Verified};
use radicle_core::NodeId;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -21,47 +20,30 @@ use thiserror::Error;
use crate::git;
use crate::git::raw::ErrorExt as _;
use crate::git::Oid;
-
use crate::node::device::Device;
-
use crate::profile::env;
use crate::storage;
-
use crate::storage::{ReadRepository, RemoteId, RepoId, WriteRepository};
+
use crate::storage::{ReadRepository, RemoteId};

pub use crate::git::refs::storage::*;

+
use super::HasRepoId;
+

/// File in which the signed references are stored, in the `refs/rad/sigrefs` branch.
pub const REFS_BLOB_PATH: &str = "refs";
/// File in which the signature over the references is stored in the `refs/rad/sigrefs` branch.
pub const SIGNATURE_BLOB_PATH: &str = "signature";

-
#[derive(Debug)]
-
pub enum Updated {
-
    /// The computed [`Refs`] were stored as a new commit.
-
    Updated { oid: Oid },
-
    /// The stored [`Refs`] were the same as the computed ones, so no new commit
-
    /// was created.
-
    Unchanged { oid: Oid },
-
}
-

#[derive(Debug, Error)]
pub enum Error {
-
    #[error("invalid signature: {0}")]
-
    InvalidSignature(#[from] crypto::Error),
-
    #[error("signer error: {0}")]
-
    Signer(#[from] crypto::signature::Error),
-
    #[error("canonical refs: {0}")]
-
    Canonical(#[from] canonical::Error),
    #[error("invalid reference")]
    InvalidRef,
-
    #[error("missing identity root reference '{0}'")]
-
    MissingIdentityRoot(git::fmt::RefString),
-
    #[error("missing identity object '{0}'")]
-
    MissingIdentity(Oid),
-
    #[error("mismatched identity: local {local}, remote {remote}")]
-
    MismatchedIdentity { local: RepoId, remote: RepoId },
    #[error("invalid reference: {0}")]
    Ref(#[from] git::RefError),
    #[error(transparent)]
    Git(#[from] git::raw::Error),
+
    #[error(transparent)]
+
    Read(#[from] sigrefs::read::error::Read),
+
    #[error(transparent)]
+
    Write(#[from] sigrefs::write::error::Write),
}

impl Error {
@@ -85,26 +67,46 @@ impl Refs {
        Self(BTreeMap::new())
    }

-
    /// Verify the given signature on these refs, and return [`SignedRefs`] on success.
-
    pub fn verified<R: ReadRepository>(
+
    /// Save the signed refs to disk.
+
    /// This creates a new commit on the signed refs branch, and updates the branch pointer.
+
    pub fn save<R, S>(
        self,
-
        signer: PublicKey,
-
        signature: Signature,
+
        namespace: NodeId,
+
        committer: sigrefs::git::Committer,
        repo: &R,
-
    ) -> Result<SignedRefs<Verified>, Error> {
-
        SignedRefs::new(self, signer, signature).verified(repo)
-
    }
-

-
    /// Sign these refs with the given signer and return [`SignedRefs`].
-
    pub fn signed<G>(self, device: &Device<G>) -> Result<SignedRefs<Unverified>, Error>
+
        signer: &S,
+
    ) -> Result<SignedRefsAt, Error>
    where
-
        G: Signer<crypto::Signature>,
+
        S: signature::Signer<crypto::Signature>,
+
        R: sigrefs::git::object::Reader + sigrefs::git::object::Writer,
+
        R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer,
    {
-
        let refs = self;
-
        let msg = refs.canonical();
-
        let signature = device.try_sign(&msg)?;
-

-
        Ok(SignedRefs::new(refs, *device.public_key(), signature))
+
        let msg = "Update signed refs\n";
+
        let reflog = format!("Save {} signed references", self.len());
+
        let update = sigrefs::write::SignedRefsWriter::new(self, namespace, repo, signer).write(
+
            committer,
+
            msg.to_string(),
+
            reflog,
+
        )?;
+
        match update {
+
            sigrefs::write::Update::Changed { entry } => Ok(entry.into_sigrefs_at(namespace)),
+
            sigrefs::write::Update::Unchanged {
+
                commit,
+
                refs,
+
                signature,
+
            } => {
+
                let sigrefs = SignedRefs {
+
                    refs,
+
                    signature,
+
                    id: namespace,
+
                    _verified: PhantomData,
+
                };
+
                Ok(SignedRefsAt {
+
                    sigrefs,
+
                    at: commit,
+
                })
+
            }
+
        }
    }

    /// Get a particular ref.
@@ -248,8 +250,7 @@ where
/// signature over the refs. This allows us to easily verify if a set of refs
/// came from a particular key.
///
-
/// The type parameter keeps track of whether the signature was [`Verified`] or
-
/// [`Unverified`].
+
/// The type parameter keeps track of whether the signature was [`Verified`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SignedRefs<V> {
    /// The signed refs.
@@ -264,61 +265,6 @@ pub struct SignedRefs<V> {
    _verified: PhantomData<V>,
}

-
impl SignedRefs<Unverified> {
-
    pub fn new(refs: Refs, author: PublicKey, signature: Signature) -> Self {
-
        Self {
-
            refs,
-
            signature,
-
            id: author,
-
            _verified: PhantomData,
-
        }
-
    }
-

-
    pub fn verified<R: ReadRepository>(self, repo: &R) -> Result<SignedRefs<Verified>, Error> {
-
        match self.verify(repo) {
-
            Ok(()) => Ok(SignedRefs {
-
                refs: self.refs,
-
                signature: self.signature,
-
                id: self.id,
-
                _verified: PhantomData,
-
            }),
-
            Err(e) => Err(e),
-
        }
-
    }
-

-
    pub fn verify<R: ReadRepository>(&self, repo: &R) -> Result<(), Error> {
-
        let canonical = self.refs.canonical();
-
        let local = repo.id();
-

-
        // Verify signature.
-
        if let Err(e) = self.id.verify(canonical, &self.signature) {
-
            return Err(e.into());
-
        }
-
        // If the identity root was signed, verify it points to the right place.
-
        if let Some(id_root) = self.refs.get(&IDENTITY_ROOT) {
-
            // Get the identity at the given oid.
-
            let Ok(doc) = repo.identity_doc_at(id_root) else {
-
                return Err(Error::MissingIdentity(id_root));
-
            };
-
            let remote = RepoId::from(doc.blob);
-

-
            // Make sure the signed identity points to the local repo identity.
-
            if remote != local {
-
                return Err(Error::MismatchedIdentity { local, remote });
-
            }
-
        } else {
-
            // TODO(cloudhead): Make this into a hard error (`Error::MissingIdentityRoot`) for
-
            // repos that have migrated to the new identity document schema.
-
            log::debug!(
-
                target: "storage",
-
                "Signed ref verification for {} in {local}: {} is not provided",
-
                self.id, *IDENTITY_ROOT
-
            );
-
        }
-
        Ok(())
-
    }
-
}
-

impl SignedRefs<Verified> {
    /// Returns the [`NodeId`] of the [`SignedRefs`].
    pub fn id(&self) -> NodeId {
@@ -330,99 +276,44 @@ impl SignedRefs<Verified> {
        &self.refs
    }

-
    pub fn load<S>(remote: RemoteId, repo: &S) -> Result<Self, Error>
+
    pub fn load<R>(remote: RemoteId, repo: &R) -> Result<Self, sigrefs::read::error::Read>
    where
-
        S: ReadRepository,
+
        R: HasRepoId,
+
        R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
    {
-
        let oid = repo.reference_oid(&remote, &SIGREFS_BRANCH)?;
-

-
        SignedRefs::load_at(oid, remote, repo)
+
        let root = repo.rid();
+
        let tip = sigrefs::read::Tip::Reference(remote);
+
        let latest = sigrefs::SignedRefsReader::new(root, tip, repo, &remote).read()?;
+
        let signature = *latest.signature();
+
        let refs = latest.into_refs();
+
        Ok(SignedRefs {
+
            refs,
+
            signature,
+
            id: remote,
+
            _verified: PhantomData,
+
        })
    }

-
    pub fn load_at<S>(oid: Oid, remote: RemoteId, repo: &S) -> Result<Self, Error>
+
    pub fn load_at<R>(
+
        oid: Oid,
+
        remote: RemoteId,
+
        repo: &R,
+
    ) -> Result<Self, sigrefs::read::error::Read>
    where
-
        S: storage::ReadRepository,
+
        R: HasRepoId,
+
        R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
    {
-
        let refs = repo.blob_at(oid, Path::new(REFS_BLOB_PATH))?;
-
        let signature = repo.blob_at(oid, Path::new(SIGNATURE_BLOB_PATH))?;
-
        let signature: crypto::Signature = signature.content().try_into()?;
-
        let refs = Refs::from_canonical(refs.content())?;
-

-
        SignedRefs::new(refs, remote, signature).verified(repo)
-
    }
-

-
    /// Save the signed refs to disk.
-
    /// This creates a new commit on the signed refs branch, and updates the branch pointer.
-
    pub fn save<S: WriteRepository>(&self, repo: &S) -> Result<Updated, Error> {
-
        let sigref = &SIGREFS_BRANCH;
-
        let remote = &self.id;
-
        let raw = repo.raw();
-

-
        // N.b. if the signatures match then there are no updates
-
        let parent = match SignedRefsAt::load(*remote, repo)? {
-
            Some(SignedRefsAt { sigrefs, at }) if sigrefs.signature == self.signature => {
-
                return Ok(Updated::Unchanged { oid: at });
-
            }
-
            Some(SignedRefsAt { at, .. }) => Some(raw.find_commit(at.into())?),
-
            None => None,
-
        };
-

-
        let tree = {
-
            let refs_blob_oid = raw.blob(&self.canonical())?;
-
            let sig_blob_oid = raw.blob(self.signature.as_ref())?;
-

-
            let mut builder = raw.treebuilder(None)?;
-
            builder.insert(REFS_BLOB_PATH, refs_blob_oid, 0o100_644)?;
-
            builder.insert(SIGNATURE_BLOB_PATH, sig_blob_oid, 0o100_644)?;
-

-
            let oid = builder.write()?;
-

-
            raw.find_tree(oid)
-
        }?;
-

-
        let sigref = sigref.with_namespace(remote.into());
-
        let author = if let Ok(s) = env::var(env::GIT_COMMITTER_DATE) {
-
            let Ok(timestamp) = s.trim().parse::<i64>() else {
-
                panic!(
-
                    "Invalid timestamp value {s:?} for `{}`",
-
                    env::GIT_COMMITTER_DATE
-
                );
-
            };
-
            let time = git::raw::Time::new(timestamp, 0);
-
            git::raw::Signature::new("radicle", remote.to_string().as_str(), &time)?
-
        } else {
-
            raw.signature()?
-
        };
-

-
        let commit = raw.commit(
-
            Some(&sigref),
-
            &author,
-
            &author,
-
            "Update signed refs\n",
-
            &tree,
-
            &parent.iter().collect::<Vec<&git::raw::Commit>>(),
-
        );
-

-
        match commit {
-
            Ok(oid) => Ok(Updated::Updated { oid: oid.into() }),
-
            Err(e) => match (e.class(), e.code()) {
-
                (git::raw::ErrorClass::Object, git::raw::ErrorCode::Modified) => {
-
                    log::warn!("Concurrent modification of refs: {e:?}");
-

-
                    Err(Error::Git(e))
-
                }
-
                _ => Err(e.into()),
-
            },
-
        }
-
    }
-

-
    pub fn unverified(self) -> SignedRefs<Unverified> {
-
        SignedRefs {
-
            refs: self.refs,
-
            signature: self.signature,
-
            id: self.id,
+
        let root = repo.rid();
+
        let tip = sigrefs::read::Tip::Commit(oid);
+
        let latest = sigrefs::SignedRefsReader::new(root, tip, repo, &remote).read()?;
+
        let signature = *latest.signature();
+
        let refs = latest.into_refs();
+
        Ok(SignedRefs {
+
            refs,
+
            signature,
+
            id: remote,
            _verified: PhantomData,
-
        }
+
        })
    }
}

@@ -452,15 +343,24 @@ pub struct RefsAt {
}

impl RefsAt {
-
    pub fn new<S: ReadRepository>(
-
        repo: &S,
-
        remote: RemoteId,
-
    ) -> Result<Self, crate::git::raw::Error> {
-
        let at = repo.reference_oid(&remote, &storage::refs::SIGREFS_BRANCH)?;
+
    pub fn new<R>(repo: &R, remote: RemoteId) -> Result<Self, sigrefs::read::error::Read>
+
    where
+
        R: sigrefs::git::reference::Reader,
+
    {
+
        let at = repo
+
            .find_reference(
+
                &storage::refs::SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(&remote)),
+
            )
+
            .map_err(sigrefs::read::error::Read::FindReference)?
+
            .ok_or_else(|| sigrefs::read::error::Read::MissingSigrefs { namespace: remote })?;
        Ok(RefsAt { remote, at })
    }

-
    pub fn load<S: ReadRepository>(&self, repo: &S) -> Result<SignedRefsAt, Error> {
+
    pub fn load<R>(&self, repo: &R) -> Result<SignedRefsAt, sigrefs::read::error::Read>
+
    where
+
        R: HasRepoId,
+
        R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
+
    {
        SignedRefsAt::load_at(self.at, self.remote, repo)
    }

@@ -488,21 +388,28 @@ impl SignedRefsAt {
    ///
    /// This will return `None` if the branch was not found, all other
    /// errors are returned.
-
    pub fn load<S>(remote: RemoteId, repo: &S) -> Result<Option<Self>, Error>
+
    pub fn load<R>(remote: RemoteId, repo: &R) -> Result<Option<Self>, sigrefs::read::error::Read>
    where
-
        S: ReadRepository,
+
        R: HasRepoId,
+
        R: ReadRepository,
+
        R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
    {
        let at = match RefsAt::new(repo, remote) {
            Ok(RefsAt { at, .. }) => at,
-
            Err(e) if e.is_not_found() => return Ok(None),
-
            Err(e) => return Err(e.into()),
+
            Err(sigrefs::read::error::Read::MissingSigrefs { .. }) => return Ok(None),
+
            Err(e) => return Err(e),
        };
        Self::load_at(at, remote, repo).map(Some)
    }

-
    pub fn load_at<S>(at: Oid, remote: RemoteId, repo: &S) -> Result<Self, Error>
+
    pub fn load_at<R>(
+
        at: Oid,
+
        remote: RemoteId,
+
        repo: &R,
+
    ) -> Result<Self, sigrefs::read::error::Read>
    where
-
        S: storage::ReadRepository,
+
        R: HasRepoId,
+
        R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader,
    {
        Ok(Self {
            sigrefs: SignedRefs::load_at(at, remote, repo)?,
@@ -547,6 +454,8 @@ mod tests {

    use super::*;
    use crate::assert_matches;
+
    use crate::node::device::Device;
+
    use crate::storage::WriteRepository as _;
    use crate::{cob::identity::Identity, cob::Title, rad, test::fixtures, Storage};

    #[quickcheck]
@@ -709,8 +618,14 @@ mod tests {
        // only modifies his own namespace. Note that anyone (eg. Eve) could create a reference
        // under her copy of Bob's namespace, and this would only be rejected during signed ref
        // validation.
-
        let result = bob_paris_sigrefs.save(&london).unwrap();
-
        assert_matches!(result, Updated::Updated { .. });
+
        {
+
            let name = &SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(bob.node_id()));
+
            let id = paris.backend.refname_to_id(name.as_str()).unwrap();
+
            london
+
                .backend
+
                .reference(name.as_str(), id, true, "Graft attack")
+
                .unwrap();
+
        }

        london
            .raw()
@@ -727,11 +642,13 @@ mod tests {
        // The graft is not allowed.
        assert_matches!(
            london.remote(bob.public_key()),
-
            Err(Error::MismatchedIdentity {
-
                local,
-
                remote,
-
            })
-
            if local == london_rid && remote == paris_rid
+
            Err(Error::Read(sigrefs::read::error::Read::Verify(sigrefs::read::error::Verify::MismatchedIdentity {
+
                expected,
+
                found,
+
                sigrefs_commit: _,
+
                identity_commit: _,
+
            })))
+
            if expected == london_rid && found == paris_rid
        );
    }
}
modified crates/radicle/src/storage/refs/sigrefs/write.rs
@@ -3,9 +3,11 @@ pub mod error;
#[cfg(test)]
mod test;

+
use std::marker::PhantomData;
use std::path::Path;

use crypto::signature::Signer;
+
use crypto::PublicKey;
use radicle_core::NodeId;
use radicle_git_metadata::author::Author;
use radicle_git_metadata::commit::{headers::Headers, trailers::OwnedTrailer, CommitData};
@@ -16,6 +18,7 @@ use crate::storage::refs::sigrefs::git::{object, reference, Committer};
use crate::storage::refs::{
    Refs, IDENTITY_ROOT, REFS_BLOB_PATH, SIGNATURE_BLOB_PATH, SIGREFS_BRANCH, SIGREFS_PARENT,
};
+
use crate::storage::refs::{SignedRefs, SignedRefsAt};

/// The result of calling [`SignedRefsWriter::write`].
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -160,11 +163,23 @@ pub struct Commit {
    signature: crypto::Signature,
}

-
#[cfg(test)]
impl Commit {
+
    #[cfg(test)]
    pub(super) fn into_refs(self) -> Refs {
        self.refs
    }
+

+
    pub(crate) fn into_sigrefs_at(self, id: PublicKey) -> SignedRefsAt {
+
        SignedRefsAt {
+
            at: self.oid,
+
            sigrefs: SignedRefs {
+
                id,
+
                signature: self.signature,
+
                refs: self.refs,
+
                _verified: PhantomData,
+
            },
+
        }
+
    }
}

struct CommitWriter<'a, R, S> {
modified crates/radicle/src/test/storage.rs
@@ -4,6 +4,8 @@ use std::io;
use std::path::{Path, PathBuf};
use std::str::FromStr;

+
use crypto::PublicKey;
+

pub use crate::git;
use crate::git::fmt;

@@ -152,6 +154,30 @@ impl MockRepository {
    }
}

+
impl self::refs::sigrefs::git::reference::Reader for MockRepository {
+
    fn find_reference(
+
        &self,
+
        reference: &git::fmt::Namespaced,
+
    ) -> Result<Option<Oid>, refs::sigrefs::git::reference::error::FindReference> {
+
        use refs::sigrefs::git::reference::error::FindReference;
+
        let ns = reference.namespace();
+

+
        let remote: PublicKey = ns.as_str().parse().map_err(FindReference::other)?;
+
        let reference = reference.strip_namespace();
+

+
        match self.remotes.get(&remote) {
+
            None => Ok(None),
+
            Some(refs) => {
+
                if reference == *refs::SIGREFS_BRANCH {
+
                    Ok(Some(refs.at))
+
                } else {
+
                    Ok(refs.sigrefs.get(&reference))
+
                }
+
            }
+
        }
+
    }
+
}
+

impl RemoteRepository for MockRepository {
    fn remote(&self, id: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
        self.remotes