diff --git a/lez/sequencer/service/protocol/src/lib.rs b/lez/sequencer/service/protocol/src/lib.rs index 475772aa..afe61ab6 100644 --- a/lez/sequencer/service/protocol/src/lib.rs +++ b/lez/sequencer/service/protocol/src/lib.rs @@ -30,8 +30,8 @@ impl FromStr for ChannelId { /// Request for `adminConfigureChannel`: replaces the channel's accredited key /// set and rotation parameters. /// -/// `keys` are hex-encoded 32-byte Ed25519 public keys; `keys[0]` must be this -/// sequencer's (admin) key. +/// - `keys` are hex-encoded 32-byte Ed25519 public keys +/// - `keys[0]` must be this sequencer's (admin) key. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ConfigureChannelRequest { pub keys: Vec, @@ -40,3 +40,81 @@ pub struct ConfigureChannelRequest { pub configuration_threshold: u16, pub withdraw_threshold: u16, } + +impl ConfigureChannelRequest { + /// Structural sanity checks for the request. + /// + /// The L1 validates this too, but its async so we don't immediately + /// know about them when we submit. Checking this here instead gives + /// immediate feedback to the caller. + /// + /// We don't need a particular error type here, it's going to be logged only. + pub fn validate(&self) -> Result<(), String> { + let key_count = self.keys.len(); + if key_count == 0 { + return Err("Channel key list must not be empty".to_owned()); + } + for (name, threshold) in [ + ("configuration_threshold", self.configuration_threshold), + ("withdraw_threshold", self.withdraw_threshold), + ] { + if threshold == 0 || usize::from(threshold) > key_count { + return Err(format!( + "{name} must be between 1 and the key count ({key_count}), got {threshold}" + )); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn configure_channel_request_validate_rejects_static_garbage() { + // `validate` is structural: key contents are not parsed here. + let base = ConfigureChannelRequest { + keys: vec!["unparsed".to_owned(), "unparsed".to_owned()], + posting_timeframe: 20, + posting_timeout: 30, + configuration_threshold: 1, + withdraw_threshold: 2, + }; + + assert!(base.validate().is_ok()); + assert!( + ConfigureChannelRequest { + keys: vec![], + ..base + } + .validate() + .is_err() + ); + assert!( + ConfigureChannelRequest { + configuration_threshold: 0, + ..base.clone() + } + .validate() + .is_err() + ); + assert!( + ConfigureChannelRequest { + configuration_threshold: 3, + ..base.clone() + } + .validate() + .is_err() + ); + assert!( + ConfigureChannelRequest { + withdraw_threshold: 3, + ..base + } + .validate() + .is_err() + ); + } +} diff --git a/lez/sequencer/service/src/service.rs b/lez/sequencer/service/src/service.rs index 53fc3897..549144f0 100644 --- a/lez/sequencer/service/src/service.rs +++ b/lez/sequencer/service/src/service.rs @@ -204,6 +204,7 @@ impl sequencer_service_rpc::Rpc &self, request: ConfigureChannelRequest, ) -> Result<(), ErrorObjectOwned> { + request.validate().map_err(invalid_params)?; let keys = request .keys .iter() @@ -234,16 +235,17 @@ fn internal_error(err: &DbError) -> ErrorObjectOwned { ErrorObjectOwned::owned(ErrorCode::InternalError.code(), err.to_string(), None::<()>) } +fn invalid_params(detail: String) -> ErrorObjectOwned { + ErrorObjectOwned::owned(ErrorCode::InvalidParams.code(), detail, None::<()>) +} + /// Parses one hex-encoded 32-byte Ed25519 public key from an RPC request. fn parse_channel_key(hex_key: &str) -> Result { - let invalid = |detail: String| { - ErrorObjectOwned::owned(ErrorCode::InvalidParams.code(), detail, None::<()>) - }; let mut bytes = [0_u8; 32]; hex::decode_to_slice(hex_key, &mut bytes) - .map_err(|err| invalid(format!("Invalid hex-encoded key: {err}")))?; + .map_err(|err| invalid_params(format!("Invalid hex-encoded key: {err}")))?; Ed25519PublicKey::from_bytes(&bytes) - .map_err(|err| invalid(format!("Invalid Ed25519 public key: {err}"))) + .map_err(|err| invalid_params(format!("Invalid Ed25519 public key: {err}"))) } #[cfg(test)]