Skip to content

Commit

Permalink
custody: fill in scope of CustodyService
Browse files Browse the repository at this point in the history
This fills in missing methods that allow the CustodyService to be the interface
to a device like a hardware wallet, or to allow websites to request
confirmation for displayed addresses.

Co-Authored-By: Lucas Meier <[email protected]>
Co-Authored-By: Jen Helsby <[email protected]>
  • Loading branch information
3 people committed Nov 14, 2023
1 parent 5044662 commit 6fff63f
Show file tree
Hide file tree
Showing 13 changed files with 1,622 additions and 760 deletions.
22 changes: 20 additions & 2 deletions crates/custody/src/null_kms.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! A basic software key management system that stores keys in memory but
//! presents as an asynchronous signer.
use penumbra_proto::custody::v1alpha1::{self as pb, AuthorizeResponse};
use penumbra_proto::custody::v1alpha1::{self as pb};
use tonic::{async_trait, Request, Response, Status};

/// A "null KMS" that has no keys and errors on any requests.
Expand All @@ -15,7 +15,25 @@ impl pb::custody_protocol_service_server::CustodyProtocolService for NullKms {
async fn authorize(
&self,
_request: Request<pb::AuthorizeRequest>,
) -> Result<Response<AuthorizeResponse>, Status> {
) -> Result<Response<pb::AuthorizeResponse>, Status> {
Err(tonic::Status::failed_precondition(
"Got authorization request in view-only mode to null KMS.",
))
}

async fn export_full_viewing_key(
&self,
_request: Request<pb::ExportFullViewingKeyRequest>,
) -> Result<Response<pb::ExportFullViewingKeyResponse>, Status> {
Err(tonic::Status::failed_precondition(
"Got authorization request in view-only mode to null KMS.",
))
}

async fn confirm_address(
&self,
_request: Request<pb::ConfirmAddressRequest>,
) -> Result<Response<pb::ConfirmAddressResponse>, Status> {
Err(tonic::Status::failed_precondition(
"Got authorization request in view-only mode to null KMS.",
))
Expand Down
5 changes: 0 additions & 5 deletions crates/custody/src/request.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use penumbra_keys::keys::WalletId;
use penumbra_proto::{custody::v1alpha1 as pb, DomainType, TypeUrl};
use penumbra_transaction::plan::TransactionPlan;

Expand All @@ -9,8 +8,6 @@ use crate::PreAuthorization;
pub struct AuthorizeRequest {
/// The transaction plan to authorize.
pub plan: TransactionPlan,
/// Identifies the FVK (and hence the spend authorization key) to use for signing.
pub wallet_id: Option<WalletId>,
/// Optionally, pre-authorization data, if required by the custodian.
pub pre_authorizations: Vec<PreAuthorization>,
}
Expand All @@ -31,7 +28,6 @@ impl TryFrom<pb::AuthorizeRequest> for AuthorizeRequest {
.plan
.ok_or_else(|| anyhow::anyhow!("missing plan"))?
.try_into()?,
wallet_id: value.wallet_id.map(TryInto::try_into).transpose()?,
pre_authorizations: value
.pre_authorizations
.into_iter()
Expand All @@ -45,7 +41,6 @@ impl From<AuthorizeRequest> for pb::AuthorizeRequest {
fn from(value: AuthorizeRequest) -> pb::AuthorizeRequest {
Self {
plan: Some(value.plan.into()),
wallet_id: value.wallet_id.map(Into::into),
pre_authorizations: value
.pre_authorizations
.into_iter()
Expand Down
37 changes: 37 additions & 0 deletions crates/custody/src/soft_kms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,41 @@ impl pb::custody_protocol_service_server::CustodyProtocolService for SoftKms {

Ok(Response::new(authorization_response))
}

async fn export_full_viewing_key(
&self,
_request: Request<pb::ExportFullViewingKeyRequest>,
) -> Result<Response<pb::ExportFullViewingKeyResponse>, Status> {
Ok(Response::new(pb::ExportFullViewingKeyResponse {
full_viewing_key: Some(self.config.spend_key.full_viewing_key().clone().into()),
}))
}

async fn confirm_address(
&self,
request: Request<pb::ConfirmAddressRequest>,
) -> Result<Response<pb::ConfirmAddressResponse>, Status> {
let address_index = request
.into_inner()
.address_index
.ok_or_else(|| {
Status::invalid_argument("missing address index in confirm address request")
})?
.try_into()
.map_err(|e| {
Status::invalid_argument(format!(
"invalid address index in confirm address request: {e:#}"
))
})?;

let (address, _dtk) = self
.config
.spend_key
.full_viewing_key()
.payment_address(address_index);

Ok(Response::new(pb::ConfirmAddressResponse {
address: Some(address.into()),
}))
}
}
227 changes: 224 additions & 3 deletions crates/proto/src/gen/penumbra.custody.v1alpha1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ pub struct AuthorizeRequest {
pub plan: ::core::option::Option<
super::super::core::transaction::v1alpha1::TransactionPlan,
>,
/// Identifies the FVK (and hence the spend authorization key) to use for signing.
#[prost(message, optional, tag = "2")]
pub wallet_id: ::core::option::Option<super::super::core::keys::v1alpha1::WalletId>,
/// Optionally, pre-authorization data, if required by the custodian.
///
/// Pre-authorization data is backend-specific, and backends are free to ignore it.
///
/// Multiple `PreAuthorization` packets can be included in a single request,
/// to support multi-party pre-authorizations.
#[prost(message, repeated, tag = "3")]
Expand Down Expand Up @@ -55,6 +54,32 @@ pub mod pre_authorization {
Ed25519(Ed25519),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ExportFullViewingKeyRequest {}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ExportFullViewingKeyResponse {
/// The full viewing key.
#[prost(message, optional, tag = "1")]
pub full_viewing_key: ::core::option::Option<
super::super::core::keys::v1alpha1::FullViewingKey,
>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ConfirmAddressRequest {
#[prost(message, optional, tag = "1")]
pub address_index: ::core::option::Option<
super::super::core::keys::v1alpha1::AddressIndex,
>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ConfirmAddressResponse {
#[prost(message, optional, tag = "1")]
pub address: ::core::option::Option<super::super::core::keys::v1alpha1::Address>,
}
/// Generated client implementations.
#[cfg(feature = "rpc")]
pub mod custody_protocol_service_client {
Expand Down Expand Up @@ -185,6 +210,76 @@ pub mod custody_protocol_service_client {
);
self.inner.unary(req, path, codec).await
}
/// Requests the full viewing key from the custodian.
///
/// Custody backends should decide whether to honor this request, and how to
/// control access to it.
pub async fn export_full_viewing_key(
&mut self,
request: impl tonic::IntoRequest<super::ExportFullViewingKeyRequest>,
) -> std::result::Result<
tonic::Response<super::ExportFullViewingKeyResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::new(
tonic::Code::Unknown,
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/penumbra.custody.v1alpha1.CustodyProtocolService/ExportFullViewingKey",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"penumbra.custody.v1alpha1.CustodyProtocolService",
"ExportFullViewingKey",
),
);
self.inner.unary(req, path, codec).await
}
/// Displays an address to a user for confirmation.
///
/// Custody backends with user interaction should present the address to the
/// user and wait for explicit confirmation before returning.
///
/// Non-interactive custody backends may return immediately.
pub async fn confirm_address(
&mut self,
request: impl tonic::IntoRequest<super::ConfirmAddressRequest>,
) -> std::result::Result<
tonic::Response<super::ConfirmAddressResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::new(
tonic::Code::Unknown,
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/penumbra.custody.v1alpha1.CustodyProtocolService/ConfirmAddress",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"penumbra.custody.v1alpha1.CustodyProtocolService",
"ConfirmAddress",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
Expand All @@ -203,6 +298,30 @@ pub mod custody_protocol_service_server {
tonic::Response<super::AuthorizeResponse>,
tonic::Status,
>;
/// Requests the full viewing key from the custodian.
///
/// Custody backends should decide whether to honor this request, and how to
/// control access to it.
async fn export_full_viewing_key(
&self,
request: tonic::Request<super::ExportFullViewingKeyRequest>,
) -> std::result::Result<
tonic::Response<super::ExportFullViewingKeyResponse>,
tonic::Status,
>;
/// Displays an address to a user for confirmation.
///
/// Custody backends with user interaction should present the address to the
/// user and wait for explicit confirmation before returning.
///
/// Non-interactive custody backends may return immediately.
async fn confirm_address(
&self,
request: tonic::Request<super::ConfirmAddressRequest>,
) -> std::result::Result<
tonic::Response<super::ConfirmAddressResponse>,
tonic::Status,
>;
}
/// The custody protocol is used by a wallet client to request authorization for
/// a transaction they've constructed.
Expand Down Expand Up @@ -342,6 +461,108 @@ pub mod custody_protocol_service_server {
};
Box::pin(fut)
}
"/penumbra.custody.v1alpha1.CustodyProtocolService/ExportFullViewingKey" => {
#[allow(non_camel_case_types)]
struct ExportFullViewingKeySvc<T: CustodyProtocolService>(
pub Arc<T>,
);
impl<
T: CustodyProtocolService,
> tonic::server::UnaryService<super::ExportFullViewingKeyRequest>
for ExportFullViewingKeySvc<T> {
type Response = super::ExportFullViewingKeyResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::ExportFullViewingKeyRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as CustodyProtocolService>::export_full_viewing_key(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let inner = inner.0;
let method = ExportFullViewingKeySvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/penumbra.custody.v1alpha1.CustodyProtocolService/ConfirmAddress" => {
#[allow(non_camel_case_types)]
struct ConfirmAddressSvc<T: CustodyProtocolService>(pub Arc<T>);
impl<
T: CustodyProtocolService,
> tonic::server::UnaryService<super::ConfirmAddressRequest>
for ConfirmAddressSvc<T> {
type Response = super::ConfirmAddressResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::ConfirmAddressRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as CustodyProtocolService>::confirm_address(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let inner = inner.0;
let method = ConfirmAddressSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
Ok(
Expand Down
Loading

0 comments on commit 6fff63f

Please sign in to comment.