#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod error;
pub mod rpc;
pub mod seal;
pub mod state;
use std::sync::Arc;
use dig_nat::{connect_with_runtime, NatConfig, NatRuntime, PeerConnection, PeerId as NatPeerId};
use dig_rpc_protocol::envelope::{JsonRpcRequest, RequestId};
use dig_rpc_protocol::types::{
AnnounceAck, AnnounceParams, Health, Methods, NetworkInfo, PeersList,
};
use dig_rpc_protocol::{JsonRpcResponse, Method};
use serde::de::DeserializeOwned;
use serde::Serialize;
pub use dig_nat::{AvailabilityItem, AvailabilityResponse, PeerStream, PeerTarget, RangeRequest};
pub use dig_tls::{NodeCert, PeerId};
pub use error::{DigPeerError, Result};
pub use seal::SealingIdentity;
pub use state::PeerState;
pub struct DigPeer {
local_peer_id: PeerId,
peer_id: PeerId,
peer_bls_pub: Option<[u8; 48]>,
conn: PeerConnection,
sealing: Option<SealingIdentity>,
state: PeerState,
next_id: u64,
}
impl std::fmt::Debug for DigPeer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DigPeer")
.field("peer_id", &self.peer_id)
.field("state", &self.state)
.field("sealable", &self.peer_bls_pub.is_some())
.field("has_sealing_identity", &self.sealing.is_some())
.finish()
}
}
impl DigPeer {
pub async fn connect(peer: &PeerTarget, tls: &Arc<NodeCert>) -> Result<Self> {
Self::connect_with_runtime(peer, tls, &NatConfig::default(), &NatRuntime::default()).await
}
pub async fn connect_with_runtime(
peer: &PeerTarget,
tls: &Arc<NodeCert>,
config: &NatConfig,
runtime: &NatRuntime,
) -> Result<Self> {
let conn = connect_with_runtime(peer, tls, config, runtime).await?;
verify_pinned_peer_id(peer.peer_id, conn.peer_id)?;
Ok(Self::from_connection(tls.peer_id(), conn))
}
#[must_use]
pub fn from_connection(local_peer_id: NatPeerId, conn: PeerConnection) -> Self {
Self {
local_peer_id,
peer_id: conn.peer_id,
peer_bls_pub: conn.peer_bls_pub,
conn,
sealing: None,
state: PeerState::Connected,
next_id: 1,
}
}
#[must_use]
pub fn with_sealing_identity(mut self, sealing: SealingIdentity) -> Self {
self.sealing = Some(sealing);
self
}
#[must_use]
pub fn peer_id(&self) -> PeerId {
self.peer_id
}
#[must_use]
pub fn peer_bls_pub(&self) -> Option<[u8; 48]> {
self.peer_bls_pub
}
#[must_use]
pub fn state(&self) -> PeerState {
self.state
}
#[must_use]
pub fn is_sealable(&self) -> bool {
self.peer_bls_pub.is_some() && self.sealing.is_some()
}
pub async fn health(&mut self) -> Result<Health> {
self.call_public(Method::Health, &serde_json::Value::Null)
.await
}
pub async fn methods(&mut self) -> Result<Methods> {
self.call_public(Method::Methods, &serde_json::Value::Null)
.await
}
pub async fn get_network_info(&mut self) -> Result<NetworkInfo> {
self.call_directed(Method::GetNetworkInfo, &serde_json::Value::Null)
.await
}
pub async fn get_peers(&mut self) -> Result<PeersList> {
self.call_directed(Method::GetPeers, &serde_json::Value::Null)
.await
}
pub async fn announce(&mut self, params: &AnnounceParams) -> Result<AnnounceAck> {
self.call_directed(Method::Announce, params).await
}
pub async fn get_availability(
&mut self,
items: Vec<AvailabilityItem>,
) -> Result<AvailabilityResponse> {
self.ensure_usable()?;
Ok(self.conn.query_availability(items).await?)
}
pub async fn fetch_range(&mut self, req: &RangeRequest) -> Result<dig_nat::PeerStream> {
self.ensure_usable()?;
Ok(self.conn.open_range_stream(req).await?)
}
pub async fn open_stream(&mut self) -> Result<PeerStream> {
self.ensure_usable()?;
Ok(self.conn.open_stream().await?)
}
pub async fn disconnect(mut self) {
self.state = PeerState::Closed;
}
async fn call_public<P, R>(&mut self, method: Method, params: &P) -> Result<R>
where
P: Serialize,
R: DeserializeOwned,
{
self.ensure_usable()?;
let request = self.build_request(method, params)?;
let body = rpc::to_json(&request)?;
let mut stream = self.conn.open_stream().await?;
rpc::write_framed(&mut stream, &body).await?;
let response_bytes = rpc::read_framed(&mut stream).await?;
Self::decode_result(&response_bytes)
}
async fn call_directed<P, R>(&mut self, method: Method, params: &P) -> Result<R>
where
P: Serialize,
R: DeserializeOwned,
{
self.ensure_usable()?;
let peer_bls_pub = self.peer_bls_pub.ok_or(DigPeerError::PeerNotSealable)?;
if self.sealing.is_none() {
return Err(DigPeerError::NoSealingIdentity);
}
let request = self.build_request(method, params)?;
let plaintext = rpc::to_json(&request)?;
let (local_peer_id, peer_id) = (self.local_peer_id, self.peer_id);
let sealing = self.sealing.as_mut().expect("checked is_some above");
let (sealed_request, correlation) =
sealing.seal_request(local_peer_id, peer_id, &peer_bls_pub, &plaintext)?;
let mut stream = self.conn.open_stream().await?;
rpc::write_framed(&mut stream, &sealed_request).await?;
let sealed_response = rpc::read_framed(&mut stream).await?;
let sealing = self.sealing.as_mut().expect("checked is_some above");
let plaintext_response =
sealing.open_response(&peer_bls_pub, correlation, &sealed_response)?;
Self::decode_result(&plaintext_response)
}
fn build_request<P: Serialize>(
&mut self,
method: Method,
params: &P,
) -> Result<JsonRpcRequest<serde_json::Value>> {
let params_value =
serde_json::to_value(params).map_err(|e| DigPeerError::Codec(e.to_string()))?;
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
Ok(JsonRpcRequest {
jsonrpc: dig_rpc_protocol::Version,
id: RequestId::Num(id),
method: method.name().to_string(),
params: Some(params_value),
})
}
fn decode_result<R: DeserializeOwned>(bytes: &[u8]) -> Result<R> {
let response: JsonRpcResponse<serde_json::Value> = rpc::from_json(bytes)?;
if let Some(error) = response.as_error() {
return Err(DigPeerError::Rpc(Box::new(error.clone())));
}
match response.as_result() {
Some(value) => serde_json::from_value(value.clone())
.map_err(|e| DigPeerError::Codec(e.to_string())),
None => Err(DigPeerError::Codec(
"response carried neither result nor error".into(),
)),
}
}
fn ensure_usable(&self) -> Result<()> {
if self.state.is_usable() {
Ok(())
} else {
Err(DigPeerError::InvalidState(self.state))
}
}
}
fn verify_pinned_peer_id(expected: PeerId, actual: PeerId) -> Result<()> {
if expected == actual {
Ok(())
} else {
Err(DigPeerError::PeerIdMismatch { expected, actual })
}
}
#[cfg(test)]
mod tests {
use super::*;
use dig_rpc_protocol::error::{ErrorCode, ErrorOrigin, RpcError};
#[test]
fn decode_result_returns_the_typed_result() {
let response = JsonRpcResponse::success(RequestId::Num(1), serde_json::json!({"n": 7}));
let bytes = serde_json::to_vec(&response).unwrap();
let value: serde_json::Value = DigPeer::decode_result(&bytes).expect("decodes");
assert_eq!(value["n"], 7);
}
#[test]
fn decode_result_maps_a_peer_error_envelope() {
let rpc_error = RpcError::new(
ErrorCode::MethodNotFound,
"method not found",
ErrorOrigin::Node,
);
let response: JsonRpcResponse = JsonRpcResponse::error(RequestId::Num(1), rpc_error);
let bytes = serde_json::to_vec(&response).unwrap();
let result: Result<serde_json::Value> = DigPeer::decode_result(&bytes);
assert!(matches!(result, Err(DigPeerError::Rpc(_))));
}
#[test]
fn decode_result_rejects_a_bodyless_response() {
let result: Result<serde_json::Value> = DigPeer::decode_result(b"garbage");
assert!(matches!(result, Err(DigPeerError::Codec(_))));
}
#[test]
fn verify_pinned_peer_id_accepts_a_match() {
let id = PeerId::from_bytes([0x11; 32]);
assert!(verify_pinned_peer_id(id, id).is_ok());
}
#[test]
fn verify_pinned_peer_id_rejects_a_mismatch() {
let expected = PeerId::from_bytes([0x11; 32]);
let actual = PeerId::from_bytes([0x22; 32]);
assert!(matches!(
verify_pinned_peer_id(expected, actual),
Err(DigPeerError::PeerIdMismatch { .. })
));
}
}