From 726312025d4dba08924793d3d6a9091f784dd0e3 Mon Sep 17 00:00:00 2001 From: erhant Date: Tue, 14 Jul 2026 13:47:52 +0300 Subject: [PATCH] feat(sequencer): adminConfigureChannel RPC for channel roster changes --- Cargo.lock | 2 + lez/sequencer/core/src/block_publisher.rs | 7 ++- lez/sequencer/service/Cargo.toml | 1 + lez/sequencer/service/protocol/Cargo.toml | 1 + lez/sequencer/service/protocol/src/lib.rs | 14 +++++ lez/sequencer/service/rpc/src/lib.rs | 12 +++- lez/sequencer/service/src/service.rs | 67 +++++++++++++++++++++-- 7 files changed, 96 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcdcec14..93d5f16b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9085,6 +9085,7 @@ dependencies = [ "common", "env_logger", "futures", + "hex", "jsonrpsee", "lee", "log", @@ -9105,6 +9106,7 @@ dependencies = [ "hex", "lee", "lee_core", + "serde", "serde_with", ] diff --git a/lez/sequencer/core/src/block_publisher.rs b/lez/sequencer/core/src/block_publisher.rs index ecd1ffc8..6230d32c 100644 --- a/lez/sequencer/core/src/block_publisher.rs +++ b/lez/sequencer/core/src/block_publisher.rs @@ -108,14 +108,17 @@ pub trait BlockPublisherTrait: Clone { /// `ChannelConfig` op. The sequencer's bedrock key must be the channel /// admin (`keys[0]`); the L1 rejects non-admin signers, so this is not /// re-validated here. - async fn configure_channel( + /// + /// Desugared (not `async fn`) so the returned future is provably `Send` — + /// generic callers awaiting it inside jsonrpsee handlers require that. + fn configure_channel( &self, keys: Vec, posting_timeframe: u32, posting_timeout: u32, configuration_threshold: u16, withdraw_threshold: u16, - ) -> Result<()>; + ) -> impl Future> + Send; fn channel_id(&self) -> ChannelId; diff --git a/lez/sequencer/service/Cargo.toml b/lez/sequencer/service/Cargo.toml index 3427dc22..94617bd9 100644 --- a/lez/sequencer/service/Cargo.toml +++ b/lez/sequencer/service/Cargo.toml @@ -19,6 +19,7 @@ programs.workspace = true clap = { workspace = true, features = ["derive", "env"] } anyhow.workspace = true env_logger.workspace = true +hex.workspace = true log.workspace = true tokio.workspace = true tokio-util.workspace = true diff --git a/lez/sequencer/service/protocol/Cargo.toml b/lez/sequencer/service/protocol/Cargo.toml index ced19e75..1eb413d0 100644 --- a/lez/sequencer/service/protocol/Cargo.toml +++ b/lez/sequencer/service/protocol/Cargo.toml @@ -13,4 +13,5 @@ lee.workspace = true lee_core.workspace = true hex.workspace = true +serde.workspace = true serde_with.workspace = true diff --git a/lez/sequencer/service/protocol/src/lib.rs b/lez/sequencer/service/protocol/src/lib.rs index 58e300f6..475772aa 100644 --- a/lez/sequencer/service/protocol/src/lib.rs +++ b/lez/sequencer/service/protocol/src/lib.rs @@ -26,3 +26,17 @@ impl FromStr for ChannelId { 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)] +pub struct ConfigureChannelRequest { + pub keys: Vec, + pub posting_timeframe: u32, + pub posting_timeout: u32, + pub configuration_threshold: u16, + pub withdraw_threshold: u16, +} diff --git a/lez/sequencer/service/rpc/src/lib.rs b/lez/sequencer/service/rpc/src/lib.rs index f7aa8b56..40b9d2bd 100644 --- a/lez/sequencer/service/rpc/src/lib.rs +++ b/lez/sequencer/service/rpc/src/lib.rs @@ -6,8 +6,8 @@ use jsonrpsee::types::ErrorObjectOwned; #[cfg(feature = "client")] pub use jsonrpsee::{core::ClientError, http_client::HttpClientBuilder as SequencerClientBuilder}; use sequencer_service_protocol::{ - Account, AccountId, Block, BlockId, ChannelId, Commitment, HashType, LeeTransaction, - MembershipProof, Nonce, ProgramId, + Account, AccountId, Block, BlockId, ChannelId, Commitment, ConfigureChannelRequest, HashType, + LeeTransaction, MembershipProof, Nonce, ProgramId, }; #[cfg(all(not(feature = "server"), not(feature = "client")))] @@ -92,4 +92,12 @@ pub trait Rpc { async fn get_channel_id(&self) -> Result; // ============================================================================================= + + /// Admin-only in effect: the L1 rejects the config op unless this + /// sequencer's key is the channel admin (`keys[0]` of the current roster). + #[method(name = "adminConfigureChannel")] + async fn admin_configure_channel( + &self, + request: ConfigureChannelRequest, + ) -> Result<(), ErrorObjectOwned>; } diff --git a/lez/sequencer/service/src/service.rs b/lez/sequencer/service/src/service.rs index 317af868..b8fce3df 100644 --- a/lez/sequencer/service/src/service.rs +++ b/lez/sequencer/service/src/service.rs @@ -9,11 +9,12 @@ use lee; use log::warn; use mempool::MemPoolHandle; use sequencer_core::{ - DbError, SequencerCore, TransactionOrigin, block_publisher::BlockPublisherTrait, + DbError, SequencerCore, TransactionOrigin, + block_publisher::{BlockPublisherTrait, Ed25519PublicKey}, }; use sequencer_service_protocol::{ - Account, AccountId, Block, BlockId, ChannelId, Commitment, HashType, MembershipProof, Nonce, - ProgramId, + Account, AccountId, Block, BlockId, ChannelId, Commitment, ConfigureChannelRequest, HashType, + MembershipProof, Nonce, ProgramId, }; use tokio::sync::Mutex; @@ -40,7 +41,7 @@ impl SequencerService { } #[async_trait] -impl sequencer_service_rpc::RpcServer +impl sequencer_service_rpc::RpcServer for SequencerService { async fn send_transaction(&self, tx: LeeTransaction) -> Result { @@ -198,8 +199,66 @@ impl sequencer_service_rpc::RpcServer let channel_id = self.sequencer.lock().await.block_publisher().channel_id(); Ok(ChannelId(*channel_id.as_ref())) } + + async fn admin_configure_channel( + &self, + request: ConfigureChannelRequest, + ) -> Result<(), ErrorObjectOwned> { + let keys = request + .keys + .iter() + .map(|hex_key| parse_channel_key(hex_key)) + .collect::, _>>()?; + + let sequencer = self.sequencer.lock().await; + sequencer + .configure_channel( + keys, + request.posting_timeframe, + request.posting_timeout, + request.configuration_threshold, + request.withdraw_threshold, + ) + .await + .map_err(|err| { + ErrorObjectOwned::owned( + ErrorCode::InternalError.code(), + format!("{err:#}"), + None::<()>, + ) + }) + } } fn internal_error(err: &DbError) -> ErrorObjectOwned { ErrorObjectOwned::owned(ErrorCode::InternalError.code(), err.to_string(), 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}")))?; + Ed25519PublicKey::from_bytes(&bytes) + .map_err(|err| invalid(format!("Invalid Ed25519 public key: {err}"))) +} + +#[cfg(test)] +mod tests { + use sequencer_core::block_publisher::Ed25519Key; + + use super::*; + + #[test] + fn parse_channel_key_roundtrips_and_rejects_garbage() { + let key = Ed25519Key::from_bytes(&[7; 32]).public_key(); + let parsed = parse_channel_key(&hex::encode(key.to_bytes())).unwrap(); + assert_eq!(parsed.to_bytes(), key.to_bytes()); + + assert!(parse_channel_key("not-hex").is_err()); + assert!(parse_channel_key("abcd").is_err()); + } +}