Skip to content

Commit

Permalink
Implement validator signing custody rpc methods
Browse files Browse the repository at this point in the history
  • Loading branch information
plaidfinch committed Mar 22, 2024
1 parent d245fe9 commit a93178d
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 73 deletions.
4 changes: 3 additions & 1 deletion crates/custody/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ pub mod threshold;

pub use client::CustodyClient;
pub use pre_auth::PreAuthorization;
pub use request::AuthorizeRequest;
pub use request::{
AuthorizeRequest, AuthorizeValidatorDefinitionRequest, AuthorizeValidatorVoteRequest,
};
152 changes: 109 additions & 43 deletions crates/custody/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,47 @@
use std::collections::HashSet;

use penumbra_keys::Address;
use penumbra_proto::{
core::{
component::{
governance::v1::ValidatorVoteBody as ProtoValidatorVoteBody,
stake::v1::Validator as ProtoValidator,
},
transaction::v1::TransactionPlan as ProtoTransactionPlan,
},
Message as _,
};
use penumbra_transaction::plan::ActionPlan;
use serde::{Deserialize, Serialize};

use crate::{AuthorizeRequest, PreAuthorization};
use crate::{
AuthorizeRequest, AuthorizeValidatorDefinitionRequest, AuthorizeValidatorVoteRequest,
PreAuthorization,
};

/// A trait for checking whether a transaction plan is allowed by a policy.
pub trait Policy {
/// Checks whether the proposed transaction plan is allowed by this policy.
fn check(&self, request: &AuthorizeRequest) -> anyhow::Result<()>;
fn check_transaction(&self, request: &AuthorizeRequest) -> anyhow::Result<()>;

/// Checks whether the proposed validator definition is allowed by this policy.
fn check_validator_definition(
&self,
_request: &AuthorizeValidatorDefinitionRequest,
) -> anyhow::Result<()>;

/// Checks whether the proposed validator vote is allowed by this policy.
fn check_validator_vote(&self, _request: &AuthorizeValidatorVoteRequest) -> anyhow::Result<()>;
}

/// A set of basic spend authorization policies.
///
/// These policies are intended to be simple enough that they can be written by
/// hand in a config file. More complex policy logic than than should be
/// implemented by a custom implementation of the [`Policy`] trait.
/// These policies are intended to be simple enough that they can be written by hand in a config
/// file. More complex policy logic than than should be implemented by a custom implementation of
/// the [`Policy`] trait.
///
/// These policies do not permit validator votes or validator definition updates, so a custom policy
/// must be used to approve these actions.
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(tag = "type")]
pub enum AuthPolicy {
Expand Down Expand Up @@ -58,6 +83,52 @@ pub enum PreAuthorizationPolicy {
},
}

impl PreAuthorizationPolicy {
fn check_pre_authorizations(
&self,
pre_authorizations: &[PreAuthorization],
signed_data: impl AsRef<[u8]>,
) -> anyhow::Result<()> {
let signed_data = signed_data.as_ref();
match self {
PreAuthorizationPolicy::Ed25519 {
required_signatures,
allowed_signers,
} => {
#[allow(clippy::unnecessary_filter_map)]
let ed25519_pre_auths =
pre_authorizations
.iter()
.filter_map(|pre_auth| match pre_auth {
PreAuthorization::Ed25519(pre_auth) => Some(pre_auth),
// _ => None,
});

let mut allowed_signers = allowed_signers.iter().cloned().collect::<HashSet<_>>();
let mut seen_signers = HashSet::new();

for pre_auth in ed25519_pre_auths {
// Remove the signer from the allowed signers set, so that
// each signer can only submit one pre-authorization.
if let Some(signer) = allowed_signers.take(&pre_auth.vk) {
pre_auth.verify(signed_data)?;
seen_signers.insert(signer);
}
}

if seen_signers.len() < *required_signatures as usize {
anyhow::bail!(
"required {} pre-authorization signatures but only saw {}",
required_signatures,
seen_signers.len(),
);
}
Ok(())
}
}
}
}

