about summary refs log tree commit diff
path: root/tvix/store/src
diff options
context:
space:
mode:
Diffstat (limited to 'tvix/store/src')
-rw-r--r--tvix/store/src/bin/tvix-store.rs31
-rw-r--r--tvix/store/src/import.rs45
-rw-r--r--tvix/store/src/lib.rs1
-rw-r--r--tvix/store/src/path_info.rs87
-rw-r--r--tvix/store/src/pathinfoservice/bigtable.rs30
-rw-r--r--tvix/store/src/pathinfoservice/combinators.rs23
-rw-r--r--tvix/store/src/pathinfoservice/fs/mod.rs32
-rw-r--r--tvix/store/src/pathinfoservice/grpc.rs40
-rw-r--r--tvix/store/src/pathinfoservice/lru.rs52
-rw-r--r--tvix/store/src/pathinfoservice/memory.rs22
-rw-r--r--tvix/store/src/pathinfoservice/mod.rs2
-rw-r--r--tvix/store/src/pathinfoservice/nix_http.rs34
-rw-r--r--tvix/store/src/pathinfoservice/redb.rs45
-rw-r--r--tvix/store/src/pathinfoservice/signing_wrapper.rs60
-rw-r--r--tvix/store/src/pathinfoservice/tests/mod.rs41
-rw-r--r--tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs12
-rw-r--r--tvix/store/src/proto/mod.rs364
-rw-r--r--tvix/store/src/proto/tests/pathinfo.rs489
-rw-r--r--tvix/store/src/tests/fixtures.rs58
19 files changed, 594 insertions, 874 deletions
diff --git a/tvix/store/src/bin/tvix-store.rs b/tvix/store/src/bin/tvix-store.rs
index b77a46dace7c..f39b73df2e0b 100644
--- a/tvix/store/src/bin/tvix-store.rs
+++ b/tvix/store/src/bin/tvix-store.rs
@@ -4,7 +4,7 @@ use clap::Subcommand;
 use futures::future::try_join_all;
 use futures::StreamExt;
 use futures::TryStreamExt;
-use nix_compat::path_info::ExportedPathInfo;
+use nix_compat::{path_info::ExportedPathInfo, store_path::StorePath};
 use serde::Deserialize;
 use serde::Serialize;
 use std::path::PathBuf;
@@ -16,15 +16,13 @@ use tracing::{info, info_span, instrument, Level, Span};
 use tracing_indicatif::span_ext::IndicatifSpanExt as _;
 use tvix_castore::import::fs::ingest_path;
 use tvix_store::nar::NarCalculationService;
-use tvix_store::proto::NarInfo;
-use tvix_store::proto::PathInfo;
 use tvix_store::utils::{ServiceUrls, ServiceUrlsGrpc};
 
 use tvix_castore::proto::blob_service_server::BlobServiceServer;
 use tvix_castore::proto::directory_service_server::DirectoryServiceServer;
 use tvix_castore::proto::GRPCBlobServiceWrapper;
 use tvix_castore::proto::GRPCDirectoryServiceWrapper;
-use tvix_store::pathinfoservice::PathInfoService;
+use tvix_store::pathinfoservice::{PathInfo, PathInfoService};
 use tvix_store::proto::path_info_service_server::PathInfoServiceServer;
 use tvix_store::proto::GRPCPathInfoServiceWrapper;
 
@@ -359,23 +357,14 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error + Send + Sync
                 // Create and upload a PathInfo pointing to the root_node,
                 // annotated with information we have from the reference graph.
                 let path_info = PathInfo {
-                    node: Some(tvix_castore::proto::Node::from_name_and_node(
-                        elem.path.to_string().into(),
-                        root_node,
-                    )),
-                    references: Vec::from_iter(
-                        elem.references.iter().map(|e| e.digest().to_vec().into()),
-                    ),
-                    narinfo: Some(NarInfo {
-                        nar_size: elem.nar_size,
-                        nar_sha256: elem.nar_sha256.to_vec().into(),
-                        signatures: vec![],
-                        reference_names: Vec::from_iter(
-                            elem.references.iter().map(|e| e.to_string()),
-                        ),
-                        deriver: None,
-                        ca: None,
-                    }),
+                    store_path: elem.path.to_owned(),
+                    node: root_node,
+                    references: elem.references.iter().map(StorePath::to_owned).collect(),
+                    nar_size: elem.nar_size,
+                    nar_sha256: elem.nar_sha256,
+                    signatures: vec![],
+                    deriver: None,
+                    ca: None,
                 };
 
                 path_info_service.put(path_info).await?;
diff --git a/tvix/store/src/import.rs b/tvix/store/src/import.rs
index 2c2b205016a1..1e9fd060b492 100644
--- a/tvix/store/src/import.rs
+++ b/tvix/store/src/import.rs
@@ -3,18 +3,17 @@ use std::path::Path;
 use tracing::{debug, instrument};
 use tvix_castore::{
     blobservice::BlobService, directoryservice::DirectoryService, import::fs::ingest_path, Node,
-    PathComponent,
 };
 
 use nix_compat::{
     nixhash::{CAHash, NixHash},
-    store_path::{self, StorePathRef},
+    store_path::{self, StorePath, StorePathRef},
 };
 
 use crate::{
     nar::NarCalculationService,
-    pathinfoservice::PathInfoService,
-    proto::{nar_info, NarInfo, PathInfo},
+    pathinfoservice::{PathInfo, PathInfoService},
+    proto::nar_info,
 };
 
 impl From<CAHash> for nar_info::Ca {
@@ -74,33 +73,29 @@ pub fn path_to_name(path: &Path) -> std::io::Result<&str> {
 /// Takes the NAR size, SHA-256 of the NAR representation, the root node and optionally
 /// a CA hash information.
 ///
-/// Returns the path information object for a NAR-style object.
+/// Constructs a [PathInfo] for a NAR-style object.
 ///
-/// This [`PathInfo`] can be further filled for signatures, deriver or verified for the expected
-/// hashes.
+/// The user can then further fill the fields (like deriver, signatures), and/or
+/// verify to have the expected hashes.
 #[inline]
 pub fn derive_nar_ca_path_info(
     nar_size: u64,
     nar_sha256: [u8; 32],
-    ca: Option<&CAHash>,
-    name: bytes::Bytes,
+    ca: Option<CAHash>,
+    store_path: StorePath<String>,
     root_node: Node,
 ) -> PathInfo {
     // assemble the [crate::proto::PathInfo] object.
     PathInfo {
-        node: Some(tvix_castore::proto::Node::from_name_and_node(
-            name, root_node,
-        )),
+        store_path,
+        node: root_node,
         // There's no reference scanning on path contents ingested like this.
         references: vec![],
-        narinfo: Some(NarInfo {
-            nar_size,
-            nar_sha256: nar_sha256.to_vec().into(),
-            signatures: vec![],
-            reference_names: vec![],
-            deriver: None,
-            ca: ca.map(|ca_hash| ca_hash.into()),
-        }),
+        nar_size,
+        nar_sha256,
+        signatures: vec![],
+        deriver: None,
+        ca,
     }
 }
 
@@ -141,19 +136,13 @@ where
         )
     })?;
 
-    let name: PathComponent = output_path
-        .to_string()
-        .as_str()
-        .try_into()
-        .expect("Tvix bug: StorePath must be PathComponent");
-
     log_node(name.as_ref(), &root_node, path.as_ref());
 
     let path_info = derive_nar_ca_path_info(
         nar_size,
         nar_sha256,
-        Some(&CAHash::Nar(NixHash::Sha256(nar_sha256))),
-        name.into(),
+        Some(CAHash::Nar(NixHash::Sha256(nar_sha256))),
+        output_path.to_owned(),
         root_node,
     );
 
diff --git a/tvix/store/src/lib.rs b/tvix/store/src/lib.rs
index 81a77cd978a2..e1517609d51c 100644
--- a/tvix/store/src/lib.rs
+++ b/tvix/store/src/lib.rs
@@ -1,6 +1,7 @@
 pub mod composition;
 pub mod import;
 pub mod nar;
+pub mod path_info;
 pub mod pathinfoservice;
 pub mod proto;
 pub mod utils;
diff --git a/tvix/store/src/path_info.rs b/tvix/store/src/path_info.rs
new file mode 100644
index 000000000000..487261b4bdcb
--- /dev/null
+++ b/tvix/store/src/path_info.rs
@@ -0,0 +1,87 @@
+use nix_compat::{
+    narinfo::{Flags, Signature},
+    nixhash::CAHash,
+    store_path::StorePath,
+};
+
+/// Holds metadata about a store path, but not its contents.
+///
+/// This is somewhat equivalent to the information Nix holds in its SQLite
+/// database, or publishes as .narinfo files, except we also embed the
+/// [tvix_castore::Node] describing the contents in the castore model.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PathInfo {
+    /// The store path this is about.
+    pub store_path: StorePath<String>,
+    /// The contents in the tvix-castore model.
+    //// Can be a directory, file or symlink.
+    pub node: tvix_castore::Node,
+    /// A list of references.
+    pub references: Vec<StorePath<String>>,
+    /// The size of the NAR representation of the contents, in bytes.
+    pub nar_size: u64,
+    /// The sha256 digest of the NAR representation of the contents.
+    pub nar_sha256: [u8; 32],
+    /// The signatures, usually shown in a .narinfo file.
+    pub signatures: Vec<Signature<String>>,
+    /// The StorePath of the .drv file producing this output.
+    /// The .drv suffix is omitted in its `name` field.
+    pub deriver: Option<StorePath<String>>,
+    /// The CA field in the .narinfo.
+    /// Its textual representations seen in the wild are one of the following:
+    ///
+    /// * `fixed:r:sha256:1gcky5hlf5vqfzpyhihydmm54grhc94mcs8w7xr8613qsqb1v2j6`
+    ///   fixed-output derivations using "recursive" `outputHashMode`.
+    /// * `fixed:sha256:19xqkh72crbcba7flwxyi3n293vav6d7qkzkh2v4zfyi4iia8vj8 fixed-output derivations using "flat" `outputHashMode\`
+    /// * `text:sha256:19xqkh72crbcba7flwxyi3n293vav6d7qkzkh2v4zfyi4iia8vj8`
+    ///   Text hashing, used for uploaded .drv files and outputs produced by
+    ///   builtins.toFile.
+    ///
+    /// Semantically, they can be split into the following components:
+    ///
+    /// * "content address prefix". Currently, "fixed" and "text" are supported.
+    /// * "hash mode". Currently, "flat" and "recursive" are supported.
+    /// * "hash type". The underlying hash function used.
+    ///   Currently, sha1, md5, sha256, sha512.
+    /// * "digest". The digest itself.
+    ///
+    /// There are some restrictions on the possible combinations.
+    /// For example, `text` and `fixed:recursive` always imply sha256.
+    pub ca: Option<CAHash>,
+}
+
+impl PathInfo {
+    /// Reconstructs a [nix_compat::narinfo::NarInfo<'_>].
+    ///
+    /// It does very little allocation (a Vec each for `signatures` and
+    /// `references`), the rest points to data owned elsewhere.
+    ///
+    /// It can be used to validate Signatures, or render a .narinfo file
+    /// (after some more fields are populated)
+    ///
+    /// Keep in mind this is not able to reconstruct all data present in the
+    /// NarInfo<'_>, as some of it is not stored at all:
+    /// - the `system`, `file_hash` and `file_size` fields are set to `None`.
+    /// - the URL is set to an empty string.
+    /// - Compression is set to "none"
+    ///
+    /// If you want to render it out to a string and be able to parse it back
+    /// in, at least URL *must* be set again.
+    pub fn to_narinfo(&self) -> nix_compat::narinfo::NarInfo<'_> {
+        nix_compat::narinfo::NarInfo {
+            flags: Flags::empty(),
+            store_path: self.store_path.as_ref(),
+            nar_hash: self.nar_sha256,
+            nar_size: self.nar_size,
+            references: self.references.iter().map(StorePath::as_ref).collect(),
+            signatures: self.signatures.iter().map(Signature::as_ref).collect(),
+            ca: self.ca.clone(),
+            system: None,
+            deriver: self.deriver.as_ref().map(StorePath::as_ref),
+            url: "",
+            compression: Some("none"),
+            file_hash: None,
+            file_size: None,
+        }
+    }
+}
diff --git a/tvix/store/src/pathinfoservice/bigtable.rs b/tvix/store/src/pathinfoservice/bigtable.rs
index 15128986ff56..3d8db8e5044a 100644
--- a/tvix/store/src/pathinfoservice/bigtable.rs
+++ b/tvix/store/src/pathinfoservice/bigtable.rs
@@ -1,6 +1,5 @@
-use super::PathInfoService;
+use super::{PathInfo, PathInfoService};
 use crate::proto;
