feat(sequencer): adminConfigureChannel RPC for channel roster changes

This commit is contained in:
erhant 2026-07-14 13:47:52 +03:00
parent 0143a7edf1
commit 726312025d
7 changed files with 96 additions and 8 deletions

2
Cargo.lock generated
View File

@ -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",
]

View File

@ -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<Ed25519PublicKey>,
posting_timeframe: u32,
posting_timeout: u32,
configuration_threshold: u16,
withdraw_threshold: u16,
) -> Result<()>;
) -> impl Future<Output = Result<()>> + Send;
fn channel_id(&self) -> ChannelId;

View File

@ -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

View File

@ -13,4 +13,5 @@ lee.workspace = true
lee_core.workspace = true
hex.workspace = true
serde.workspace = true
serde_with.workspace = true

View File

@ -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<String>,
pub posting_timeframe: u32,
pub posting_timeout: u32,
pub configuration_threshold: u16,
pub withdraw_threshold: u16,
}

View File

@ -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<ChannelId, ErrorObjectOwned>;
// =============================================================================================
/// 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>;
}

View File

@ -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<BC: BlockPublisherTrait> SequencerService<BC> {
}
#[async_trait]
impl<BC: BlockPublisherTrait + Send + 'static> sequencer_service_rpc::RpcServer
impl<BC: BlockPublisherTrait + Send + Sync + 'static> sequencer_service_rpc::RpcServer
for SequencerService<BC>
{
async fn send_transaction(&self, tx: LeeTransaction) -> Result<HashType, ErrorObjectOwned> {
@ -198,8 +199,66 @@ impl<BC: BlockPublisherTrait + Send + 'static> 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::<Result<Vec<_>, _>>()?;
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<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}")))?;
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());
}
}