mod address_as_string {
use std::str::FromStr;

Expand Down Expand Up @@ -130,7 +201,7 @@ mod ed25519_vec_base64 {
}

impl Policy for AuthPolicy {
fn check(&self, request: &AuthorizeRequest) -> anyhow::Result<()> {
fn check_transaction(&self, request: &AuthorizeRequest) -> anyhow::Result<()> {
let plan = &request.plan;
match self {
AuthPolicy::DestinationAllowList {
Expand Down Expand Up @@ -161,49 +232,44 @@ impl Policy for AuthPolicy {
}
Ok(())
}
AuthPolicy::PreAuthorization(policy) => policy.check(request),
AuthPolicy::PreAuthorization(policy) => policy.check_transaction(request),
}
}

fn check_validator_definition(
&self,
_request: &AuthorizeValidatorDefinitionRequest,
) -> anyhow::Result<()> {
anyhow::bail!("validator definitions are not allowed by this policy")
}

fn check_validator_vote(&self, _request: &AuthorizeValidatorVoteRequest) -> anyhow::Result<()> {
anyhow::bail!("validator votes are not allowed by this policy")
}
}

impl Policy for PreAuthorizationPolicy {
fn check(&self, request: &AuthorizeRequest) -> anyhow::Result<()> {
match self {
PreAuthorizationPolicy::Ed25519 {
required_signatures,
allowed_signers,
} => {
#[allow(clippy::unnecessary_filter_map)]
let ed25519_pre_auths =
request
.pre_authorizations
.iter()
.filter_map(|pre_auth| match pre_auth {
PreAuthorization::Ed25519(pre_auth) => Some(pre_auth),
// _ => None,
});

let mut allowed_signers = allowed_signers.iter().cloned().collect::<HashSet<_>>();
let mut seen_signers = HashSet::new();
fn check_transaction(&self, request: &AuthorizeRequest) -> anyhow::Result<()> {
self.check_pre_authorizations(
&request.pre_authorizations,
ProtoTransactionPlan::from(request.plan.clone()).encode_to_vec(),
)
}

for pre_auth in ed25519_pre_auths {
// Remove the signer from the allowed signers set, so that
// each signer can only submit one pre-authorization.
if let Some(signer) = allowed_signers.take(&pre_auth.vk) {
pre_auth.verify_plan(&request.plan)?;
seen_signers.insert(signer);
}
}
fn check_validator_definition(
&self,
request: &AuthorizeValidatorDefinitionRequest,
) -> anyhow::Result<()> {
self.check_pre_authorizations(
&request.pre_authorizations,
ProtoValidator::from(request.validator_definition.clone()).encode_to_vec(),
)
}

if seen_signers.len() < *required_signatures as usize {
anyhow::bail!(
"required {} pre-authorization signatures but only saw {}",
required_signatures,
seen_signers.len(),
);
}
Ok(())
}
}
fn check_validator_vote(&self, request: &AuthorizeValidatorVoteRequest) -> anyhow::Result<()> {
self.check_pre_authorizations(
&request.pre_authorizations,
ProtoValidatorVoteBody::from(request.validator_vote.clone()).encode_to_vec(),
)
}
}
7 changes: 3 additions & 4 deletions crates/custody/src/pre_auth.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use penumbra_proto::{custody::v1 as pb, DomainType};
use penumbra_transaction::TransactionPlan;
use serde::{Deserialize, Serialize};

/// A pre-authorization packet. This allows a custodian to delegate (partial)
Expand Down Expand Up @@ -28,9 +27,9 @@ pub struct Ed25519 {

impl Ed25519 {
/// Verifies the provided `TransactionPlan`.
pub fn verify_plan(&self, plan: &TransactionPlan) -> anyhow::Result<()> {
let plan_bytes = plan.encode_to_vec();
self.vk.verify(&self.sig, &plan_bytes).map_err(Into::into)
pub fn verify(&self, message: impl AsRef<[u8]>) -> anyhow::Result<()> {
let bytes = message.as_ref();
self.vk.verify(&self.sig, &bytes).map_err(Into::into)
}
}

Expand Down
88 changes: 88 additions & 0 deletions crates/custody/src/request.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use penumbra_governance::ValidatorVoteBody;
use penumbra_proto::{custody::v1 as pb, DomainType};
use penumbra_stake::validator::Validator;
use penumbra_transaction::TransactionPlan;

use crate::PreAuthorization;
Expand Down Expand Up @@ -45,3 +47,89 @@ impl From<AuthorizeRequest> for pb::AuthorizeRequest {
}
}
}

/// A validator definition authorization request submitted to a custody service for approval.
#[derive(Debug, Clone)]
pub struct AuthorizeValidatorDefinitionRequest {
/// The validator definition to authorize.
pub validator_definition: Validator,
/// Optionally, pre-authorization data, if required by the custodian.
pub pre_authorizations: Vec<PreAuthorization>,
}

impl DomainType for AuthorizeValidatorDefinitionRequest {
type Proto = pb::AuthorizeValidatorDefinitionRequest;
}

impl TryFrom<pb::AuthorizeValidatorDefinitionRequest> for AuthorizeValidatorDefinitionRequest {
type Error = anyhow::Error;
fn try_from(value: pb::AuthorizeValidatorDefinitionRequest) -> Result<Self, Self::Error> {
Ok(Self {
validator_definition: value
.validator_definition
.ok_or_else(|| anyhow::anyhow!("missing validator definition"))?
.try_into()?,
pre_authorizations: value
.pre_authorizations
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()?,
})
}
}

impl From<AuthorizeValidatorDefinitionRequest> for pb::AuthorizeValidatorDefinitionRequest {
fn from(value: AuthorizeValidatorDefinitionRequest) -> pb::AuthorizeValidatorDefinitionRequest {
Self {
validator_definition: Some(value.validator_definition.into()),
pre_authorizations: value
.pre_authorizations
.into_iter()
.map(Into::into)
.collect(),
}
}
}

/// A validator vote authorization request submitted to a custody service for approval.
#[derive(Debug, Clone)]
pub struct AuthorizeValidatorVoteRequest {
/// The transaction plan to authorize.
pub validator_vote: ValidatorVoteBody,
/// Optionally, pre-authorization data, if required by the custodian.
pub pre_authorizations: Vec<PreAuthorization>,
}

impl DomainType for AuthorizeValidatorVoteRequest {
type Proto = pb::AuthorizeValidatorVoteRequest;
}

impl TryFrom<pb::AuthorizeValidatorVoteRequest> for AuthorizeValidatorVoteRequest {
type Error = anyhow::Error;
fn try_from(value: pb::AuthorizeValidatorVoteRequest) -> Result<Self, Self::Error> {
Ok(Self {
validator_vote: value
.validator_vote
.ok_or_else(|| anyhow::anyhow!("missing validator vote"))?
.try_into()?,
pre_authorizations: value
.pre_authorizations
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()?,
})
}
}

impl From<AuthorizeValidatorVoteRequest> for pb::AuthorizeValidatorVoteRequest {
fn from(value: AuthorizeValidatorVoteRequest) -> pb::AuthorizeValidatorVoteRequest {
Self {
validator_vote: Some(value.validator_vote.into()),
pre_authorizations: value
.pre_authorizations
.into_iter()
.map(Into::into)
.collect(),
}
}
}
Loading

0 comments on commit a93178d

Please sign in to comment.