-use crate::proto::PathInfo;
 use async_stream::try_stream;
 use bigtable_rs::{bigtable, google::bigtable::v2 as bigtable_v2};
 use bytes::Bytes;
@@ -232,14 +231,13 @@ impl PathInfoService for BigtablePathInfoService {
         }
 
         // Try to parse the value into a PathInfo message
-        let path_info = proto::PathInfo::decode(Bytes::from(cell.value))
+        let path_info_proto = proto::PathInfo::decode(Bytes::from(cell.value))
             .map_err(|e| Error::StorageError(format!("unable to decode pathinfo proto: {}", e)))?;
 
-        let store_path = path_info
-            .validate()
-            .map_err(|e| Error::StorageError(format!("invalid PathInfo: {}", e)))?;
+        let path_info = PathInfo::try_from(path_info_proto)
+            .map_err(|e| Error::StorageError(format!("Invalid path info: {e}")))?;
 
-        if store_path.digest() != &digest {
+        if path_info.store_path.digest() != &digest {
             return Err(Error::StorageError("PathInfo has unexpected digest".into()));
         }
 
@@ -248,14 +246,10 @@ impl PathInfoService for BigtablePathInfoService {
 
     #[instrument(level = "trace", skip_all, fields(path_info.root_node = ?path_info.node))]
     async fn put(&self, path_info: PathInfo) -> Result<PathInfo, Error> {
-        let store_path = path_info
-            .validate()
-            .map_err(|e| Error::InvalidRequest(format!("pathinfo failed validation: {}", e)))?;
-
         let mut client = self.client.clone();
-        let path_info_key = derive_pathinfo_key(store_path.digest());
+        let path_info_key = derive_pathinfo_key(path_info.store_path.digest());
 
-        let data = path_info.encode_to_vec();
+        let data = proto::PathInfo::from(path_info.clone()).encode_to_vec();
         if data.len() as u64 > CELL_SIZE_LIMIT {
             return Err(Error::StorageError(
                 "PathInfo exceeds cell limit on Bigtable".into(),
@@ -340,16 +334,12 @@ impl PathInfoService for BigtablePathInfoService {
                 }
 
                 // Try to parse the value into a PathInfo message.
-                let path_info = proto::PathInfo::decode(Bytes::from(cell.value))
+                let path_info_proto = proto::PathInfo::decode(Bytes::from(cell.value))
                     .map_err(|e| Error::StorageError(format!("unable to decode pathinfo proto: {}", e)))?;
 
-                // Validate the containing PathInfo, ensure its StorePath digest
-                // matches row key.
-                let store_path = path_info
-                    .validate()
-                    .map_err(|e| Error::StorageError(format!("invalid PathInfo: {}", e)))?;
+                let path_info = PathInfo::try_from(path_info_proto).map_err(|e| Error::StorageError(format!("Invalid path info: {e}")))?;
 
-                let exp_path_info_key = derive_pathinfo_key(store_path.digest());
+                let exp_path_info_key = derive_pathinfo_key(path_info.store_path.digest());
 
                 if exp_path_info_key.as_bytes() != row_key.as_slice() {
                     Err(Error::StorageError("PathInfo has unexpected digest".into()))?
diff --git a/tvix/store/src/pathinfoservice/combinators.rs b/tvix/store/src/pathinfoservice/combinators.rs
index bb5595f72b10..1f413cf310a2 100644
--- a/tvix/store/src/pathinfoservice/combinators.rs
+++ b/tvix/store/src/pathinfoservice/combinators.rs
@@ -1,6 +1,5 @@
 use std::sync::Arc;
 
-use crate::proto::PathInfo;
 use futures::stream::BoxStream;
 use nix_compat::nixbase32;
 use tonic::async_trait;
@@ -8,7 +7,7 @@ use tracing::{debug, instrument};
 use tvix_castore::composition::{CompositionContext, ServiceBuilder};
 use tvix_castore::Error;
 
-use super::PathInfoService;
+use super::{PathInfo, PathInfoService};
 
 /// Asks near first, if not found, asks far.
 /// If found in there, returns it, and *inserts* it into
@@ -105,11 +104,9 @@ mod test {
 
     use crate::{
         pathinfoservice::{LruPathInfoService, MemoryPathInfoService, PathInfoService},
-        tests::fixtures::PATH_INFO_WITH_NARINFO,
+        tests::fixtures::PATH_INFO,
     };
 
-    const PATH_INFO_DIGEST: [u8; 20] = [0; 20];
-
     /// Helper function setting up an instance of a "far" and "near"
     /// PathInfoService.
     async fn create_pathinfoservice() -> super::Cache<LruPathInfoService, MemoryPathInfoService> {
@@ -129,21 +126,25 @@ mod test {
         let svc = create_pathinfoservice().await;
 
         // query the PathInfo, things should not be there.
-        assert!(svc.get(PATH_INFO_DIGEST).await.unwrap().is_none());
+        assert!(svc
+            .get(*PATH_INFO.store_path.digest())
+            .await
+            .unwrap()
+            .is_none());
 
         // insert it into the far one.
-        svc.far.put(PATH_INFO_WITH_NARINFO.clone()).await.unwrap();
+        svc.far.put(PATH_INFO.clone()).await.unwrap();
 
         // now try getting it again, it should succeed.
         assert_eq!(
-            Some(PATH_INFO_WITH_NARINFO.clone()),
-            svc.get(PATH_INFO_DIGEST).await.unwrap()
+            Some(PATH_INFO.clone()),
+            svc.get(*PATH_INFO.store_path.digest()).await.unwrap()
         );
 
         // peek near, it should now be there.
         assert_eq!(
-            Some(PATH_INFO_WITH_NARINFO.clone()),
-            svc.near.get(PATH_INFO_DIGEST).await.unwrap()
+            Some(PATH_INFO.clone()),
+            svc.near.get(*PATH_INFO.store_path.digest()).await.unwrap()
         );
     }
 }
diff --git a/tvix/store/src/pathinfoservice/fs/mod.rs b/tvix/store/src/pathinfoservice/fs/mod.rs
index 1f7fa8a8afce..d996ec9f6f76 100644
--- a/tvix/store/src/pathinfoservice/fs/mod.rs
+++ b/tvix/store/src/pathinfoservice/fs/mod.rs
@@ -58,32 +58,20 @@ where
             .as_ref()
             .get(*store_path.digest())
             .await?
-            .map(|path_info| {
-                let node = path_info
-                    .node
-                    .as_ref()
-                    .expect("missing root node")
-                    .to_owned();
-
-                match node.into_name_and_node() {
-                    Ok((_name, node)) => Ok(node),
-                    Err(e) => Err(Error::StorageError(e.to_string())),
-                }
-            })
-            .transpose()?)
+            .map(|path_info| path_info.node))
     }
 
     fn list(&self) -> BoxStream<Result<(PathComponent, Node), Error>> {
         Box::pin(self.0.as_ref().list().map(|result| {
-            result.and_then(|path_info| {
-                let node = path_info
-                    .node
-                    .as_ref()
-                    .expect("missing root node")
-                    .to_owned();
-
-                node.into_name_and_node()
-                    .map_err(|e| Error::StorageError(e.to_string()))
+            result.map(|path_info| {
+                let basename = path_info.store_path.to_string();
+                (
+                    basename
+                        .as_str()
+                        .try_into()
+                        .expect("Tvix bug: StorePath must be PathComponent"),
+                    path_info.node,
+                )
             })
         }))
     }
diff --git a/tvix/store/src/pathinfoservice/grpc.rs b/tvix/store/src/pathinfoservice/grpc.rs
index 7510ccd911f0..d292b2a784f6 100644
--- a/tvix/store/src/pathinfoservice/grpc.rs
+++ b/tvix/store/src/pathinfoservice/grpc.rs
@@ -1,7 +1,7 @@
-use super::PathInfoService;
+use super::{PathInfo, PathInfoService};
 use crate::{
     nar::NarCalculationService,
-    proto::{self, ListPathInfoRequest, PathInfo},
+    proto::{self, ListPathInfoRequest},
 };
 use async_stream::try_stream;
 use futures::stream::BoxStream;
@@ -53,15 +53,10 @@ where
             .await;
 
         match path_info {
-            Ok(path_info) => {
-                let path_info = path_info.into_inner();
-
-                path_info
-                    .validate()
-                    .map_err(|e| Error::StorageError(format!("invalid pathinfo: {}", e)))?;
-
-                Ok(Some(path_info))
-            }
+            Ok(path_info) => Ok(Some(
+                PathInfo::try_from(path_info.into_inner())
+                    .map_err(|e| Error::StorageError(format!("Invalid path info: {e}")))?,
+            )),
             Err(e) if e.code() == Code::NotFound => Ok(None),
             Err(e) => Err(Error::StorageError(e.to_string())),
         }
@@ -72,12 +67,12 @@ where
         let path_info = self
             .grpc_client
             .clone()
-            .put(path_info)
+            .put(proto::PathInfo::from(path_info))
             .await
             .map_err(|e| Error::StorageError(e.to_string()))?
             .into_inner();
-
-        Ok(path_info)
+        Ok(PathInfo::try_from(path_info)
+            .map_err(|e| Error::StorageError(format!("Invalid path info: {e}")))?)
     }
 
     #[instrument(level = "trace", skip_all)]
@@ -91,21 +86,8 @@ where
 
             loop {
                 match stream.message().await {
-                    Ok(o) => match o {
-                        Some(pathinfo) => {
-                            // validate the pathinfo
-                            if let Err(e) = pathinfo.validate() {
-                                Err(Error::StorageError(format!(
-                                    "pathinfo {:?} failed validation: {}",
-                                    pathinfo, e
-                                )))?;
-                            }
-                            yield pathinfo
-                        }
-                        None => {
-                            return;
-                        },
-                    },
+                    Ok(Some(path_info)) => yield PathInfo::try_from(path_info).map_err(|e| Error::StorageError(format!("Invalid path info: {e}")))?,
+                    Ok(None) => return,
                     Err(e) => Err(Error::StorageError(e.to_string()))?,
                 }
             }
diff --git a/tvix/store/src/pathinfoservice/lru.rs b/tvix/store/src/pathinfoservice/lru.rs
index 695c04636089..2d8d52e3c9f6 100644
--- a/tvix/store/src/pathinfoservice/lru.rs
+++ b/tvix/store/src/pathinfoservice/lru.rs
@@ -8,11 +8,10 @@ use tokio::sync::RwLock;
 use tonic::async_trait;
 use tracing::instrument;
 
-use crate::proto::PathInfo;
 use tvix_castore::composition::{CompositionContext, ServiceBuilder};
 use tvix_castore::Error;
 
-use super::PathInfoService;
+use super::{PathInfo, PathInfoService};
 
 pub struct LruPathInfoService {
     lru: Arc<RwLock<LruCache<[u8; 20], PathInfo>>>,
@@ -35,15 +34,10 @@ impl PathInfoService for LruPathInfoService {
 
     #[instrument(level = "trace", skip_all, fields(path_info.root_node = ?path_info.node))]
     async fn put(&self, path_info: PathInfo) -> Result<PathInfo, Error> {
-        // call validate
-        let store_path = path_info
-            .validate()
-            .map_err(|e| Error::InvalidRequest(format!("invalid PathInfo: {}", e)))?;
-
         self.lru
             .write()
             .await
-            .put(*store_path.digest(), path_info.clone());
+            .put(*path_info.store_path.digest(), path_info.clone());
 
         Ok(path_info)
     }
@@ -91,40 +85,22 @@ impl ServiceBuilder for LruPathInfoServiceConfig {
 
 #[cfg(test)]
 mod test {
+    use nix_compat::store_path::StorePath;
     use std::num::NonZeroUsize;
 
     use crate::{
-        pathinfoservice::{LruPathInfoService, PathInfoService},
-        proto::PathInfo,
-        tests::fixtures::PATH_INFO_WITH_NARINFO,
+        pathinfoservice::{LruPathInfoService, PathInfo, PathInfoService},
+        tests::fixtures::PATH_INFO,
     };
     use lazy_static::lazy_static;
-    use tvix_castore::proto as castorepb;
 
     lazy_static! {
-        static ref PATHINFO_1: PathInfo = PATH_INFO_WITH_NARINFO.clone();
-        static ref PATHINFO_1_DIGEST: [u8; 20] = [0; 20];
         static ref PATHINFO_2: PathInfo = {
-            let mut p = PATHINFO_1.clone();
-            let root_node = p.node.as_mut().unwrap();
-            if let castorepb::Node { node: Some(node) } = root_node {
-                match node {
-                    castorepb::node::Node::Directory(n) => {
-                        n.name = "11111111111111111111111111111111-dummy2".into()
-                    }
-                    castorepb::node::Node::File(n) => {
-                        n.name = "11111111111111111111111111111111-dummy2".into()
-                    }
-                    castorepb::node::Node::Symlink(n) => {
-                        n.name = "11111111111111111111111111111111-dummy2".into()
-                    }
-                }
-            } else {
-                unreachable!()
-            }
+            let mut p = PATH_INFO.clone();
+            p.store_path = StorePath::from_name_and_digest_fixed("dummy", [1; 20]).unwrap();
             p
         };
-        static ref PATHINFO_2_DIGEST: [u8; 20] = *(PATHINFO_2.validate().unwrap()).digest();
+        static ref PATHINFO_2_DIGEST: [u8; 20] = *PATHINFO_2.store_path.digest();
     }
 
     #[tokio::test]
@@ -133,18 +109,20 @@ mod test {
 
         // pathinfo_1 should not be there
         assert!(svc
-            .get(*PATHINFO_1_DIGEST)
+            .get(*PATH_INFO.store_path.digest())
             .await
             .expect("no error")
             .is_none());
 
         // insert it
-        svc.put(PATHINFO_1.clone()).await.expect("no error");
+        svc.put(PATH_INFO.clone()).await.expect("no error");
 
         // now it should be there.
         assert_eq!(
-            Some(PATHINFO_1.clone()),
-            svc.get(*PATHINFO_1_DIGEST).await.expect("no error")
+            Some(PATH_INFO.clone()),
+            svc.get(*PATH_INFO.store_path.digest())
+                .await
+                .expect("no error")
         );
 
         // insert pathinfo_2. This will evict pathinfo 1
@@ -158,7 +136,7 @@ mod test {
 
         // … but pathinfo 1 not anymore.
         assert!(svc
-            .get(*PATHINFO_1_DIGEST)
+            .get(*PATH_INFO.store_path.digest())
             .await
             .expect("no error")
             .is_none());
diff --git a/tvix/store/src/pathinfoservice/memory.rs b/tvix/store/src/pathinfoservice/memory.rs
index 3fabd239c7b1..fd013fe9a573 100644
--- a/tvix/store/src/pathinfoservice/memory.rs
+++ b/tvix/store/src/pathinfoservice/memory.rs
@@ -1,5 +1,4 @@
-use super::PathInfoService;
-use crate::proto::PathInfo;
+use super::{PathInfo, PathInfoService};
 use async_stream::try_stream;
 use futures::stream::BoxStream;
 use nix_compat::nixbase32;
@@ -29,22 +28,11 @@ impl PathInfoService for MemoryPathInfoService {
 
     #[instrument(level = "trace", skip_all, fields(path_info.root_node = ?path_info.node))]
     async fn put(&self, path_info: PathInfo) -> Result<PathInfo, Error> {
-        // Call validate on the received PathInfo message.
-        match path_info.validate() {
-            Err(e) => Err(Error::InvalidRequest(format!(
-                "failed to validate PathInfo: {}",
-                e
-            ))),
+        // This overwrites existing PathInfo objects with the same store path digest.
+        let mut db = self.db.write().await;
+        db.insert(*path_info.store_path.digest(), path_info.clone());
 
-            // In case the PathInfo is valid, and we were able to extract a NixPath, store it in the database.
-            // This overwrites existing PathInfo objects.
-            Ok(nix_path) => {
-                let mut db = self.db.write().await;
-                db.insert(*nix_path.digest(), path_info.clone());
-
-                Ok(path_info)
-            }
-        }
+        Ok(path_info)
     }
 
     fn list(&self) -> BoxStream<'static, Result<PathInfo, Error>> {
diff --git a/tvix/store/src/pathinfoservice/mod.rs b/tvix/store/src/pathinfoservice/mod.rs
index 8d60ff79a33b..0a91d6267260 100644
--- a/tvix/store/src/pathinfoservice/mod.rs
+++ b/tvix/store/src/pathinfoservice/mod.rs
@@ -19,7 +19,7 @@ use tvix_castore::composition::{Registry, ServiceBuilder};
 use tvix_castore::Error;
 
 use crate::nar::NarCalculationService;
-use crate::proto::PathInfo;
+pub use crate::path_info::PathInfo;
 
 pub use self::combinators::{
     Cache as CachePathInfoService, CacheConfig as CachePathInfoServiceConfig,
diff --git a/tvix/store/src/pathinfoservice/nix_http.rs b/tvix/store/src/pathinfoservice/nix_http.rs
index 2ff094858bc9..ed386f0e9d14 100644
--- a/tvix/store/src/pathinfoservice/nix_http.rs
+++ b/tvix/store/src/pathinfoservice/nix_http.rs
@@ -1,10 +1,11 @@
-use super::PathInfoService;
-use crate::{nar::ingest_nar_and_hash, proto::PathInfo};
+use super::{PathInfo, PathInfoService};
+use crate::nar::ingest_nar_and_hash;
 use futures::{stream::BoxStream, TryStreamExt};
 use nix_compat::{
-    narinfo::{self, NarInfo},
+    narinfo::{self, NarInfo, Signature},
     nixbase32,
     nixhash::NixHash,
+    store_path::StorePath,
 };
 use reqwest::StatusCode;
 use std::sync::Arc;
@@ -12,9 +13,7 @@ use tokio::io::{self, AsyncRead};
 use tonic::async_trait;
 use tracing::{debug, instrument, warn};
 use tvix_castore::composition::{CompositionContext, ServiceBuilder};
-use tvix_castore::{
-    blobservice::BlobService, directoryservice::DirectoryService, proto as castorepb, Error,
-};
+use tvix_castore::{blobservice::BlobService, directoryservice::DirectoryService, Error};
 use url::Url;
 
 /// NixHTTPPathInfoService acts as a bridge in between the Nix HTTP Binary cache
@@ -137,12 +136,11 @@ where
             }
         }
 
-        // Convert to a (sparse) PathInfo. We still need to populate the node field,
-        // and for this we need to download the NAR file.
+        // To construct the full PathInfo, we also need to populate the node field,
+        // and for this we need to download the NAR file and ingest it into castore.
         // FUTUREWORK: Keep some database around mapping from narsha256 to
         // (unnamed) rootnode, so we can use that (and the name from the
         // StorePath) and avoid downloading the same NAR a second time.
-        let pathinfo: PathInfo = (&narinfo).into();
 
         // create a request for the NAR file itself.
         let nar_url = self.base_url.join(narinfo.url).map_err(|e| {
@@ -228,12 +226,18 @@ where
         }
 
         Ok(Some(PathInfo {
-            node: Some(castorepb::Node::from_name_and_node(
-                narinfo.store_path.to_string().into(),
-                root_node,
-            )),
-            references: pathinfo.references,
-            narinfo: pathinfo.narinfo,
+            store_path: narinfo.store_path.to_owned(),
+            node: root_node,
+            references: narinfo.references.iter().map(StorePath::to_owned).collect(),
+            nar_size: narinfo.nar_size,
+            nar_sha256: narinfo.nar_hash,
+            deriver: narinfo.deriver.as_ref().map(StorePath::to_owned),
+            signatures: narinfo
+                .signatures
+                .into_iter()
+                .map(|s| Signature::<String>::new(s.name().to_string(), s.bytes().to_owned()))
+                .collect(),
+            ca: narinfo.ca,
         }))
     }
 
diff --git a/tvix/store/src/pathinfoservice/redb.rs b/tvix/store/src/pathinfoservice/redb.rs
index bd0e0fc2b686..6e794e1981f0 100644
--- a/tvix/store/src/pathinfoservice/redb.rs
+++ b/tvix/store/src/pathinfoservice/redb.rs
@@ -1,5 +1,5 @@
-use super::PathInfoService;
-use crate::proto::PathInfo;
+use super::{PathInfo, PathInfoService};
+use crate::proto;
 use data_encoding::BASE64;
 use futures::{stream::BoxStream, StreamExt};
 use prost::Message;
@@ -78,10 +78,13 @@ impl PathInfoService for RedbPathInfoService {
                 let table = txn.open_table(PATHINFO_TABLE)?;
                 match table.get(digest)? {
                     Some(pathinfo_bytes) => Ok(Some(
-                        PathInfo::decode(pathinfo_bytes.value().as_slice()).map_err(|e| {
-                            warn!(err=%e, "failed to decode stored PathInfo");
-                            Error::StorageError("failed to decode stored PathInfo".to_string())
-                        })?,
+                        proto::PathInfo::decode(pathinfo_bytes.value().as_slice())
+                            .map_err(|e| {
+                                warn!(err=%e, "failed to decode stored PathInfo");
+                                Error::StorageError("failed to decode stored PathInfo".to_string())
+                            })?
+                            .try_into()
+                            .map_err(|e| Error::StorageError(format!("Invalid path info: {e}")))?,
                     )),
                     None => Ok(None),
                 }
@@ -92,25 +95,19 @@ impl PathInfoService for RedbPathInfoService {
 
     #[instrument(level = "trace", skip_all, fields(path_info.root_node = ?path_info.node))]
     async fn put(&self, path_info: PathInfo) -> Result<PathInfo, Error> {
-        // Call validate on the received PathInfo message.
-        let store_path = path_info
-            .validate()
-            .map_err(|e| {
-                warn!(err=%e, "failed to validate PathInfo");
-                Error::StorageError("failed to validate PathInfo".to_string())
-            })?
-            .to_owned();
-
-        let path_info_encoded = path_info.encode_to_vec();
         let db = self.db.clone();
 
         tokio::task::spawn_blocking({
+            let path_info = path_info.clone();
             move || -> Result<(), Error> {
                 let txn = db.begin_write()?;
                 {
                     let mut table = txn.open_table(PATHINFO_TABLE)?;
                     table
-                        .insert(store_path.digest(), path_info_encoded)
+                        .insert(
+                            *path_info.store_path.digest(),
+                            proto::PathInfo::from(path_info).encode_to_vec(),
+                        )
                         .map_err(|e| {
                             warn!(err=%e, "failed to insert PathInfo");
                             Error::StorageError("failed to insert PathInfo".to_string())
@@ -137,12 +134,18 @@ impl PathInfoService for RedbPathInfoService {
                 for elem in table.iter()? {
                     let elem = elem?;
                     tokio::runtime::Handle::current()
-                        .block_on(tx.send(Ok(
-                            PathInfo::decode(elem.1.value().as_slice()).map_err(|e| {
+                        .block_on(tx.send(Ok({
+                            let path_info_proto = proto::PathInfo::decode(
+                                elem.1.value().as_slice(),
+                            )
+                            .map_err(|e| {
                                 warn!(err=%e, "invalid PathInfo");
                                 Error::StorageError("invalid PathInfo".to_string())
-                            })?,
-                        )))
+                            })?;
+                            PathInfo::try_from(path_info_proto).map_err(|e| {
+                                Error::StorageError(format!("Invalid path info: {e}"))
+                            })?
+                        })))
                         .map_err(|e| Error::StorageError(e.to_string()))?;
                 }
 
diff --git a/tvix/store/src/pathinfoservice/signing_wrapper.rs b/tvix/store/src/pathinfoservice/signing_wrapper.rs
index 7f754ec49849..3230e000ab96 100644
--- a/tvix/store/src/pathinfoservice/signing_wrapper.rs
+++ b/tvix/store/src/pathinfoservice/signing_wrapper.rs
@@ -1,7 +1,6 @@
 //! This module provides a [PathInfoService] implementation that signs narinfos
 
-use super::PathInfoService;
-use crate::proto::PathInfo;
+use super::{PathInfo, PathInfoService};
 use futures::stream::BoxStream;
 use std::path::PathBuf;
 use std::sync::Arc;
@@ -11,9 +10,9 @@ use tvix_castore::composition::{CompositionContext, ServiceBuilder};
 
 use tvix_castore::Error;
 
-use nix_compat::narinfo::{parse_keypair, SigningKey};
+use nix_compat::narinfo::{parse_keypair, Signature, SigningKey};
 use nix_compat::nixbase32;
-use tracing::{instrument, warn};
+use tracing::instrument;
 
 #[cfg(test)]
 use super::MemoryPathInfoService;
@@ -52,22 +51,15 @@ where
     }
 
     async fn put(&self, path_info: PathInfo) -> Result<PathInfo, Error> {
-        let store_path = path_info.validate().map_err(|e| {
-            warn!(err=%e, "invalid PathInfo");
-            Error::StorageError(e.to_string())
-        })?;
-        let root_node = path_info.node.clone();
-        // If we have narinfo then sign it, else passthrough to the upper pathinfoservice
-        let path_info_to_put = match path_info.to_narinfo(store_path.as_ref()) {
-            Some(mut nar_info) => {
-                nar_info.add_signature(self.signing_key.as_ref());
-                let mut signed_path_info = PathInfo::from(&nar_info);
-                signed_path_info.node = root_node;
-                signed_path_info
-            }
-            None => path_info,
-        };
-        self.inner.put(path_info_to_put).await
+        let mut path_info = path_info.clone();
+        let mut nar_info = path_info.to_narinfo();
+        nar_info.add_signature(self.signing_key.as_ref());
+        path_info.signatures = nar_info
+            .signatures
+            .into_iter()
+            .map(|s| Signature::<String>::new(s.name().to_string(), s.bytes().to_owned()))
+            .collect();
+        self.inner.put(path_info).await
     }
 
     fn list(&self) -> BoxStream<'static, Result<PathInfo, Error>> {
@@ -134,51 +126,35 @@ pub const DUMMY_VERIFYING_KEY: &str = "do.not.use:cuXqnuzlWfGTKmfzBPx2kXShjRryZM
 
 #[cfg(test)]
 mod test {
-    use crate::{
-        pathinfoservice::PathInfoService,
-        proto::PathInfo,
-        tests::fixtures::{DUMMY_PATH, PATH_INFO_WITH_NARINFO},
-    };
+    use crate::{pathinfoservice::PathInfoService, tests::fixtures::PATH_INFO};
     use nix_compat::narinfo::VerifyingKey;
 
-    use lazy_static::lazy_static;
-    use nix_compat::store_path::StorePath;
-
-    lazy_static! {
-        static ref PATHINFO_1: PathInfo = PATH_INFO_WITH_NARINFO.clone();
-        static ref PATHINFO_1_DIGEST: [u8; 20] = [0; 20];
-    }
-
     #[tokio::test]
     async fn put_and_verify_signature() {
         let svc = super::test_signing_service();
 
         // pathinfo_1 should not be there ...
         assert!(svc
-            .get(*PATHINFO_1_DIGEST)
+            .get(*PATH_INFO.store_path.digest())
             .await
             .expect("no error")
             .is_none());
 
         // ... and not be signed
-        assert!(PATHINFO_1.narinfo.clone().unwrap().signatures.is_empty());
+        assert!(PATH_INFO.signatures.is_empty());
 
         // insert it
-        svc.put(PATHINFO_1.clone()).await.expect("no error");
+        svc.put(PATH_INFO.clone()).await.expect("no error");
 
         // now it should be there ...
         let signed = svc
-            .get(*PATHINFO_1_DIGEST)
+            .get(*PATH_INFO.store_path.digest())
             .await
             .expect("no error")
             .unwrap();
 
         // and signed
-        let narinfo = signed
-            .to_narinfo(
-                StorePath::from_bytes(DUMMY_PATH.as_bytes()).expect("DUMMY_PATH to be parsed"),
-            )
-            .expect("no error");
+        let narinfo = signed.to_narinfo();
         let fp = narinfo.fingerprint();
 
         // load our keypair from the fixtures
diff --git a/tvix/store/src/pathinfoservice/tests/mod.rs b/tvix/store/src/pathinfoservice/tests/mod.rs
index 028fa5af57fc..12c685c80fad 100644
--- a/tvix/store/src/pathinfoservice/tests/mod.rs
+++ b/tvix/store/src/pathinfoservice/tests/mod.rs
@@ -6,12 +6,10 @@ use futures::TryStreamExt;
 use rstest::*;
 use rstest_reuse::{self, *};
 
-use super::PathInfoService;
+use super::{PathInfo, PathInfoService};
 use crate::pathinfoservice::redb::RedbPathInfoService;
 use crate::pathinfoservice::MemoryPathInfoService;
-use crate::proto::PathInfo;
-use crate::tests::fixtures::DUMMY_PATH_DIGEST;
-use tvix_castore::proto as castorepb;
+use crate::tests::fixtures::{DUMMY_PATH_DIGEST, PATH_INFO};
 
 use crate::pathinfoservice::test_signing_service;
 
@@ -52,32 +50,35 @@ async fn not_found(svc: impl PathInfoService) {
 #[apply(path_info_services)]
 #[tokio::test]
 async fn put_get(svc: impl PathInfoService) {
-    let path_info = PathInfo {
-        node: Some(castorepb::Node {
-            node: Some(castorepb::node::Node::Symlink(castorepb::SymlinkNode {
-                name: "00000000000000000000000000000000-foo".into(),
-                target: "doesntmatter".into(),
-            })),
-        }),
-        ..Default::default()
-    };
-
     // insert
-    let resp = svc.put(path_info.clone()).await.expect("must succeed");
+    let resp = svc.put(PATH_INFO.clone()).await.expect("must succeed");
 
-    // expect the returned PathInfo to be equal (for now)
-    // in the future, some stores might add additional fields/signatures.
-    assert_eq!(path_info, resp);
+    // expect the returned PathInfo to be equal,
+    // remove the signatures as the SigningPathInfoService adds them
+    assert_eq!(*PATH_INFO, strip_signatures(resp));
 
     // get it back
     let resp = svc.get(DUMMY_PATH_DIGEST).await.expect("must succeed");
 
-    assert_eq!(Some(path_info.clone()), resp);
+    assert_eq!(Some(PATH_INFO.clone()), resp.map(strip_signatures));
 
     // Ensure the listing endpoint works, and returns the same path_info.
     // FUTUREWORK: split this, some impls might (rightfully) not support listing
     let pathinfos: Vec<PathInfo> = svc.list().try_collect().await.expect("must succeed");
 
     // We should get a single pathinfo back, the one we inserted.
-    assert_eq!(vec![path_info], pathinfos);
+    assert_eq!(
+        vec![PATH_INFO.clone()],
+        pathinfos
+            .into_iter()
+            .map(strip_signatures)
+            .collect::<Vec<_>>()
+    );
+}
+
+fn strip_signatures(path_info: PathInfo) -> PathInfo {
+    PathInfo {
+        signatures: vec![],
+        ..path_info
+    }
 }
diff --git a/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs b/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs
index 60da73012df7..5da3b23c269c 100644
--- a/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs
+++ b/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs
@@ -1,5 +1,5 @@
 use crate::nar::{NarCalculationService, RenderError};
-use crate::pathinfoservice::PathInfoService;
+use crate::pathinfoservice::{PathInfo, PathInfoService};
 use crate::proto;
 use futures::{stream::BoxStream, TryStreamExt};
 use std::ops::Deref;
@@ -44,7 +44,7 @@ where
                     .map_err(|_e| Status::invalid_argument("invalid output digest length"))?;
                 match self.path_info_service.get(digest).await {
                     Ok(None) => Err(Status::not_found("PathInfo not found")),
-                    Ok(Some(path_info)) => Ok(Response::new(path_info)),
+                    Ok(Some(path_info)) => Ok(Response::new(proto::PathInfo::from(path_info))),
                     Err(e) => {
                         warn!(err = %e, "failed to get PathInfo");
                         Err(e.into())
@@ -56,12 +56,15 @@ where
 
     #[instrument(skip_all)]
     async fn put(&self, request: Request<proto::PathInfo>) -> Result<Response<proto::PathInfo>> {
-        let path_info = request.into_inner();
+        let path_info_proto = request.into_inner();
+
+        let path_info = PathInfo::try_from(path_info_proto)
+            .map_err(|e| Status::invalid_argument(format!("Invalid path info: {e}")))?;
 
         // Store the PathInfo in the client. Clients MUST validate the data
         // they receive, so we don't validate additionally here.
         match self.path_info_service.put(path_info).await {
-            Ok(path_info_new) => Ok(Response::new(path_info_new)),
+            Ok(path_info_new) => Ok(Response::new(proto::PathInfo::from(path_info_new))),
             Err(e) => {
                 warn!(err = %e, "failed to put PathInfo");
                 Err(e.into())
@@ -99,6 +102,7 @@ where
         let stream = Box::pin(
             self.path_info_service
                 .list()
+                .map_ok(proto::PathInfo::from)
                 .map_err(|e| Status::internal(e.to_string())),
         );
 
diff --git a/tvix/store/src/proto/mod.rs b/tvix/store/src/proto/mod.rs
index f3ea4b196946..807f03854ddc 100644
--- a/tvix/store/src/proto/mod.rs
+++ b/tvix/store/src/proto/mod.rs
@@ -4,7 +4,7 @@ use bytes::Bytes;
 use data_encoding::BASE64;
 // https://github.com/hyperium/tonic/issues/1056
 use nix_compat::{
-    narinfo::Flags,
+    narinfo::{Signature, SignatureError},
     nixhash::{CAHash, NixHash},
     store_path::{self, StorePathRef},
 };
@@ -17,6 +17,8 @@ pub use grpc_pathinfoservice_wrapper::GRPCPathInfoServiceWrapper;
 
 tonic::include_proto!("tvix.store.v1");
 
+use tvix_castore::proto as castorepb;
+
 #[cfg(feature = "tonic-reflection")]
 /// Compiled file descriptors for implementing [gRPC
 /// reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) with e.g.
@@ -70,183 +72,18 @@ pub enum ValidatePathInfoError {
     /// The deriver field is invalid.
     #[error("deriver field is invalid: {0}")]
     InvalidDeriverField(store_path::Error),
-}
 
-/// Parses a root node name.
-///
-/// On success, this returns the parsed [store_path::StorePathRef].
-/// On error, it returns an error generated from the supplied constructor.
-fn parse_node_name_root<E>(
-    name: &[u8],
-    err: fn(Vec<u8>, store_path::Error) -> E,
-) -> Result<store_path::StorePathRef<'_>, E> {
-    store_path::StorePathRef::from_bytes(name).map_err(|e| err(name.to_vec(), e))
-}
+    /// The narinfo field is missing
+    #[error("The narinfo field is missing")]
+    NarInfoFieldMissing,
 
-impl PathInfo {
-    /// validate performs some checks on the PathInfo struct,
-    /// Returning either a [store_path::StorePath] of the root node, or a
-    /// [ValidatePathInfoError].
-    pub fn validate(&self) -> Result<store_path::StorePath<String>, ValidatePathInfoError> {
-        // ensure the references have the right number of bytes.
-        for (i, reference) in self.references.iter().enumerate() {
-            if reference.len() != store_path::DIGEST_SIZE {
-                return Err(ValidatePathInfoError::InvalidReferenceDigestLen(
-                    i,
-                    reference.len(),
-                ));
-            }
-        }
+    /// The ca field is invalid
+    #[error("The ca field is invalid: {0}")]
+    InvalidCaField(ConvertCAError),
 
-        // If there is a narinfo field populated…
-        if let Some(narinfo) = &self.narinfo {
-            // ensure the nar_sha256 digest has the correct length.
-            if narinfo.nar_sha256.len() != 32 {
-                return Err(ValidatePathInfoError::InvalidNarSha256DigestLen(
-                    narinfo.nar_sha256.len(),
-                ));
-            }
-
-            // ensure the number of references there matches PathInfo.references count.
-            if narinfo.reference_names.len() != self.references.len() {
-                return Err(ValidatePathInfoError::InconsistentNumberOfReferences(
-                    self.references.len(),
-                    narinfo.reference_names.len(),
-                ));
-            }
-
-            // parse references in reference_names.
-            for (i, reference_name_str) in narinfo.reference_names.iter().enumerate() {
-                // ensure thy parse as (non-absolute) store path
-                let reference_names_store_path = store_path::StorePathRef::from_bytes(
-                    reference_name_str.as_bytes(),
-                )
-                .map_err(|_| {
-                    ValidatePathInfoError::InvalidNarinfoReferenceName(
-                        i,
-                        reference_name_str.to_owned(),
-                    )
-                })?;
-
-                // ensure their digest matches the one at self.references[i].
-                {
-                    // This is safe, because we ensured the proper length earlier already.
-                    let reference_digest = self.references[i].to_vec().try_into().unwrap();
-
-                    if reference_names_store_path.digest() != &reference_digest {
-                        return Err(
-                            ValidatePathInfoError::InconsistentNarinfoReferenceNameDigest(
-                                i,
-                                reference_digest,
-                                *reference_names_store_path.digest(),
-                            ),
-                        );
-                    }
-                }
-
-                // If the Deriver field is populated, ensure it parses to a
-                // [store_path::StorePath].
-                // We can't check for it to *not* end with .drv, as the .drv files produced by
-                // recursive Nix end with multiple .drv suffixes, and only one is popped when
-                // converting to this field.
-                if let Some(deriver) = &narinfo.deriver {
-                    store_path::StorePathRef::from_name_and_digest(&deriver.name, &deriver.digest)
-                        .map_err(ValidatePathInfoError::InvalidDeriverField)?;
-                }
-            }
-        }
-
-        // Ensure there is a (root) node present, and it properly parses to a [store_path::StorePath].
-        let root_nix_path = match &self.node {
-            None => Err(ValidatePathInfoError::NoNodePresent)?,
-            Some(node) => {
-                // NOTE: We could have some PathComponent not allocating here,
-                // so this can return StorePathRef.
-                // However, as this will get refactored away to stricter types
-                // soon anyways, there's no point.
-                let (name, _node) = node
-                    .clone()
-                    .into_name_and_node()
-                    .map_err(ValidatePathInfoError::InvalidRootNode)?;
-
-                // parse the name of the node itself and return
-                parse_node_name_root(name.as_ref(), ValidatePathInfoError::InvalidNodeName)?
-                    .to_owned()
-            }
-        };
-
-        // return the root nix path
-        Ok(root_nix_path)
-    }
-
-    /// With self and its store path name, this reconstructs a
-    /// [nix_compat::narinfo::NarInfo<'_>].
-    /// It can be used to validate Signatures, or get back a (sparse) NarInfo
-    /// struct to prepare writing it out.
-    ///
-    /// It assumes self to be validated first, and will only return None if the
-    /// `narinfo` field is unpopulated.
-    ///
-    /// It does very little allocation (a Vec each for `signatures` and
-    /// `references`), the rest points to data owned elsewhere.
-    ///
-    /// Keep in mind this is not able to reconstruct all data present in the
-    /// NarInfo<'_>, as some of it is not stored at all:
-    /// - the `system`, `file_hash` and `file_size` fields are set to `None`.
-    /// - the URL is set to an empty string.
-    /// - Compression is set to "none"
-    ///
-    /// If you want to render it out to a string and be able to parse it back
-    /// in, at least URL *must* be set again.
-    pub fn to_narinfo<'a>(
-        &'a self,
-        store_path: store_path::StorePathRef<'a>,
-    ) -> Option<nix_compat::narinfo::NarInfo<'_>> {
-        let narinfo = &self.narinfo.as_ref()?;
-
-        Some(nix_compat::narinfo::NarInfo {
-            flags: Flags::empty(),
-            store_path,
-            nar_hash: narinfo
-                .nar_sha256
-                .as_ref()
-                .try_into()
-                .expect("invalid narhash"),
-            nar_size: narinfo.nar_size,
-            references: narinfo
-                .reference_names
-                .iter()
-                .map(|ref_name| {
-                    // This shouldn't pass validation
-                    StorePathRef::from_bytes(ref_name.as_bytes()).expect("invalid reference")
-                })
-                .collect(),
-            signatures: narinfo
-                .signatures
-                .iter()
-                .map(|sig| {
-                    nix_compat::narinfo::SignatureRef::new(
-                        &sig.name,
-                        // This shouldn't pass validation
-                        sig.data[..].try_into().expect("invalid signature len"),
-                    )
-                })
-                .collect(),
-            ca: narinfo
-                .ca
-                .as_ref()
-                .map(|ca| ca.try_into().expect("invalid ca")),
-            system: None,
-            deriver: narinfo.deriver.as_ref().map(|deriver| {
-                StorePathRef::from_name_and_digest(&deriver.name, &deriver.digest)
-                    .expect("invalid deriver")
-            }),
-            url: "",
-            compression: Some("none"),
-            file_hash: None,
-            file_size: None,
-        })
-    }
+    /// The signature at position is invalid
+    #[error("The signature at position {0} is invalid: {1}")]
+    InvalidSignature(usize, SignatureError),
 }
 
 /// Errors that can occur when converting from a [nar_info::Ca] to a (stricter)
@@ -341,45 +178,154 @@ impl From<&nix_compat::nixhash::CAHash> for nar_info::Ca {
     }
 }
 
-impl From<&nix_compat::narinfo::NarInfo<'_>> for NarInfo {
-    /// Converts from a NarInfo (returned from the NARInfo parser) to the proto-
-    /// level NarInfo struct.
-    fn from(value: &nix_compat::narinfo::NarInfo<'_>) -> Self {
-        let signatures = value
-            .signatures
-            .iter()
-            .map(|sig| nar_info::Signature {
-                name: sig.name().to_string(),
-                data: Bytes::copy_from_slice(sig.bytes()),
-            })
-            .collect();
-
-        NarInfo {
-            nar_size: value.nar_size,
-            nar_sha256: Bytes::copy_from_slice(&value.nar_hash),
-            signatures,
-            reference_names: value.references.iter().map(|r| r.to_string()).collect(),
-            deriver: value.deriver.as_ref().map(|sp| StorePath {
-                name: (*sp.name()).to_owned(),
-                digest: Bytes::copy_from_slice(sp.digest()),
-            }),
-            ca: value.ca.as_ref().map(|ca| ca.into()),
-        }
-    }
-}
-
-impl From<&nix_compat::narinfo::NarInfo<'_>> for PathInfo {
-    /// Converts from a NarInfo (returned from the NARInfo parser) to a PathInfo
-    /// struct with the node set to None.
-    fn from(value: &nix_compat::narinfo::NarInfo<'_>) -> Self {
+impl From<crate::pathinfoservice::PathInfo> for PathInfo {
+    fn from(value: crate::pathinfoservice::PathInfo) -> Self {
         Self {
-            node: None,
+            node: Some(castorepb::Node::from_name_and_node(
+                value.store_path.to_string().into_bytes().into(),
+                value.node,
+            )),
             references: value
                 .references
                 .iter()
-                .map(|x| Bytes::copy_from_slice(x.digest()))
+                .map(|reference| Bytes::copy_from_slice(reference.digest()))
                 .collect(),
-            narinfo: Some(value.into()),
+            narinfo: Some(NarInfo {
+                nar_size: value.nar_size,
+                nar_sha256: Bytes::copy_from_slice(&value.nar_sha256),
+                signatures: value
+                    .signatures
+                    .iter()
+                    .map(|sig| nar_info::Signature {
+                        name: sig.name().to_string(),
+                        data: Bytes::copy_from_slice(sig.bytes()),
+                    })
+                    .collect(),
+                reference_names: value.references.iter().map(|r| r.to_string()).collect(),
+                deriver: value.deriver.as_ref().map(|sp| StorePath {
+                    name: (*sp.name()).to_owned(),
+                    digest: Bytes::copy_from_slice(sp.digest()),
+                }),
+                ca: value.ca.as_ref().map(|ca| ca.into()),
+            }),
+        }
+    }
+}
+
+impl TryFrom<PathInfo> for crate::pathinfoservice::PathInfo {
+    type Error = ValidatePathInfoError;
+    fn try_from(value: PathInfo) -> Result<Self, Self::Error> {
+        let narinfo = value
+            .narinfo
+            .ok_or_else(|| ValidatePathInfoError::NarInfoFieldMissing)?;
+
+        // ensure the references have the right number of bytes.
+        for (i, reference) in value.references.iter().enumerate() {
+            if reference.len() != store_path::DIGEST_SIZE {
+                return Err(ValidatePathInfoError::InvalidReferenceDigestLen(
+                    i,
+                    reference.len(),
+                ));
+            }
+        }
+
+        // ensure the number of references there matches PathInfo.references count.
+        if narinfo.reference_names.len() != value.references.len() {
+            return Err(ValidatePathInfoError::InconsistentNumberOfReferences(
+                value.references.len(),
+                narinfo.reference_names.len(),
+            ));
+        }
+
+        // parse references in reference_names.
+        let mut references = vec![];
+        for (i, reference_name_str) in narinfo.reference_names.iter().enumerate() {
+            // ensure thy parse as (non-absolute) store path
+            let reference_names_store_path =
+                StorePathRef::from_bytes(reference_name_str.as_bytes()).map_err(|_| {
+                    ValidatePathInfoError::InvalidNarinfoReferenceName(
+                        i,
+                        reference_name_str.to_owned(),
+                    )
+                })?;
+
+            // ensure their digest matches the one at self.references[i].
+            {
+                // This is safe, because we ensured the proper length earlier already.
+                let reference_digest = value.references[i].to_vec().try_into().unwrap();
+
+                if reference_names_store_path.digest() != &reference_digest {
+                    return Err(
+                        ValidatePathInfoError::InconsistentNarinfoReferenceNameDigest(
+                            i,
+                            reference_digest,
+                            *reference_names_store_path.digest(),
+                        ),
+                    );
+                } else {
+                    references.push(reference_names_store_path.to_owned());
+                }
+            }
         }
+
+        let nar_sha256_length = narinfo.nar_sha256.len();
+
+        // split value.node into the name and node components
+        let (name, node) = value
+            .node
+            .ok_or_else(|| ValidatePathInfoError::NoNodePresent)?
+            .into_name_and_node()
+            .map_err(ValidatePathInfoError::InvalidRootNode)?;
+
+        Ok(Self {
+            // value.node has a valid name according to the castore model but might not parse to a
+            // [StorePath]
+            store_path: nix_compat::store_path::StorePath::from_bytes(name.as_ref()).map_err(
+                |err| ValidatePathInfoError::InvalidNodeName(name.as_ref().to_vec(), err),
+            )?,
+            node,
+            references,
+            nar_size: narinfo.nar_size,
+            nar_sha256: narinfo.nar_sha256.to_vec()[..]
+                .try_into()
+                .map_err(|_| ValidatePathInfoError::InvalidNarSha256DigestLen(nar_sha256_length))?,
+            // If the Deriver field is populated, ensure it parses to a
+            // [StorePath].
+            // We can't check for it to *not* end with .drv, as the .drv files produced by
+            // recursive Nix end with multiple .drv suffixes, and only one is popped when
+            // converting to this field.
+            deriver: narinfo
+                .deriver
+                .map(|deriver| {
+                    nix_compat::store_path::StorePath::from_name_and_digest(
+                        &deriver.name,
+                        &deriver.digest,
+                    )
+                    .map_err(ValidatePathInfoError::InvalidDeriverField)
+                })
+                .transpose()?,
+            signatures: narinfo
+                .signatures
+                .into_iter()
+                .enumerate()
+                .map(|(i, signature)| {
+                    signature.data.to_vec()[..]
+                        .try_into()
+                        .map_err(|_| {
+                            ValidatePathInfoError::InvalidSignature(
+                                i,
+                                SignatureError::InvalidSignatureLen(signature.data.len()),
+                            )
+                        })
+                        .map(|signature_data| Signature::new(signature.name, signature_data))
+                })
+                .collect::<Result<Vec<_>, ValidatePathInfoError>>()?,
+            ca: narinfo
+                .ca
+                .as_ref()
+                .map(TryFrom::try_from)
+                .transpose()
+                .map_err(ValidatePathInfoError::InvalidCaField)?,
+        })
     }
 }
diff --git a/tvix/store/src/proto/tests/pathinfo.rs b/tvix/store/src/proto/tests/pathinfo.rs
index f5ecc798bbfb..320f419b6c59 100644
--- a/tvix/store/src/proto/tests/pathinfo.rs
+++ b/tvix/store/src/proto/tests/pathinfo.rs
@@ -1,274 +1,226 @@
-use crate::proto::{nar_info::Signature, NarInfo, PathInfo, ValidatePathInfoError};
-use crate::tests::fixtures::*;
+use crate::pathinfoservice::PathInfo;
+use crate::proto::{self, ValidatePathInfoError};
+use crate::tests::fixtures::{DUMMY_PATH, DUMMY_PATH_DIGEST, DUMMY_PATH_STR};
 use bytes::Bytes;
-use data_encoding::BASE64;
-use nix_compat::nixbase32;
-use nix_compat::store_path::{self, StorePath, StorePathRef};
+use lazy_static::lazy_static;
+use nix_compat::store_path;
 use rstest::rstest;
+use tvix_castore::fixtures::DUMMY_DIGEST;
 use tvix_castore::proto as castorepb;
 use tvix_castore::{DirectoryError, ValidateNodeError};
 
-#[rstest]
-#[case::no_node(None, Err(ValidatePathInfoError::NoNodePresent))]
-#[case::no_node_2(Some(castorepb::Node { node: None}), Err(ValidatePathInfoError::InvalidRootNode(DirectoryError::NoNodeSet)))]
+lazy_static! {
+    /// A valid PathInfo message
+    /// The references in `narinfo.reference_names` aligns with what's in
+    /// `references`.
+    static ref PROTO_PATH_INFO : proto::PathInfo = proto::PathInfo {
+        node: Some(castorepb::Node {
+            node: Some(castorepb::node::Node::Directory(castorepb::DirectoryNode {
+                name: DUMMY_PATH_STR.into(),
+                digest: DUMMY_DIGEST.clone().into(),
+                size: 0,
+            })),
+        }),
+        references: vec![DUMMY_PATH_DIGEST.as_slice().into()],
+        narinfo: Some(proto::NarInfo {
+            nar_size: 0,
+            nar_sha256: DUMMY_DIGEST.clone().into(),
+            signatures: vec![],
+            reference_names: vec![DUMMY_PATH_STR.to_string()],
+            deriver: None,
+            ca: Some(proto::nar_info::Ca { r#type: proto::nar_info::ca::Hash::NarSha256.into(), digest:  DUMMY_DIGEST.clone().into() })
+        }),
+    };
+}
+
+#[test]
+fn convert_valid() {
+    let path_info = PROTO_PATH_INFO.clone();
+    PathInfo::try_from(path_info).expect("must succeed");
+}
+
+/// Create a PathInfo with a correct deriver field and ensure it succeeds.
+#[test]
+fn convert_valid_deriver() {
+    let mut path_info = PROTO_PATH_INFO.clone();
+
+    // add a valid deriver
+    let narinfo = path_info.narinfo.as_mut().unwrap();
+    narinfo.deriver = Some(crate::proto::StorePath {
+        name: DUMMY_PATH.name().to_string(),
+        digest: Bytes::from(DUMMY_PATH_DIGEST.as_slice()),
+    });
 
-fn validate_pathinfo(
+    let path_info = PathInfo::try_from(path_info).expect("must succeed");
+    assert_eq!(DUMMY_PATH.clone(), path_info.deriver.unwrap())
+}
+
+#[rstest]
+#[case::no_node(None, ValidatePathInfoError::NoNodePresent)]
+#[case::no_node_2(Some(castorepb::Node { node: None}), ValidatePathInfoError::InvalidRootNode(DirectoryError::NoNodeSet))]
+fn convert_pathinfo_wrong_nodes(
     #[case] node: Option<castorepb::Node>,
-    #[case] exp_result: Result<StorePath<String>, ValidatePathInfoError>,
+    #[case] exp_err: ValidatePathInfoError,
 ) {
     // construct the PathInfo object
-    let p = PathInfo {
-        node,
-        ..Default::default()
-    };
+    let mut path_info = PROTO_PATH_INFO.clone();
+    path_info.node = node;
 
-    assert_eq!(exp_result, p.validate());
+    assert_eq!(
+        exp_err,
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
+/// Constructs a [proto::PathInfo] with root nodes that have wrong data in
+/// various places, causing the conversion to [PathInfo] to fail.
 #[rstest]
-#[case::ok(castorepb::DirectoryNode {
-        name: DUMMY_PATH.into(),
-        digest: DUMMY_DIGEST.clone().into(),
-        size: 0,
-}, Ok(StorePath::from_bytes(DUMMY_PATH.as_bytes()).unwrap()))]
-#[case::invalid_digest_length(castorepb::DirectoryNode {
-        name: DUMMY_PATH.into(),
+#[case::directory_invalid_digest_length(
+    castorepb::node::Node::Directory(castorepb::DirectoryNode {
+        name: DUMMY_PATH_STR.into(),
         digest: Bytes::new(),
         size: 0,
-}, Err(ValidatePathInfoError::InvalidRootNode(DirectoryError::InvalidNode(DUMMY_PATH.into(), ValidateNodeError::InvalidDigestLen(0)))))]
-#[case::invalid_node_name_no_storepath(castorepb::DirectoryNode {
+    }),
+    ValidatePathInfoError::InvalidRootNode(DirectoryError::InvalidNode(DUMMY_PATH_STR.into(), ValidateNodeError::InvalidDigestLen(0)))
+)]
+#[case::directory_invalid_node_name_no_storepath(
+    castorepb::node::Node::Directory(castorepb::DirectoryNode {
         name: "invalid".into(),
         digest: DUMMY_DIGEST.clone().into(),
         size: 0,
-}, Err(ValidatePathInfoError::InvalidNodeName(
-        "invalid".into(),
-        store_path::Error::InvalidLength
-)))]
-fn validate_directory(
-    #[case] directory_node: castorepb::DirectoryNode,
-    #[case] exp_result: Result<StorePath<String>, ValidatePathInfoError>,
-) {
-    // construct the PathInfo object
-    let p = PathInfo {
-        node: Some(castorepb::Node {
-            node: Some(castorepb::node::Node::Directory(directory_node)),
-        }),
-        ..Default::default()
-    };
-    assert_eq!(exp_result, p.validate());
-}
-
-#[rstest]
-#[case::ok(
-    castorepb::FileNode {
-        name: DUMMY_PATH.into(),
-        digest: DUMMY_DIGEST.clone().into(),
-        size: 0,
-        executable: false,
-    },
-    Ok(StorePath::from_bytes(DUMMY_PATH.as_bytes()).unwrap())
+    }),
+    ValidatePathInfoError::InvalidNodeName("invalid".into(), store_path::Error::InvalidLength)
 )]
-#[case::invalid_digest_len(
-    castorepb::FileNode {
-        name: DUMMY_PATH.into(),
+#[case::file_invalid_digest_len(
+    castorepb::node::Node::File(castorepb::FileNode {
+        name: DUMMY_PATH_STR.into(),
         digest: Bytes::new(),
         ..Default::default()
-    },
-    Err(ValidatePathInfoError::InvalidRootNode(DirectoryError::InvalidNode(DUMMY_PATH.into(), ValidateNodeError::InvalidDigestLen(0))))
+    }),
+    ValidatePathInfoError::InvalidRootNode(DirectoryError::InvalidNode(DUMMY_PATH_STR.into(), ValidateNodeError::InvalidDigestLen(0)))
 )]
-#[case::invalid_node_name(
-    castorepb::FileNode {
+#[case::file_invalid_node_name(
+    castorepb::node::Node::File(castorepb::FileNode {
         name: "invalid".into(),
         digest: DUMMY_DIGEST.clone().into(),
         ..Default::default()
-    },
-    Err(ValidatePathInfoError::InvalidNodeName(
+    }),
+    ValidatePathInfoError::InvalidNodeName(
         "invalid".into(),
         store_path::Error::InvalidLength
-    ))
-)]
-fn validate_file(
-    #[case] file_node: castorepb::FileNode,
-    #[case] exp_result: Result<StorePath<String>, ValidatePathInfoError>,
-) {
-    // construct the PathInfo object
-    let p = PathInfo {
-        node: Some(castorepb::Node {
-            node: Some(castorepb::node::Node::File(file_node)),
-        }),
-        ..Default::default()
-    };
-    assert_eq!(exp_result, p.validate());
-}
-
-#[rstest]
-#[case::ok(
-    castorepb::SymlinkNode {
-        name: DUMMY_PATH.into(),
-        target: "foo".into(),
-    },
-    Ok(StorePath::from_bytes(DUMMY_PATH.as_bytes()).unwrap())
+    )
 )]
-#[case::invalid_node_name(
-    castorepb::SymlinkNode {
+#[case::symlink_invalid_node_name(
+    castorepb::node::Node::Symlink(castorepb::SymlinkNode {
         name: "invalid".into(),
         target: "foo".into(),
-    },
-    Err(ValidatePathInfoError::InvalidNodeName(
+    }),
+    ValidatePathInfoError::InvalidNodeName(
         "invalid".into(),
         store_path::Error::InvalidLength
-    ))
+    )
 )]
-fn validate_symlink(
-    #[case] symlink_node: castorepb::SymlinkNode,
-    #[case] exp_result: Result<StorePath<String>, ValidatePathInfoError>,
-) {
-    // construct the PathInfo object
-    let p = PathInfo {
-        node: Some(castorepb::Node {
-            node: Some(castorepb::node::Node::Symlink(symlink_node)),
-        }),
-        ..Default::default()
-    };
-    assert_eq!(exp_result, p.validate());
-}
+fn convert_fail_node(#[case] node: castorepb::node::Node, #[case] exp_err: ValidatePathInfoError) {
+    // construct the proto::PathInfo object
+    let mut p = PROTO_PATH_INFO.clone();
+    p.node = Some(castorepb::Node { node: Some(node) });
 
-/// Ensure parsing a correct PathInfo without narinfo populated succeeds.
-#[test]
-fn validate_references_without_narinfo_ok() {
-    assert!(PATH_INFO_WITHOUT_NARINFO.validate().is_ok());
+    assert_eq!(exp_err, PathInfo::try_from(p).expect_err("must fail"));
 }
 
-/// Ensure parsing a correct PathInfo with narinfo populated succeeds.
+/// Ensure a PathInfo without narinfo populated fails converting!
 #[test]
-fn validate_references_with_narinfo_ok() {
-    assert!(PATH_INFO_WITH_NARINFO.validate().is_ok());
+fn convert_without_narinfo_fail() {
+    let mut path_info = PROTO_PATH_INFO.clone();
+    path_info.narinfo = None;
+
+    assert_eq!(
+        ValidatePathInfoError::NarInfoFieldMissing,
+        PathInfo::try_from(path_info).expect_err("must fail"),
+    );
 }
 
 /// Create a PathInfo with a wrong digest length in narinfo.nar_sha256, and
-/// ensure validation fails.
+/// ensure conversion fails.
 #[test]
-fn validate_wrong_nar_sha256() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_wrong_nar_sha256() {
+    let mut path_info = PROTO_PATH_INFO.clone();
     path_info.narinfo.as_mut().unwrap().nar_sha256 = vec![0xbe, 0xef].into();
 
-    match path_info.validate().expect_err("must_fail") {
-        ValidatePathInfoError::InvalidNarSha256DigestLen(2) => {}
-        e => panic!("unexpected error: {:?}", e),
-    };
+    assert_eq!(
+        ValidatePathInfoError::InvalidNarSha256DigestLen(2),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
 /// Create a PathInfo with a wrong count of narinfo.reference_names,
 /// and ensure validation fails.
 #[test]
-fn validate_inconsistent_num_refs_fail() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_inconsistent_num_refs_fail() {
+    let mut path_info = PROTO_PATH_INFO.clone();
     path_info.narinfo.as_mut().unwrap().reference_names = vec![];
 
-    match path_info.validate().expect_err("must_fail") {
-        ValidatePathInfoError::InconsistentNumberOfReferences(1, 0) => {}
-        e => panic!("unexpected error: {:?}", e),
-    };
+    assert_eq!(
+        ValidatePathInfoError::InconsistentNumberOfReferences(1, 0),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
 /// Create a PathInfo with a wrong digest length in references.
 #[test]
-fn validate_invalid_reference_digest_len() {
-    let mut path_info = PATH_INFO_WITHOUT_NARINFO.clone();
+fn convert_invalid_reference_digest_len() {
+    let mut path_info = PROTO_PATH_INFO.clone();
     path_info.references.push(vec![0xff, 0xff].into());
 
-    match path_info.validate().expect_err("must fail") {
+    assert_eq!(
         ValidatePathInfoError::InvalidReferenceDigestLen(
             1, // position
             2, // unexpected digest len
-        ) => {}
-        e => panic!("unexpected error: {:?}", e),
-    };
+        ),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
 /// Create a PathInfo with a narinfo.reference_name[1] that is no valid store path.
 #[test]
-fn validate_invalid_narinfo_reference_name() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_invalid_narinfo_reference_name() {
+    let mut path_info = PROTO_PATH_INFO.clone();
 
     // This is invalid, as the store prefix is not part of reference_names.
     path_info.narinfo.as_mut().unwrap().reference_names[0] =
         "/nix/store/00000000000000000000000000000000-dummy".to_string();
 
-    match path_info.validate().expect_err("must fail") {
-        ValidatePathInfoError::InvalidNarinfoReferenceName(0, reference_name) => {
-            assert_eq!(
-                "/nix/store/00000000000000000000000000000000-dummy",
-                reference_name
-            );
-        }
-        e => panic!("unexpected error: {:?}", e),
-    }
+    assert_eq!(
+        ValidatePathInfoError::InvalidNarinfoReferenceName(
+            0,
+            "/nix/store/00000000000000000000000000000000-dummy".to_string()
+        ),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
 /// Create a PathInfo with a narinfo.reference_name[0] that doesn't match references[0].
 #[test]
-fn validate_inconsistent_narinfo_reference_name_digest() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_inconsistent_narinfo_reference_name_digest() {
+    let mut path_info = PROTO_PATH_INFO.clone();
 
     // mutate the first reference, they were all zeroes before
     path_info.references[0] = vec![0xff; store_path::DIGEST_SIZE].into();
 
-    match path_info.validate().expect_err("must fail") {
-        ValidatePathInfoError::InconsistentNarinfoReferenceNameDigest(0, e_expected, e_actual) => {
-            assert_eq!(path_info.references[0][..], e_expected[..]);
-            assert_eq!(DUMMY_PATH_DIGEST, e_actual);
-        }
-        e => panic!("unexpected error: {:?}", e),
-    }
-}
-
-/// Create a node with an empty symlink target, and ensure it fails validation.
-#[test]
-fn validate_symlink_empty_target_invalid() {
-    castorepb::Node {
-        node: Some(castorepb::node::Node::Symlink(castorepb::SymlinkNode {
-            name: "foo".into(),
-            target: "".into(),
-        })),
-    }
-    .into_name_and_node()
-    .expect_err("must fail validation");
-}
-
-/// Create a node with a symlink target including null bytes, and ensure it
-/// fails validation.
-#[test]
-fn validate_symlink_target_null_byte_invalid() {
-    castorepb::Node {
-        node: Some(castorepb::node::Node::Symlink(castorepb::SymlinkNode {
-            name: "foo".into(),
-            target: "foo\0".into(),
-        })),
-    }
-    .into_name_and_node()
-    .expect_err("must fail validation");
-}
-
-/// Create a PathInfo with a correct deriver field and ensure it succeeds.
-#[test]
-fn validate_valid_deriver() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
-
-    // add a valid deriver
-    let narinfo = path_info.narinfo.as_mut().unwrap();
-    narinfo.deriver = Some(crate::proto::StorePath {
-        name: "foo".to_string(),
-        digest: Bytes::from(DUMMY_PATH_DIGEST.as_slice()),
-    });
-
-    path_info.validate().expect("must validate");
+    assert_eq!(
+        ValidatePathInfoError::InconsistentNarinfoReferenceNameDigest(
+            0,
+            path_info.references[0][..].try_into().unwrap(),
+            DUMMY_PATH_DIGEST
+        ),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    )
 }
 
 /// Create a PathInfo with a broken deriver field and ensure it fails.
 #[test]
-fn validate_invalid_deriver() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_invalid_deriver() {
+    let mut path_info = PROTO_PATH_INFO.clone();
 
     // add a broken deriver (invalid digest)
     let narinfo = path_info.narinfo.as_mut().unwrap();
@@ -277,157 +229,8 @@ fn validate_invalid_deriver() {
         digest: vec![].into(),
     });
 
-    match path_info.validate().expect_err("must fail validation") {
-        ValidatePathInfoError::InvalidDeriverField(_) => {}
-        e => panic!("unexpected error: {:?}", e),
-    }
-}
-
-#[test]
-fn from_nixcompat_narinfo() {
-    let narinfo_parsed = nix_compat::narinfo::NarInfo::parse(
-        r#"StorePath: /nix/store/s66mzxpvicwk07gjbjfw9izjfa797vsw-hello-2.12.1
-URL: nar/1nhgq6wcggx0plpy4991h3ginj6hipsdslv4fd4zml1n707j26yq.nar.xz
-Compression: xz
-FileHash: sha256:1nhgq6wcggx0plpy4991h3ginj6hipsdslv4fd4zml1n707j26yq
-FileSize: 50088
-NarHash: sha256:0yzhigwjl6bws649vcs2asa4lbs8hg93hyix187gc7s7a74w5h80
-NarSize: 226488
-References: 3n58xw4373jp0ljirf06d8077j15pc4j-glibc-2.37-8 s66mzxpvicwk07gjbjfw9izjfa797vsw-hello-2.12.1
-Deriver: ib3sh3pcz10wsmavxvkdbayhqivbghlq-hello-2.12.1.drv
-Sig: cache.nixos.org-1:8ijECciSFzWHwwGVOIVYdp2fOIOJAfmzGHPQVwpktfTQJF6kMPPDre7UtFw3o+VqenC5P8RikKOAAfN7CvPEAg=="#).expect("must parse");
-
-    assert_eq!(
-        PathInfo {
-            node: None,
-            references: vec![
-                Bytes::copy_from_slice(&nixbase32::decode_fixed::<20>("3n58xw4373jp0ljirf06d8077j15pc4j").unwrap()),
-                Bytes::copy_from_slice(&nixbase32::decode_fixed::<20>("s66mzxpvicwk07gjbjfw9izjfa797vsw").unwrap()),
-            ],
-            narinfo: Some(
-                NarInfo {
-                    nar_size: 226488,
-                    nar_sha256: Bytes::copy_from_slice(
-                        &nixbase32::decode_fixed::<32>("0yzhigwjl6bws649vcs2asa4lbs8hg93hyix187gc7s7a74w5h80".as_bytes())
-                            .unwrap()
-                    ),
-                    signatures: vec![Signature {
-                        name: "cache.nixos.org-1".to_string(),
-                        data: BASE64.decode("8ijECciSFzWHwwGVOIVYdp2fOIOJAfmzGHPQVwpktfTQJF6kMPPDre7UtFw3o+VqenC5P8RikKOAAfN7CvPEAg==".as_bytes()).unwrap().into(),
-                    }],
-                    reference_names: vec![
-                        "3n58xw4373jp0ljirf06d8077j15pc4j-glibc-2.37-8".to_string(),
-                        "s66mzxpvicwk07gjbjfw9izjfa797vsw-hello-2.12.1".to_string()
-                    ],
-                    deriver: Some(crate::proto::StorePath {
-                        digest: Bytes::copy_from_slice(&nixbase32::decode_fixed::<20>("ib3sh3pcz10wsmavxvkdbayhqivbghlq").unwrap()),
-                        name: "hello-2.12.1".to_string(),
-                     }),
-                    ca: None,
-                }
-            )
-        },
-        (&narinfo_parsed).into(),
-    );
-}
-
-#[test]
-fn from_nixcompat_narinfo_fod() {
-    let narinfo_parsed = nix_compat::narinfo::NarInfo::parse(
-        r#"StorePath: /nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz
-URL: nar/1zjrhzhaizsrlsvdkqfl073vivmxcqnzkff4s50i0cdf541ary1r.nar.xz
-Compression: xz
-FileHash: sha256:1zjrhzhaizsrlsvdkqfl073vivmxcqnzkff4s50i0cdf541ary1r
-FileSize: 1033524
-NarHash: sha256:1lvqpbk2k1sb39z8jfxixf7p7v8sj4z6mmpa44nnmff3w1y6h8lh
-NarSize: 1033416
-References: 
-Deriver: dyivpmlaq2km6c11i0s6bi6mbsx0ylqf-hello-2.12.1.tar.gz.drv
-Sig: cache.nixos.org-1:ywnIG629nQZQhEr6/HLDrLT/mUEp5J1LC6NmWSlJRWL/nM7oGItJQUYWGLvYGhSQvHrhIuvMpjNmBNh/WWqCDg==
-CA: fixed:sha256:086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd"#
-    ).expect("must parse");
-
-    assert_eq!(
-        PathInfo {
-            node: None,
-            references: vec![],
-            narinfo: Some(
-                NarInfo {
-                    nar_size: 1033416,
-                    nar_sha256: Bytes::copy_from_slice(
-                        &nixbase32::decode_fixed::<32>(
-                            "1lvqpbk2k1sb39z8jfxixf7p7v8sj4z6mmpa44nnmff3w1y6h8lh"
-                        )
-                        .unwrap()
-                    ),
-                    signatures: vec![Signature {
-                        name: "cache.nixos.org-1".to_string(),
-                        data: BASE64
-                            .decode("ywnIG629nQZQhEr6/HLDrLT/mUEp5J1LC6NmWSlJRWL/nM7oGItJQUYWGLvYGhSQvHrhIuvMpjNmBNh/WWqCDg==".as_bytes())
-                            .unwrap()
-                            .into(),
-                    }],
-                    reference_names: vec![],
-                    deriver: Some(crate::proto::StorePath {
-                        digest: Bytes::copy_from_slice(
-                            &nixbase32::decode_fixed::<20>("dyivpmlaq2km6c11i0s6bi6mbsx0ylqf").unwrap()
-                        ),
-                        name: "hello-2.12.1.tar.gz".to_string(),
-                    }),
-                    ca: Some(crate::proto::nar_info::Ca {
-                        r#type: crate::proto::nar_info::ca::Hash::FlatSha256.into(),
-                        digest: Bytes::copy_from_slice(
-                            &nixbase32::decode_fixed::<32>(
-                                "086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd"
-                            )
-                            .unwrap()
-                        )
-                    }),
-                }
-            ),
-        },
-        (&narinfo_parsed).into()
-    );
-}
-
-/// Exercise .as_narinfo() on a PathInfo and ensure important fields are preserved..
-#[test]
-fn as_narinfo() {
-    let narinfo_parsed = nix_compat::narinfo::NarInfo::parse(
-        r#"StorePath: /nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz
-URL: nar/1zjrhzhaizsrlsvdkqfl073vivmxcqnzkff4s50i0cdf541ary1r.nar.xz
-Compression: xz
-FileHash: sha256:1zjrhzhaizsrlsvdkqfl073vivmxcqnzkff4s50i0cdf541ary1r
-FileSize: 1033524
-NarHash: sha256:1lvqpbk2k1sb39z8jfxixf7p7v8sj4z6mmpa44nnmff3w1y6h8lh
-NarSize: 1033416
-References: 
-Deriver: dyivpmlaq2km6c11i0s6bi6mbsx0ylqf-hello-2.12.1.tar.gz.drv
-Sig: cache.nixos.org-1:ywnIG629nQZQhEr6/HLDrLT/mUEp5J1LC6NmWSlJRWL/nM7oGItJQUYWGLvYGhSQvHrhIuvMpjNmBNh/WWqCDg==
-CA: fixed:sha256:086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd"#
-    ).expect("must parse");
-
-    let path_info: PathInfo = (&narinfo_parsed).into();
-
-    let mut narinfo_returned = path_info
-        .to_narinfo(
-            StorePathRef::from_bytes(b"pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz")
-                .expect("invalid storepath"),
-        )
-        .expect("must be some");
-    narinfo_returned.url = "some.nar";
-
     assert_eq!(
-        r#"StorePath: /nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz
-URL: some.nar
-Compression: none
-NarHash: sha256:1lvqpbk2k1sb39z8jfxixf7p7v8sj4z6mmpa44nnmff3w1y6h8lh
-NarSize: 1033416
-References: 
-Deriver: dyivpmlaq2km6c11i0s6bi6mbsx0ylqf-hello-2.12.1.tar.gz.drv
-Sig: cache.nixos.org-1:ywnIG629nQZQhEr6/HLDrLT/mUEp5J1LC6NmWSlJRWL/nM7oGItJQUYWGLvYGhSQvHrhIuvMpjNmBNh/WWqCDg==
-CA: fixed:sha256:086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd
-"#,
-        narinfo_returned.to_string(),
-    );
+        ValidatePathInfoError::InvalidDeriverField(store_path::Error::InvalidLength),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    )
 }
diff --git a/tvix/store/src/tests/fixtures.rs b/tvix/store/src/tests/fixtures.rs
index 48cc365365a9..91628f2fee79 100644
--- a/tvix/store/src/tests/fixtures.rs
+++ b/tvix/store/src/tests/fixtures.rs
@@ -1,24 +1,27 @@
+use crate::pathinfoservice::PathInfo;
 use lazy_static::lazy_static;
+use nix_compat::nixhash::{CAHash, NixHash};
+use nix_compat::store_path::StorePath;
 use rstest::{self, *};
 use rstest_reuse::*;
 use std::io;
 use std::sync::Arc;
-pub use tvix_castore::fixtures::*;
+use tvix_castore::fixtures::{
+    DIRECTORY_COMPLICATED, DIRECTORY_WITH_KEEP, DUMMY_DIGEST, EMPTY_BLOB_CONTENTS,
+    EMPTY_BLOB_DIGEST, HELLOWORLD_BLOB_CONTENTS, HELLOWORLD_BLOB_DIGEST,
+};
 use tvix_castore::{
     blobservice::{BlobService, MemoryBlobService},
     directoryservice::{DirectoryService, MemoryDirectoryService},
-    proto as castorepb, Node,
-};
-
-use crate::proto::{
-    nar_info::{ca, Ca},
-    NarInfo, PathInfo,
+    Node,
 };
 
-pub const DUMMY_PATH: &str = "00000000000000000000000000000000-dummy";
+pub const DUMMY_PATH_STR: &str = "00000000000000000000000000000000-dummy";
 pub const DUMMY_PATH_DIGEST: [u8; 20] = [0; 20];
 
 lazy_static! {
+    pub static ref DUMMY_PATH: StorePath<String> = StorePath::from_name_and_digest_fixed("dummy", DUMMY_PATH_DIGEST).unwrap();
+
     pub static ref CASTORE_NODE_SYMLINK: Node = Node::Symlink {
         target: "/nix/store/somewhereelse".try_into().unwrap(),
     };
@@ -130,32 +133,19 @@ lazy_static! {
         1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
     ];
 
-    /// A PathInfo message without .narinfo populated.
-    pub static ref PATH_INFO_WITHOUT_NARINFO : PathInfo = PathInfo {
-        node: Some(castorepb::Node {
-            node: Some(castorepb::node::Node::Directory(castorepb::DirectoryNode {
-                name: DUMMY_PATH.into(),
-                digest: DUMMY_DIGEST.clone().into(),
-                size: 0,
-            })),
-        }),
-        references: vec![DUMMY_PATH_DIGEST.as_slice().into()],
-        narinfo: None,
-    };
-
-    /// A PathInfo message with .narinfo populated.
-    /// The references in `narinfo.reference_names` aligns with what's in
-    /// `references`.
-    pub static ref PATH_INFO_WITH_NARINFO : PathInfo = PathInfo {
-        narinfo: Some(NarInfo {
-            nar_size: 0,
-            nar_sha256: DUMMY_DIGEST.clone().into(),
-            signatures: vec![],
-            reference_names: vec![DUMMY_PATH.to_string()],
-            deriver: None,
-            ca: Some(Ca { r#type: ca::Hash::NarSha256.into(), digest:  DUMMY_DIGEST.clone().into() })
-        }),
-      ..PATH_INFO_WITHOUT_NARINFO.clone()
+    /// A PathInfo message
+    pub static ref PATH_INFO: PathInfo = PathInfo {
+        store_path: DUMMY_PATH.clone(),
+        node: tvix_castore::Node::Directory {
+            digest: DUMMY_DIGEST.clone(),
+            size: 0,
+        },
+        references: vec![DUMMY_PATH.clone()],
+        nar_sha256: [0; 32],
+        nar_size: 0,
+        signatures: vec![],
+        deriver: None,
+        ca: Some(CAHash::Nar(NixHash::Sha256([0; 32]))),
     };
 }