diff --git a/lez/sequencer/core/src/gossip/mod.rs b/lez/sequencer/core/src/gossip/mod.rs index af9fe4530..1c0aa860b 100644 --- a/lez/sequencer/core/src/gossip/mod.rs +++ b/lez/sequencer/core/src/gossip/mod.rs @@ -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; diff --git a/lez/sequencer/core/src/gossip/network.rs b/lez/sequencer/core/src/gossip/network.rs index 8676b687d..6161c99db 100644 --- a/lez/sequencer/core/src/gossip/network.rs +++ b/lez/sequencer/core/src/gossip/network.rs @@ -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>, shutdown: CancellationToken, listen_addrs: Vec, @@ -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 { - 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, ) diff --git a/lez/sequencer/core/src/gossip/tests.rs b/lez/sequencer/core/src/gossip/tests.rs index bf621bbdb..6fff1baf0 100644 --- a/lez/sequencer/core/src/gossip/tests.rs +++ b/lez/sequencer/core/src/gossip/tests.rs @@ -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, -) -> (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) } diff --git a/lez/sequencer/service/src/lib.rs b/lez/sequencer/service/src/lib.rs index 4f7d124e7..bf387c7cc 100644 --- a/lez/sequencer/service/src/lib.rs +++ b/lez/sequencer/service/src/lib.rs @@ -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, + /// 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, } impl SequencerHandle { @@ -56,7 +61,7 @@ impl SequencerHandle { driver_cancellation: CancellationToken, background_tasks: Vec, store: StoreRelease, - gossip_network: Option, + gossip: Option, ) -> 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 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| { - 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 { sequencer: Arc>>, mempool_handle: MemPoolHandle<(TransactionOrigin, LeeTransaction)>, max_block_size: u64, - tx_publisher: Option, + gossip_tx_publisher: Option, } impl SequencerService { @@ -31,13 +31,13 @@ impl SequencerService { sequencer: Arc>>, mempool_handle: MemPoolHandle<(TransactionOrigin, LeeTransaction)>, max_block_size: u64, - tx_publisher: Option, + gossip_tx_publisher: Option, ) -> Self { Self { sequencer, mempool_handle, max_block_size, - tx_publisher, + gossip_tx_publisher, } } } @@ -105,11 +105,13 @@ impl 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