Browse Source

Added a new crate called btrun for implementing an actor runtime.

Matthew Carr 1 year ago
parent
commit
e2ff1d4d96

+ 44 - 0
Cargo.lock

@@ -402,7 +402,11 @@ dependencies = [
 name = "btlib-tests"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "btlib",
+ "btserde",
+ "lazy_static",
+ "serde",
  "swtpm-harness",
  "tempdir",
 ]
@@ -444,6 +448,23 @@ dependencies = [
  "termion",
 ]
 
+[[package]]
+name = "btrun"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "btlib",
+ "btlib-tests",
+ "btmsg",
+ "btserde",
+ "bytes",
+ "futures",
+ "serde",
+ "strum",
+ "tokio",
+ "uuid",
+]
+
 [[package]]
 name = "btserde"
 version = "0.1.0"
@@ -3074,6 +3095,29 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
 
+[[package]]
+name = "uuid"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
+dependencies = [
+ "getrandom",
+ "rand 0.8.5",
+ "serde",
+ "uuid-macro-internal",
+]
+
+[[package]]
+name = "uuid-macro-internal"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f67b459f42af2e6e1ee213cb9da4dbd022d3320788c3fb3e1b893093f1e45da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.13",
+]
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"

+ 4 - 2
crates/btlib-tests/Cargo.toml

@@ -7,7 +7,9 @@ edition = "2021"
 
 [dependencies]
 btlib = { path = "../btlib" }
+btserde = { path = "../btserde" }
 swtpm-harness = { path = "../swtpm-harness" }
-
-[dev-dependencies]
 tempdir = "0.3.7"
+anyhow = { version = "1.0.66", features = ["std", "backtrace"] }
+serde = { version = "^1.0.136", features = ["derive"] }
+lazy_static = { version = "1.4.0" }

+ 124 - 0
crates/btlib-tests/src/cred_store_tests.rs

@@ -0,0 +1,124 @@
+//! Contains the macro [cred_store_test_case] which expands to a collection of test cases for
+//! a credential store implementation.
+
+/// Expands to a set of test cases for a credential store implementation.
+///
+/// This macro accepts a single argument which is an expression which creates a new instance of the
+/// credential store being tested.
+#[macro_export]
+macro_rules! cred_store_test_cases {
+    ($ctor:expr) => {
+        use btlib::{
+            crypto::{Creds, CredsPriv, Error},
+            Epoch, Principaled,
+        };
+        use btserde::to_vec;
+        use std::time::Duration;
+
+        #[test]
+        fn create_new() {
+            let _ = $ctor;
+        }
+
+        #[test]
+        fn node_creds() {
+            let case = $ctor;
+
+            let result = case.node_creds();
+
+            assert!(result.is_ok());
+        }
+
+        #[test]
+        fn root_creds_returns_same_as_gen_root_creds() {
+            const PASSWORD: &str = "MaximalIrony";
+            let case = $ctor;
+
+            let expected = case.gen_root_creds(PASSWORD).unwrap();
+            let actual = case.root_creds(PASSWORD).unwrap();
+
+            assert!(std::ptr::eq(expected.as_ref(), actual.as_ref()));
+        }
+
+        #[test]
+        fn root_creds_wrong_password_is_error() {
+            let case = $ctor;
+
+            case.gen_root_creds("right").unwrap();
+            let result = case.root_creds("wrong");
+
+            let passed = if let Some(err) = result.err() {
+                if let Some(Error::WrongRootPassword) = err.downcast_ref::<Error>() {
+                    true
+                } else {
+                    false
+                }
+            } else {
+                false
+            };
+            assert!(passed);
+        }
+
+        #[test]
+        fn storage_key() {
+            let case = $ctor;
+
+            let result = case.storage_key();
+
+            assert!(result.is_ok());
+        }
+
+        #[test]
+        fn export_import_root_creds() {
+            const SRC_PASSWORD: &str = "FALLING_MAN";
+            const DST_PASSWORD: &str = "RUNNING_MAN";
+            let src = $ctor;
+            let dst = $ctor;
+
+            let expected = src.gen_root_creds(SRC_PASSWORD).unwrap();
+            let previous = dst.gen_root_creds(DST_PASSWORD).unwrap();
+            let storage_key = dst.storage_key().unwrap();
+            let exported = src
+                .export_root_creds(&expected, SRC_PASSWORD, &storage_key)
+                .unwrap();
+            let actual = dst.import_root_creds(SRC_PASSWORD, exported).unwrap();
+
+            assert!(!std::ptr::eq(previous.as_ref(), actual.as_ref()));
+            assert!(to_vec(expected.as_ref()).unwrap() == to_vec(actual.as_ref()).unwrap());
+        }
+
+        #[test]
+        fn import_root_creds_wrong_password_is_error() {
+            const RIGHT_PW: &str = "right";
+            const WRONG_PW: &str = "wrong";
+            let src = $ctor;
+            let dst = $ctor;
+
+            let root_creds = src.gen_root_creds("right").unwrap();
+            let storage_key = dst.storage_key().unwrap();
+            let exported = src
+                .export_root_creds(&root_creds, RIGHT_PW, &storage_key)
+                .unwrap();
+            let result = dst.import_root_creds(WRONG_PW, exported);
+
+            assert!(result.is_err());
+        }
+
+        #[test]
+        fn assign_node_writecap() {
+            let case = $ctor;
+
+            let mut node_creds = case.node_creds().unwrap();
+            let root_creds = case.gen_root_creds("password").unwrap();
+            let expires = Epoch::now() + Duration::from_secs(3600);
+            let expected = root_creds
+                .issue_writecap(node_creds.principal(), &mut std::iter::empty(), expires)
+                .unwrap();
+            case.assign_node_writecap(&mut node_creds, expected.clone())
+                .unwrap();
+            let actual = node_creds.writecap().unwrap();
+
+            assert_eq!(&expected, actual);
+        }
+    };
+}

