fix(sequencer): validate adminConfigureChannel requests, per Copilot review

This commit is contained in:
erhant 2026-07-14 18:10:34 +03:00
parent a621a65f40
commit a5c5e06700
2 changed files with 87 additions and 7 deletions

View File

@ -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<String>,
@ -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()
);
}
}

View File

@ -204,6 +204,7 @@ impl<BC: BlockPublisherTrait + Send + Sync + 'static> 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<Ed25519PublicKey, ErrorObjectOwned> {
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)]