210 lines
6.5 KiB
Rust

//! Reexports of types used by sequencer rpc specification.
use std::{fmt::Display, str::FromStr};
pub use common::{HashType, block::Block, transaction::LeeTransaction};
pub use lee::{Account, AccountId, ProgramId};
pub use lee_core::{BlockId, Commitment, CommitmentSetDigest, MembershipProof, account::Nonce};
use serde_with::{DeserializeFromStr, SerializeDisplay};
#[derive(Debug, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
pub struct ChannelId(pub [u8; 32]);
impl Display for ChannelId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let hex_string = hex::encode(self.0);
write!(f, "{hex_string}")
}
}
impl FromStr for ChannelId {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0_u8; 32];
hex::decode_to_slice(s, &mut bytes)?;
Ok(Self(bytes))
}
}
/// 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.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(try_from = "UncheckedConfigureChannelRequest")]
pub struct ConfigureChannelRequest {
pub keys: Vec<String>,
pub posting_timeframe: u32,
pub posting_timeout: u32,
pub configuration_threshold: u16,
pub withdraw_threshold: u16,
}
/// Wire-shape mirror of [`ConfigureChannelRequest`]; deserialization goes
/// through it so [`ConfigureChannelRequest::validate`] runs on every decode.
#[derive(serde::Deserialize)]
struct UncheckedConfigureChannelRequest {
keys: Vec<String>,
posting_timeframe: u32,
posting_timeout: u32,
configuration_threshold: u16,
withdraw_threshold: u16,
}
impl TryFrom<UncheckedConfigureChannelRequest> for ConfigureChannelRequest {
type Error = String;
fn try_from(unchecked: UncheckedConfigureChannelRequest) -> Result<Self, Self::Error> {
let UncheckedConfigureChannelRequest {
keys,
posting_timeframe,
posting_timeout,
configuration_threshold,
withdraw_threshold,
} = unchecked;
let request = Self {
keys,
posting_timeframe,
posting_timeout,
configuration_threshold,
withdraw_threshold,
};
request.validate()?;
Ok(request)
}
}
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}"
));
}
}
if self.posting_timeframe == 0 || self.posting_timeout == 0 {
return Err(format!(
"posting_timeframe and posting_timeout must be nonzero, got {} and {}",
self.posting_timeframe, self.posting_timeout
));
}
if self.posting_timeout < self.posting_timeframe {
return Err(format!(
"posting_timeout ({}) must not be shorter than posting_timeframe ({})",
self.posting_timeout, self.posting_timeframe
));
}
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 {
posting_timeframe: 0,
..base.clone()
}
.validate()
.is_err()
);
assert!(
ConfigureChannelRequest {
posting_timeout: 0,
..base.clone()
}
.validate()
.is_err()
);
assert!(
ConfigureChannelRequest {
posting_timeout: 10,
..base.clone()
}
.validate()
.is_err()
);
assert!(
ConfigureChannelRequest {
withdraw_threshold: 3,
..base
}
.validate()
.is_err()
);
}
#[test]
fn configure_channel_request_validates_on_deserialization() {
let valid = serde_json::json!({
"keys": ["unparsed"],
"posting_timeframe": 20,
"posting_timeout": 30,
"configuration_threshold": 1,
"withdraw_threshold": 1,
});
assert!(serde_json::from_value::<ConfigureChannelRequest>(valid.clone()).is_ok());
let mut invalid = valid;
invalid["posting_timeout"] = serde_json::json!(10);
let err = serde_json::from_value::<ConfigureChannelRequest>(invalid)
.expect_err("timeout below timeframe must fail to deserialize");
assert!(err.to_string().contains("posting_timeout"));
}
}