refactor(sequencer): some renamings, better key-reuse

This commit is contained in:
erhant
2026-08-08 16:34:35 +03:00
parent 565ff8632e
commit 7d781dc799
5 changed files with 58 additions and 55 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
//! down degrades to L1-only behavior, and a gossip failure after startup
//! never halts the node.
pub use network::{Libp2pNetwork, PeerNetworkTrait, TxPublisher};
pub use network::{GossipNetwork, PeerNetworkTrait, TxPublisher};
pub mod accreditation;
pub mod network;
+19 -15
View File
@@ -15,6 +15,7 @@ use libp2p::{
multiaddr::Protocol,
swarm::{NetworkBehaviour, Swarm, SwarmEvent},
};
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
use mempool::MemPoolHandle;
use tokio::sync::{mpsc, watch};
use tokio_util::sync::CancellationToken;
@@ -55,7 +56,7 @@ pub trait PeerNetworkTrait {
}
/// Handle to the running gossip network. Dropping it stops the drive task.
pub struct Libp2pNetwork {
pub struct GossipNetwork {
connected_rx: watch::Receiver<Vec<[u8; 32]>>,
shutdown: CancellationToken,
listen_addrs: Vec<Multiaddr>,
@@ -77,7 +78,7 @@ impl TxPublisher {
}
}
impl Libp2pNetwork {
impl GossipNetwork {
/// Builds the swarm, binds `listen_addr`, seeds Kademlia and dials
/// bootstrap peers, and spawns the drive task. Fails fast on a bad
/// listen/bootstrap multiaddr or bind failure; after that, gossip errors
@@ -85,12 +86,15 @@ impl Libp2pNetwork {
pub async fn start(
config: GossipConfig,
channel_id: [u8; 32],
secret_key: [u8; 32],
signing_key: Ed25519Key,
mempool_handle: MemPoolHandle<(TransactionOrigin, LeeTransaction)>,
max_block_size: u64,
) -> Result<Self> {
let mut secret_for_libp2p = secret_key;
let keypair = Keypair::ed25519_from_bytes(&mut secret_for_libp2p)
// Reuse the node's L1 bedrock signing key as the libp2p identity. The
// secret stays in a `Zeroizing` buffer that both `ed25519_from_bytes`
// and drop wipe.
let mut secret = signing_key.into_unsecured().to_bytes();
let keypair = Keypair::ed25519_from_bytes(&mut *secret)
.map_err(|err| anyhow!("Invalid bedrock signing key for libp2p identity: {err}"))?;
let local_peer_id = keypair.public().to_peer_id();
@@ -249,7 +253,7 @@ impl Libp2pNetwork {
}
}
impl PeerNetworkTrait for Libp2pNetwork {
impl PeerNetworkTrait for GossipNetwork {
fn connected_peers(&self) -> Vec<[u8; 32]> {
self.connected_rx.borrow().clone()
}
@@ -259,7 +263,7 @@ impl PeerNetworkTrait for Libp2pNetwork {
}
}
impl Drop for Libp2pNetwork {
impl Drop for GossipNetwork {
fn drop(&mut self) {
self.shutdown.cancel();
}
@@ -559,10 +563,10 @@ mod tests {
#[tokio::test]
async fn start_binds_and_reports_listen_addr() {
let network = Libp2pNetwork::start(
let network = GossipNetwork::start(
test_config(),
[1; 32],
[9; 32],
Ed25519Key::from_bytes(&[9; 32]),
test_mempool_handle(),
TEST_MAX_BLOCK_SIZE,
)
@@ -581,10 +585,10 @@ mod tests {
..test_config()
};
assert!(
Libp2pNetwork::start(
GossipNetwork::start(
config,
[1; 32],
[9; 32],
Ed25519Key::from_bytes(&[9; 32]),
test_mempool_handle(),
TEST_MAX_BLOCK_SIZE
)
@@ -600,10 +604,10 @@ mod tests {
..test_config()
};
assert!(
Libp2pNetwork::start(
GossipNetwork::start(
config,
[1; 32],
[9; 32],
Ed25519Key::from_bytes(&[9; 32]),
test_mempool_handle(),
TEST_MAX_BLOCK_SIZE
)
@@ -614,10 +618,10 @@ mod tests {
#[tokio::test]
async fn drop_cancels_driver() {
let network = Libp2pNetwork::start(
let network = GossipNetwork::start(
test_config(),
[1; 32],
[9; 32],
Ed25519Key::from_bytes(&[9; 32]),
test_mempool_handle(),
TEST_MAX_BLOCK_SIZE,
)
+11 -6
View File
@@ -8,7 +8,7 @@ use testnet_initial_state::{initial_pub_accounts_private_keys, initial_public_us
use crate::{
TransactionOrigin,
config::GossipConfig,
gossip::{Libp2pNetwork, PeerNetworkTrait as _},
gossip::{GossipNetwork, PeerNetworkTrait as _},
};
const CHANNEL: [u8; 32] = [1; 32];
@@ -46,16 +46,21 @@ fn invalidly_signed_transaction() -> LeeTransaction {
async fn start_node(
secret: [u8; 32],
bootstrap: Vec<String>,
) -> (Libp2pNetwork, MemPool<(TransactionOrigin, LeeTransaction)>) {
) -> (GossipNetwork, MemPool<(TransactionOrigin, LeeTransaction)>) {
let config = GossipConfig {
listen_addr: "/ip4/127.0.0.1/udp/0/quic-v1".to_owned(),
bootstrap_peers: bootstrap,
};
let (mempool, mempool_handle) = MemPool::new(1000);
let network =
Libp2pNetwork::start(config, CHANNEL, secret, mempool_handle, TEST_MAX_BLOCK_SIZE)
.await
.expect("node should start");
let network = GossipNetwork::start(
config,
CHANNEL,
Ed25519Key::from_bytes(&secret),
mempool_handle,
TEST_MAX_BLOCK_SIZE,
)
.await
.expect("node should start");
(network, mempool)
}
+19 -27
View File
@@ -15,6 +15,7 @@ pub use sequencer_core::config::*;
use sequencer_core::{
TransactionOrigin,
block_publisher::BlockPublisherTrait as _,
load_or_create_signing_key,
task_group::{StoreRelease, TaskGroup},
};
use sequencer_service_rpc::RpcServer as _;
@@ -43,9 +44,13 @@ pub struct SequencerHandle {
/// handle stops, so watching the count go to zero is how shutdown knows the
/// database file is actually closed rather than assuming it from drop order.
store: StoreRelease,
/// Held for its lifetime: dropping it aborts the gossip drive task. `None`
/// when gossip is unconfigured.
_gossip_network: Option<sequencer_core::gossip::Libp2pNetwork>,
/// Held for its lifetime: dropping it stops the gossip drive task.
/// `None` when gossip is unconfigured.
#[expect(
dead_code,
reason = "never read; kept alive so drop stops the gossip driver"
)]
gossip: Option<sequencer_core::gossip::GossipNetwork>,
}
impl SequencerHandle {
@@ -56,7 +61,7 @@ impl SequencerHandle {
driver_cancellation: CancellationToken,
background_tasks: Vec<TaskGroup>,
store: StoreRelease,
gossip_network: Option<sequencer_core::gossip::Libp2pNetwork>,
gossip: Option<sequencer_core::gossip::GossipNetwork>,
) -> Self {
Self {
addr,
@@ -65,7 +70,7 @@ impl SequencerHandle {
driver_cancellation,
background_tasks,
store,
_gossip_network: gossip_network,
gossip,
}
}
@@ -118,7 +123,7 @@ impl SequencerHandle {
driver_cancellation,
background_tasks: _,
store: _,
_gossip_network: _,
gossip: _,
} = self;
// Cloned rather than taken: `stopped()` consumes a handle, and taking
@@ -152,7 +157,7 @@ impl SequencerHandle {
driver_cancellation,
background_tasks,
store: _,
_gossip_network: _,
gossip: _,
} = self;
let stopped = server_handle.is_stopped()
@@ -179,7 +184,7 @@ impl Drop for SequencerHandle {
driver_cancellation: _,
background_tasks: _,
store: _,
_gossip_network: _,
gossip: _,
} = self;
main_loop_handle.abort();
@@ -240,27 +245,14 @@ pub async fn run(config: SequencerConfig, listen_addr: SocketAddr) -> Result<Seq
let gossip_network = match gossip_config {
None => None,
Some(gossip_config) => {
// On-disk format matches `sequencer_core::load_or_create_signing_key`
// (raw 32-byte secret). `Ed25519Key` has no accessor for the raw
// secret outside the `unsafe` feature, and `Libp2pNetwork::start`
// needs it directly, so this reads the file itself rather than
// going through the KMS type. The node's L1 bedrock signing key is
// deliberately reused as the libp2p identity.
let key_path = sequencer_home.join("bedrock_signing_key");
let secret: [u8; 32] = std::fs::read(&key_path)
.with_context(|| format!("Failed to read {}", key_path.display()))?
.try_into()
.map_err(|bytes: Vec<u8>| {
anyhow!(
"Bedrock signing key has incorrect length: expected 32 bytes, got {}",
bytes.len()
)
})?;
// The node's L1 bedrock signing key is deliberately reused as the
// libp2p identity; `GossipNetwork::start` derives the keypair.
let signing_key = load_or_create_signing_key(&sequencer_home)?;
let channel_id = *bedrock_config.channel_id.as_ref();
let network = sequencer_core::gossip::Libp2pNetwork::start(
let network = sequencer_core::gossip::GossipNetwork::start(
gossip_config,
channel_id,
secret,
signing_key,
mempool_handle.clone(),
max_block_size.as_u64(),
)
@@ -272,7 +264,7 @@ pub async fn run(config: SequencerConfig, listen_addr: SocketAddr) -> Result<Seq
};
let tx_publisher = gossip_network
.as_ref()
.map(sequencer_core::gossip::Libp2pNetwork::tx_publisher);
.map(sequencer_core::gossip::GossipNetwork::tx_publisher);
let driver_cancellation = sequencer_core.block_publisher().driver_cancellation();
// Taken while the core is still owned here: once it is behind the `Arc`
+8 -6
View File
@@ -23,7 +23,7 @@ pub struct SequencerService<BC: BlockPublisherTrait> {
sequencer: Arc<Mutex<SequencerCore<BC>>>,
mempool_handle: MemPoolHandle<(TransactionOrigin, LeeTransaction)>,
max_block_size: u64,
tx_publisher: Option<sequencer_core::gossip::network::TxPublisher>,
gossip_tx_publisher: Option<sequencer_core::gossip::network::TxPublisher>,
}
impl<BC: BlockPublisherTrait> SequencerService<BC> {
@@ -31,13 +31,13 @@ impl<BC: BlockPublisherTrait> SequencerService<BC> {
sequencer: Arc<Mutex<SequencerCore<BC>>>,
mempool_handle: MemPoolHandle<(TransactionOrigin, LeeTransaction)>,
max_block_size: u64,
tx_publisher: Option<sequencer_core::gossip::network::TxPublisher>,
gossip_tx_publisher: Option<sequencer_core::gossip::network::TxPublisher>,
) -> Self {
Self {
sequencer,
mempool_handle,
max_block_size,
tx_publisher,
gossip_tx_publisher,
}
}
}
@@ -105,11 +105,13 @@ impl<BC: BlockPublisherTrait + Send + Sync + 'static> sequencer_service_rpc::Rpc
})?;
// Publish to the gossip mesh before the (blocking) local mempool push so
// a full mempool doesn't delay propagation. Fire-and-forget; the clone
// only happens when gossip is enabled.
if let Some(publisher) = &self.tx_publisher {
// a full mempool doesn't delay propagation.
//
// TODO: may change with actor-based mempool
if let Some(publisher) = &self.gossip_tx_publisher {
publisher.publish(authenticated_tx.clone());
}
self.mempool_handle
.push((TransactionOrigin::User, authenticated_tx))
.await