feat(proposal): reject proposals with an excessive TTL
This commit is contained in:
@@ -20,7 +20,8 @@ use kameo::{Actor, actor::ActorRef, messages};
|
|||||||
use strum::IntoDiscriminant as _;
|
use strum::IntoDiscriminant as _;
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
pub const DEFAULT_TTL_SECS: i64 = 7 * 24 * 60 * 60; // 7 days
|
pub const DEFAULT_TTL_SECS: u32 = 7 * 24 * 60 * 60; // 7 days
|
||||||
|
pub const MAX_TTL_SECS: u32 = DEFAULT_TTL_SECS;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum VoteOutcome {
|
pub enum VoteOutcome {
|
||||||
@@ -37,6 +38,8 @@ pub enum Error {
|
|||||||
ProposalNotPending,
|
ProposalNotPending,
|
||||||
#[error("Proposal has expired")]
|
#[error("Proposal has expired")]
|
||||||
ProposalExpired,
|
ProposalExpired,
|
||||||
|
#[error("Requested TTL exceeds the maximum of {} seconds", MAX_TTL_SECS)]
|
||||||
|
TtlTooLong,
|
||||||
#[error("Operator already voted on this proposal")]
|
#[error("Operator already voted on this proposal")]
|
||||||
AlreadyVoted,
|
AlreadyVoted,
|
||||||
#[error("Invalid vote signature")]
|
#[error("Invalid vote signature")]
|
||||||
@@ -100,10 +103,14 @@ impl ProposalManager {
|
|||||||
&mut self,
|
&mut self,
|
||||||
kind: ProposalKind,
|
kind: ProposalKind,
|
||||||
initiator_id: i32,
|
initiator_id: i32,
|
||||||
ttl_secs: Option<i64>,
|
ttl_secs: Option<u32>,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
let ttl = ttl_secs.unwrap_or(DEFAULT_TTL_SECS);
|
let ttl = ttl_secs.unwrap_or(DEFAULT_TTL_SECS);
|
||||||
let expires_at = SqliteTimestamp::from(Utc::now() + chrono::Duration::seconds(ttl));
|
if ttl > MAX_TTL_SECS {
|
||||||
|
return Err(Error::TtlTooLong);
|
||||||
|
}
|
||||||
|
let expires_at =
|
||||||
|
SqliteTimestamp::from(Utc::now() + chrono::Duration::seconds(i64::from(ttl)));
|
||||||
|
|
||||||
let new_proposal = NewProposal {
|
let new_proposal = NewProposal {
|
||||||
kind: kind.discriminant(),
|
kind: kind.discriminant(),
|
||||||
|
|||||||
@@ -72,10 +72,11 @@ async fn handle_create(
|
|||||||
}
|
}
|
||||||
None => return Err(Status::invalid_argument("Missing proposal kind")),
|
None => return Err(Status::invalid_argument("Missing proposal kind")),
|
||||||
};
|
};
|
||||||
let ttl_secs = req.ttl_secs.map(i64::from);
|
|
||||||
|
|
||||||
let proposal_id = actor
|
let proposal_id = actor
|
||||||
.ask(HandleCreateProposal { kind, ttl_secs })
|
.ask(HandleCreateProposal {
|
||||||
|
kind,
|
||||||
|
ttl_secs: req.ttl_secs,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
warn!(?e, "create_proposal failed");
|
warn!(?e, "create_proposal failed");
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ impl OperatorSession {
|
|||||||
pub(crate) async fn handle_create_proposal(
|
pub(crate) async fn handle_create_proposal(
|
||||||
&mut self,
|
&mut self,
|
||||||
kind: crate::db::models::ProposalKind,
|
kind: crate::db::models::ProposalKind,
|
||||||
ttl_secs: Option<i64>,
|
ttl_secs: Option<u32>,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
use crate::actors::proposal_manager::CreateProposal;
|
use crate::actors::proposal_manager::CreateProposal;
|
||||||
let initiator_id = self.credentials.id;
|
let initiator_id = self.credentials.id;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use arbiter_server::{
|
|||||||
GlobalActors,
|
GlobalActors,
|
||||||
proposal_manager::{
|
proposal_manager::{
|
||||||
CancelRecoveryWakeup, CastRecoveryVote, CastVote, CreateProposal,
|
CancelRecoveryWakeup, CastRecoveryVote, CastVote, CreateProposal,
|
||||||
Error as ProposalError, QueryPending, RequestRecoveryWakeup, VoteOutcome,
|
Error as ProposalError, MAX_TTL_SECS, QueryPending, RequestRecoveryWakeup, VoteOutcome,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
crypto::KeyCell,
|
crypto::KeyCell,
|
||||||
@@ -131,6 +131,45 @@ async fn create_proposal_returns_id() {
|
|||||||
assert!(proposal_id > 0);
|
assert!(proposal_id > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_proposal_caps_the_ttl() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
actors
|
||||||
|
.vault
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key: KeyCell::from([0u8; 32]),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let key = authn::SigningKey::generate();
|
||||||
|
let op = register_operator(&db, &key.public_key()).await;
|
||||||
|
|
||||||
|
let create = async |ttl: u32| {
|
||||||
|
actors
|
||||||
|
.proposal_manager
|
||||||
|
.ask(CreateProposal {
|
||||||
|
kind: ProposalKind::ApproveSdkClient { client_id: 1 },
|
||||||
|
initiator_id: op,
|
||||||
|
ttl_secs: Some(ttl),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
// The boundary itself must still be accepted: the check is `>`, not `>=`.
|
||||||
|
create(MAX_TTL_SECS)
|
||||||
|
.await
|
||||||
|
.expect("a TTL at the ceiling must be accepted");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
create(MAX_TTL_SECS + 1).await,
|
||||||
|
Err(kameo::error::SendError::HandlerError(
|
||||||
|
ProposalError::TtlTooLong { .. }
|
||||||
|
))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn single_operator_vote_reaches_quorum() {
|
async fn single_operator_vote_reaches_quorum() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
@@ -406,13 +445,13 @@ async fn expired_proposal_is_hidden_and_unvotable() {
|
|||||||
let client_key = authn::SigningKey::generate();
|
let client_key = authn::SigningKey::generate();
|
||||||
let client_id = insert_unapproved_client(&db, &client_key.public_key()).await;
|
let client_id = insert_unapproved_client(&db, &client_key.public_key()).await;
|
||||||
|
|
||||||
// Create proposal with ttl_secs = -1 so it's immediately expired
|
// Create proposal with ttl_secs = 0 so it's immediately expired
|
||||||
let proposal_id = actors
|
let proposal_id = actors
|
||||||
.proposal_manager
|
.proposal_manager
|
||||||
.ask(CreateProposal {
|
.ask(CreateProposal {
|
||||||
kind: ProposalKind::ApproveSdkClient { client_id },
|
kind: ProposalKind::ApproveSdkClient { client_id },
|
||||||
initiator_id: op,
|
initiator_id: op,
|
||||||
ttl_secs: Some(-1),
|
ttl_secs: Some(0),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user