+ 181 - 0
crates/btlib-tests/src/in_mem_cred_store.rs

@@ -0,0 +1,181 @@
+//! This module contains an in-memory implementation of a [CredStore] called [InMemCredStore].
+//! A static instance of this struct called [TEST_STORE] can be used to get credentials during
+//! testing. The root password for this credential store is defined in [TEST_STORE_ROOT_PASSWORD].
+
+use std::{
+    sync::{Arc, RwLock},
+    time::Duration,
+};
+
+use btlib::{
+    bterr,
+    crypto::{AsymKeyPub, ConcreteCreds, CredStore, CredStoreMut, Creds, Encrypt, Error},
+    error::DisplayErr,
+    Epoch, Result,
+};
+use lazy_static::lazy_static;
+use serde::{Deserialize, Serialize};
+
+/// The root password to use with the [TEST_STORE] static credential store.
+pub const TEST_STORE_ROOT_PASSWORD: &str = "shubawumba";
+
+lazy_static! {
+    /// A static instance of [InMemCredStore] for use in tests.
+    pub static ref TEST_STORE: InMemCredStore = {
+        let store = InMemCredStore::new().unwrap();
+        let expires = Epoch::now() + Duration::from_secs(7200);
+        let root_creds = store.provision_root(TEST_STORE_ROOT_PASSWORD, expires).unwrap();
+        let node_principal = store.provision_node_start().unwrap();
+        let writecap = root_creds
+            .issue_writecap(node_principal, &mut std::iter::empty(), expires)
+            .unwrap();
+        store.provision_node_finish(writecap).unwrap();
+        store
+    };
+}
+
+/// A credential store that keeps all credential in memory. Thus these credentials will be
+/// lost when this struct is dropped.
+pub struct InMemCredStore {
+    node_creds: RwLock<Arc<ConcreteCreds>>,
+    root_creds: RwLock<Option<RootInfo>>,
+    storage_key: AsymKeyPub<Encrypt>,
+}
+
+impl InMemCredStore {
+    pub fn new() -> Result<Self> {
+        let node_creds = ConcreteCreds::generate()?;
+        let root_creds = RwLock::new(None);
+        // Using the node credentials like this is bad practice, but for testing it's fine.
+        // For production code a separate key pair for storage must be used.
+        let storage_key = node_creds.encrypt_pair().public().clone();
+        let node_creds = RwLock::new(Arc::new(node_creds));
+        Ok(Self {
+            node_creds,
+            root_creds,
+            storage_key,
+        })
+    }
+}
+
+impl CredStore for InMemCredStore {
+    type CredHandle = Arc<ConcreteCreds>;
+    type ExportedCreds = ExportedCreds;
+
+    fn node_creds(&self) -> btlib::Result<Self::CredHandle> {
+        let guard = self.node_creds.read().display_err()?;
+        Ok(guard.clone())
+    }
+
+    fn root_creds(&self, password: &str) -> btlib::Result<Self::CredHandle> {
+        let guard = self.root_creds.read().display_err()?;
+        if let Some(info) = guard.as_ref() {
+            if info.password == password {
+                Ok(info.creds.clone())
+            } else {
+                Err(bterr!(Error::WrongRootPassword))
+            }
+        } else {
+            Err(bterr!("root credentials have not been generated"))
+        }
+    }
+
+    fn storage_key(&self) -> Result<AsymKeyPub<Encrypt>> {
+        Ok(self.storage_key.clone())
+    }
+
+    fn export_root_creds(
+        &self,
+        root_creds: &Self::CredHandle,
+        _password: &str,
+        _new_parent: &AsymKeyPub<Encrypt>,
+    ) -> Result<Self::ExportedCreds> {
+        Ok(ExportedCreds {
+            password: _password.to_string(),
+            creds: root_creds.as_ref().clone(),
+        })
+    }
+}
+
+impl CredStoreMut for InMemCredStore {
+    fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle> {
+        {
+            let guard = self.root_creds.read().display_err()?;
+            if guard.is_some() {
+                return Err(bterr!("root creds have already been generated"));
+            }
+        }
+
+        let mut guard = self.root_creds.write().display_err()?;
+        let creds = Arc::new(ConcreteCreds::generate()?);
+        *guard = Some(RootInfo {
+            password: password.to_owned(),
+            creds: creds.clone(),
+        });
+        Ok(creds)
+    }
+
+    fn import_root_creds(
+        &self,
+        password: &str,
+        exported: Self::ExportedCreds,
+    ) -> Result<Self::CredHandle> {
+        if exported.password != password {
+            return Err(Error::WrongRootPassword.into());
+        }
+        let creds = Arc::new(exported.creds);
+        let mut guard = self.root_creds.write().display_err()?;
+        *guard = Some(RootInfo {
+            password: password.to_owned(),
+            creds: creds.clone(),
+        });
+        Ok(creds)
+    }
+
+    fn assign_node_writecap(
+        &self,
+        handle: &mut Self::CredHandle,
+        writecap: btlib::Writecap,
+    ) -> Result<()> {
+        let creds = Arc::make_mut(handle);
+        creds.set_writecap(writecap)?;
+        let mut guard = self.node_creds.write().display_err()?;
+        *guard = handle.clone();
+        Ok(())
+    }
+
+    fn assign_root_writecap(
+        &self,
+        handle: &mut Self::CredHandle,
+        writecap: btlib::Writecap,
+    ) -> Result<()> {
+        let creds = Arc::make_mut(handle);
+        creds.set_writecap(writecap)?;
+        let mut guard = self.root_creds.write().display_err()?;
+        if let Some(info) = guard.as_mut() {
+            info.creds = handle.clone();
+            Ok(())
+        } else {
+            Err(bterr!("no root creds have been generated"))
+        }
+    }
+}
+
+struct RootInfo {
+    password: String,
+    creds: Arc<ConcreteCreds>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct ExportedCreds {
+    password: String,
+    creds: ConcreteCreds,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::cred_store_test_cases;
+
+    cred_store_test_cases!(InMemCredStore::new().unwrap());
+}

+ 5 - 0
crates/btlib-tests/src/lib.rs

@@ -6,3 +6,8 @@ pub mod fs_queries;
 
 mod cred_store_testing_ext;
 pub use cred_store_testing_ext::CredStoreTestingExt;
+
+mod in_mem_cred_store;
+pub use in_mem_cred_store::{InMemCredStore, TEST_STORE, TEST_STORE_ROOT_PASSWORD};
+
+pub mod cred_store_tests;

+ 36 - 0
crates/btlib/src/block_path.rs

@@ -123,6 +123,18 @@ mod private {
 
     impl Eq for BlockPath {}
 
+    impl PartialOrd for BlockPath {
+        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+            self.deref().partial_cmp(other.deref())
+        }
+    }
+
+    impl Ord for BlockPath {
+        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+            self.deref().cmp(other.deref())
+        }
+    }
+
     impl<T: AsRef<str>> PartialEq<T> for BlockPath {
         fn eq(&self, other: &T) -> bool {
             self.deref().eq(&other.as_ref())
@@ -190,6 +202,18 @@ mod private {
 
     impl<'a> Eq for BlockPathRef<'a> {}
 
+    impl<'a> PartialOrd for BlockPathRef<'a> {
+        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+            self.deref().partial_cmp(other.deref())
+        }
+    }
+
+    impl<'a> Ord for BlockPathRef<'a> {
+        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+            self.deref().cmp(other.deref())
+        }
+    }
+
     impl<'a, T: AsRef<str>> PartialEq<T> for BlockPathRef<'a> {
         fn eq(&self, other: &T) -> bool {
             self.deref().eq(&other.as_ref())
@@ -460,6 +484,18 @@ mod private {
 
     impl<T: AsRef<str>> Eq for BlockPathGen<T> {}
 
+    impl<T: AsRef<str>> PartialOrd for BlockPathGen<T> {
+        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+            self.path.as_ref().partial_cmp(other.path.as_ref())
+        }
+    }
+
+    impl<T: AsRef<str>> Ord for BlockPathGen<T> {
+        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+            self.path.as_ref().cmp(other.path.as_ref())
+        }
+    }
+
     impl<T: AsRef<str>, U: AsRef<str>> PartialEq<U> for BlockPathGen<T> {
         fn eq(&self, other: &U) -> bool {
             self.path.as_ref() == other.as_ref()

+ 7 - 0
crates/btlib/src/crypto.rs

@@ -117,6 +117,11 @@ pub enum Error {
     OpAlreadyFinished,
     /// Indicates that a writecap was not issued to a particular [Principal].
     NotIssuedTo,
+    /// Indicates that no root credentials were present in a [CredStore].
+    NoRootCreds,
+    /// Indicates that the wrong root password was given when attempting to access the root
+    /// credentials.
+    WrongRootPassword,
 }
 
 impl Error {
@@ -170,6 +175,8 @@ impl Display for Error {
             Error::Library(err) => err.fmt(f),
             Error::OpAlreadyFinished => write!(f, "operation is already finished"),
             Error::NotIssuedTo => write!(f, "writecap was not issued to the given principal"),
+            Error::NoRootCreds => write!(f, "root creds are not present"),
+            Error::WrongRootPassword => write!(f, "incorrect root password"),
         }
     }
 }

+ 4 - 25
crates/btlib/src/crypto/file_cred_store.rs

@@ -2,17 +2,17 @@ use crate::{
     atomic_file::{mask, AtomicFile, TryDefault},
     crypto::{
         AsymKeyPair, AsymKeyPub, ConcreteCreds, CredStore, CredStoreMut, DerivationParams, Encrypt,
-        Envelope, Scheme, TaggedCiphertext,
+        Envelope, Error, Scheme, TaggedCiphertext,
     },
     error::DisplayErr,
     Result,
 };
 use btserde::field_helpers;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use std::{fmt::Display, path::PathBuf, sync::Arc};
+use std::{path::PathBuf, sync::Arc};
 use zeroize::{Zeroize, Zeroizing};
 
-pub use private::{Error, FileCredStore};
+pub use private::FileCredStore;
 
 mod private {
     use super::*;
@@ -51,27 +51,6 @@ mod private {
         Ok(opt.map(Zeroizing::new))
     }
 
-    /// Errors which can occur when using a [FileCredStore].
-    #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
-    pub enum Error {
-        /// Indicates that no root credentials were present in a [CredStore].
-        NoRootCreds,
-        /// Indicates that the wrong root password was given when attempting to access the root
-        /// credentials.
-        WrongRootPassword,
-    }
-
-    impl Display for Error {
-        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-            match self {
-                Error::NoRootCreds => write!(f, "root creds are not present"),
-                Error::WrongRootPassword => write!(f, "incorrect root password"),
-            }
-        }
-    }
-
-    impl std::error::Error for Error {}
-
     #[derive(Serialize, Deserialize)]
     struct State {
         #[serde(with = "field_helpers::smart_ptr")]
@@ -310,7 +289,7 @@ mod test {
     }
 
     #[test]
-    fn gen_root_creds_and_root_creds() {
+    fn root_creds_returns_same_as_gen_root_creds() {
         const PASSWORD: &str = "MaximalIrony";
         let case = TestCase::new();
 

+ 21 - 0
crates/btrun/Cargo.toml

@@ -0,0 +1,21 @@
+[package]
+name = "btrun"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+btlib = { path = "../btlib" }
+btmsg = { path = "../btmsg" }
+btserde = { path = "../btserde" }
+tokio = { version = "1.23.0", features = ["rt"] }
+futures = "0.3.25"
+serde = { version = "^1.0.136", features = ["derive"] }
+uuid = { version = "1.3.3", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] }
+anyhow = { version = "1.0.66", features = ["std", "backtrace"] }
+bytes = "1.3.0"
+strum = { version = "^0.24.0", features = ["derive"] }
+
+[dev-dependencies]
+btlib-tests = { path = "../btlib-tests" }

+ 409 - 0
crates/btrun/src/lib.rs

@@ -0,0 +1,409 @@
+#![feature(impl_trait_in_assoc_type)]
+
+use std::{any::Any, collections::HashMap, future::Future, net::IpAddr, pin::Pin, sync::Arc};
+
+use btlib::{bterr, crypto::Creds, BlockPath, Result};
+use btmsg::{MsgCallback, Receiver, Replier, Transmitter};
+use btserde::field_helpers::smart_ptr;
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use tokio::{
+    sync::{mpsc, oneshot, RwLock},
+    task::JoinHandle,
+};
+use uuid::Uuid;
+
+/// Creates a new [Runtime] instance which listens at the given IP address and which uses the given
+/// credentials.
+pub fn new_runtime<C: 'static + Send + Sync + Creds>(
+    ip_addr: IpAddr,
+    creds: Arc<C>,
+) -> Result<Runtime<impl Receiver>> {
+    let path = Arc::new(creds.bind_path()?);
+    let handles = Arc::new(RwLock::new(HashMap::new()));
+    let callback = RuntimeCallback::new(handles.clone());
+    let rx = btmsg::receiver(ip_addr, creds, callback)?;
+    Ok(Runtime {
+        _rx: rx,
+        path,
+        handles,
+        peers: RwLock::new(HashMap::new()),
+    })
+}
+
+/// An actor runtime.
+/// 
+/// Actors can be activated by the runtime and execute autonomously until they halt. Running actors
+/// can be sent messages using the `send` method, which does not wait for a response from the
+/// recipient. If a reply is needed, then `call` can be used, which returns a future that will not
+/// be ready until the reply has been received.
+pub struct Runtime<Rx: Receiver> {
+    _rx: Rx,
+    path: Arc<BlockPath>,
+    handles: Arc<RwLock<HashMap<Uuid, ActorHandle>>>,
+    peers: RwLock<HashMap<Arc<BlockPath>, Rx::Transmitter>>,
+}
+
+macro_rules! deliver {
+    ($self:expr, $to:expr, $msg:expr, $method:ident) => {
+        if $to.path == $self.path {
+            let guard = $self.handles.read().await;
+            if let Some(handle) = guard.get(&$to.act_id) {
+                handle.$method($msg).await
+            } else {
+                Err(bterr!("invalid actor name"))
+            }
+        } else {
+            let guard = $self.peers.read().await;
+            if let Some(peer) = guard.get(&$to.path) {
+                peer.$method(Adapter($msg)).await
+            } else {
+                // TODO: Use the filesystem to discover the address of the recipient and connect to
+                // it.
+                todo!()
+            }
+        }
+    };
+}
+
+impl<Rx: Receiver> Runtime<Rx> {
+    pub fn path(&self) -> &Arc<BlockPath> {
+        &self.path
+    }
+
+    /// Sends a message to the actor identified by the given [ActorName].
+    pub async fn send<T: 'static + SendMsg>(&self, to: &ActorName, msg: T) -> Result<()> {
+        deliver!(self, to, msg, send)
+    }
+
+    /// Sends a message to the actor identified by the given [ActorName] and returns a future which
+    /// is ready when the reply has been received.
+    pub async fn call<T: 'static + CallMsg>(&self, to: &ActorName, msg: T) -> Result<T::Reply> {
+        deliver!(self, to, msg, call_through)
+    }
+
+    /// Resolves the given [ServiceName] to an [ActorName] which is part of it.
+    pub async fn resolve<'a>(&'a self, _service: &ServiceName) -> Result<ActorName> {
+        todo!()
+    }
+
+    /// Activates a new actor using the given activator function and returns a handle to it.
+    pub async fn activate<Msg, F, Fut, G>(&self, activator: F, deserializer: G) -> ActorName
+    where
+        Msg: 'static + CallMsg,
+        Fut: 'static + Send + Future<Output = ()>,
+        F: FnOnce(mpsc::Receiver<Envelope<Msg>>, Uuid) -> Fut,
+        G: 'static + Send + Sync + Fn(&[u8]) -> Result<Msg>,
+    {
+        let mut guard = self.handles.write().await;
+        let act_id = {
+            let mut act_id = Uuid::new_v4();
+            while guard.contains_key(&act_id) {
+                act_id = Uuid::new_v4();
+            }
+            act_id
+        };
+        let (tx, rx) = mpsc::channel::<Envelope<Msg>>(MAILBOX_LIMIT);
+        // The deliverer closure is responsible for deserializing messages received over the wire
+        // and delivering them to the actor's mailbox. It's also responsible for sending replies to
+        // call messages.
+        let deliverer = {
+            let tx = tx.clone();
+            move |envelope: WireEnvelope| {
+                let (msg, replier) = envelope.into_parts();
+                let result = deserializer(msg);
+                let tx_clone = tx.clone();
+                let fut: FutureResult = Box::pin(async move {
+                    let msg = result?;
+                    if let Some(mut replier) = replier {
+                        let (envelope, rx) = Envelope::call(msg);
+                        tx_clone.send(envelope).await.map_err(|_| {
+                            bterr!("failed to deliver message. Recipient may have halted.")
+                        })?;
+                        // TODO: `reply` does not have the right type.
+                        // It needs to be WireEnvelope::Reply.
+                        match rx.await {
+                            Ok(reply) => replier.reply(reply).await,
+                            Err(err) => replier.reply_err(err.to_string(), None).await,
+                        }
+                    } else {
+                        tx_clone.send(Envelope::Send { msg }).await.map_err(|_| {
+                            bterr!("failed to deliver message. Recipient may have halted.")
+                        })
+                    }
+                });
+                fut
+            }
+        };
+        let handle = tokio::task::spawn(activator(rx, act_id));
+        let actor_handle = ActorHandle::new(handle, tx, deliverer);
+        guard.insert(act_id, actor_handle);
+        ActorName::new(self.path.clone(), act_id)
+    }
+
+    /// Registers an actor as a service with the given [ServiceId].
+    pub async fn register<Msg, Fut, F, G>(&self, _id: ServiceId, _activator: F, _deserializer: G) -> Result<()>
+    where
+        Msg: 'static + CallMsg,
+        Fut: 'static + Send + Future<Output = ()>,
+        F: Fn(mpsc::Receiver<Envelope<Msg>>, Uuid) -> Fut,
+        G: 'static + Send + Sync + Fn(&[u8]) -> Result<Msg>,
+    {
+        todo!()
+    }
+}
+
+/// A unique identifier for a particular service.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
+pub struct ServiceId(#[serde(with = "smart_ptr")] Arc<String>);
+
+impl From<String> for ServiceId {
+    fn from(value: String) -> Self {
+        Self(Arc::new(value))
+    }
+}
+
+impl<'a> From<&'a str> for ServiceId {
+    fn from(value: &'a str) -> Self {
+        Self(Arc::new(value.to_owned()))
+    }
+}
+
+/// A unique identifier for a service.
+///
+/// A service is a collection of actors in the same directory which provide some functionality.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
+pub struct ServiceName {
+    /// The path to the directory containing the service.
+    #[serde(with = "smart_ptr")]
+    path: Arc<BlockPath>,
+    /// The id of the service.
+    service_id: ServiceId,
+}
+
+/// A unique identifier for a specific actor activation.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
+pub struct ActorName {
+    /// The path to the directory containing this actor.
+    #[serde(with = "smart_ptr")]
+    path: Arc<BlockPath>,
+    /// A unique identifier for an actor activation. Even as an actor transitions to different types
+    /// as it handles messages, this value does not change. Thus this value can be used to trace an
+    /// actor through a series of state transitions.
+    act_id: Uuid,
+}
+
+impl ActorName {
+    pub fn new(path: Arc<BlockPath>, act_id: Uuid) -> Self {
+        Self { path, act_id }
+    }
+}
+
+/// Trait for messages which expect exactly one reply.
+pub trait CallMsg: Serialize + DeserializeOwned + Send + Sync {
+    /// The reply type expected for this message.
+    type Reply: Serialize + DeserializeOwned + Send + Sync;
+}
+
+/// Trait for messages which expect exactly zero replies.
+pub trait SendMsg: CallMsg {}
+
+/// An adapter which allows a [CallMsg] to be used sent remotely using a [Transmitter].
+#[derive(Serialize, Deserialize)]
+#[repr(transparent)]
+#[serde(transparent)]
+struct Adapter<T>(T);
+
+impl<'a, T: CallMsg> btmsg::CallMsg<'a> for Adapter<T> {
+    type Reply<'r> = T::Reply;
+}
+
+impl<'a, T: SendMsg> btmsg::SendMsg<'a> for Adapter<T> {}
+
+/// The maximum number of messages which can be kept in an actor's mailbox.
+const MAILBOX_LIMIT: usize = 32;
+
+/// The type of messages sent remotely.
+enum WireEnvelope<'de> {
+    Send { msg: &'de [u8] },
+    Call { msg: &'de [u8], replier: Replier },
+}
+
+impl<'de> WireEnvelope<'de> {
+    fn into_parts(self) -> (&'de [u8], Option<Replier>) {
+        match self {
+            Self::Send { msg } => (msg, None),
+            Self::Call { msg, replier } => (msg, Some(replier)),
+        }
+    }
+}
+
+/// Wraps a message to indicate if it was sent with `call` or `send`.
+/// 
+/// If the message was sent with call, then this enum will contain a channel that can be used to
+/// reply to it.
+pub enum Envelope<T: CallMsg> {
+    /// The message was sent with `send` and does not expect a reply.
+    Send { msg: T },
+    /// The message was sent with `call` and expects a reply.
+    Call {
+        msg: T,
+        /// A reply message must be sent using this channel.
+        reply: oneshot::Sender<T::Reply>,
+    },
+}
+
+impl<T: CallMsg> Envelope<T> {
+    fn send(msg: T) -> Self {
+        Self::Send { msg }
+    }
+
+    fn call(msg: T) -> (Self, oneshot::Receiver<T::Reply>) {
+        let (tx, rx) = oneshot::channel::<T::Reply>();
+        let kind = Envelope::Call { msg, reply: tx };
+        (kind, rx)
+    }
+}
+
+type FutureResult = Pin<Box<dyn Send + Future<Output = Result<()>>>>;
+
+struct ActorHandle {
+    _handle: JoinHandle<()>,
+    sender: Box<dyn Send + Sync + Any>,
+    deliverer: Box<dyn Send + Sync + Fn(WireEnvelope<'_>) -> FutureResult>,
+}
+
+impl ActorHandle {
+    fn new<T, F>(handle: JoinHandle<()>, sender: mpsc::Sender<Envelope<T>>, deliverer: F) -> Self
+    where
+        T: 'static + CallMsg,
+        F: 'static + Send + Sync + Fn(WireEnvelope<'_>) -> FutureResult,
+    {
+        Self {
+            _handle: handle,
+            sender: Box::new(sender),
+            deliverer: Box::new(deliverer),
+        }
+    }
+
+    fn sender<T: 'static + CallMsg>(&self) -> Result<&mpsc::Sender<Envelope<T>>> {
+        self.sender
+            .downcast_ref::<mpsc::Sender<Envelope<T>>>()
+            .ok_or_else(|| bterr!("unexpected message type"))
+    }
+
+    async fn send<T: 'static + SendMsg>(&self, msg: T) -> Result<()> {
+        let sender = self.sender()?;
+        sender
+            .send(Envelope::send(msg))
+            .await
+            .map_err(|_| bterr!("failed to enqueue message"))?;
+        Ok(())
+    }
+
+    async fn call_through<T: 'static + CallMsg>(&self, msg: T) -> Result<T::Reply> {
+        let sender = self.sender()?;
+        let (envelope, rx) = Envelope::call(msg);
+        sender
+            .send(envelope)
+            .await
+            .map_err(|_| bterr!("failed to enqueue call"))?;
+        let reply = rx.await?;
+        Ok(reply)
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+enum WireReply<'a> {
+    Ok(&'a [u8]),
+    Err(&'a str),
+}
+
+#[derive(Serialize, Deserialize)]
+struct WireMsg<'a> {
+    act_id: Uuid,
+    payload: &'a [u8],
+}
+
+impl<'a> btmsg::CallMsg<'a> for WireMsg<'a> {
+    type Reply<'r> = WireReply<'r>;
+}
+
+/// This struct implements the server callback for network messages.
+#[derive(Clone)]
+struct RuntimeCallback {
+    handles: Arc<RwLock<HashMap<Uuid, ActorHandle>>>,
+}
+
+impl RuntimeCallback {
+    fn new(handles: Arc<RwLock<HashMap<Uuid, ActorHandle>>>) -> Self {
+        Self { handles }
+    }
+}
+
+impl MsgCallback for RuntimeCallback {
+    type Arg<'de> = WireMsg<'de>;
+    type CallFut<'de> = impl 'de + Future<Output = Result<()>>;
+    fn call<'de>(&'de self, mut arg: btmsg::MsgReceived<Self::Arg<'de>>) -> Self::CallFut<'de> {
+        async move {
+            let replier = arg.take_replier();
+            let body = arg.body();
+            let guard = self.handles.read().await;
+            if let Some(handle) = guard.get(&body.act_id) {
+                let envelope = if let Some(replier) = replier {
+                    WireEnvelope::Call {
+                        msg: body.payload,
+                        replier,
+                    }
+                } else {
+                    WireEnvelope::Send { msg: body.payload }
+                };
+                (handle.deliverer)(envelope).await
+            } else {
+                Err(bterr!("invalid actor ID: {}", body.act_id))
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::net::IpAddr;
+
+    use super::*;
+
+    use btlib::crypto::CredStore;
+    use btlib_tests::TEST_STORE;
+    use btserde::from_slice;
+
+    #[derive(Serialize, Deserialize)]
+    struct EchoMsg(String);
+
+    impl CallMsg for EchoMsg {
+        type Reply = EchoMsg;
+    }
+
+    async fn echo(mut mailbox: mpsc::Receiver<Envelope<EchoMsg>>, _act_id: Uuid) {
+        while let Some(msg) = mailbox.recv().await {
+            if let Envelope::Call { msg, reply } = msg {
+                if let Err(_) = reply.send(msg) {
+                    panic!("failed to send reply");
+                }
+            }
+        }
+    }
+
+    fn echo_deserializer(slice: &[u8]) -> Result<EchoMsg> {
+        from_slice(slice).map_err(|err| err.into())
+    }
+
+    #[tokio::test]
+    async fn local_call() {
+        const EXPECTED: &str = "hello";
+        let ip_addr = IpAddr::from([127, 0, 0, 1]);
+        let creds = TEST_STORE.node_creds().unwrap();
+        let runtime = new_runtime(ip_addr, creds).unwrap();
+        let name = runtime.activate(echo, echo_deserializer).await;
+        let reply = runtime.call(&name, EchoMsg(EXPECTED.into())).await.unwrap();
+        assert_eq!(EXPECTED, reply.0)
+    }
+}