Temporary freezing of network transactions by delaying one block by 500% or more of the average block time of the preceding 24 hours beyond standard difficulty adjustments
Description
Brief
The base-consensus CLI flag --rpc.enable-admin is parsed (crates/client/cli/src/rpc.rs:32), stored on RpcBuilder.enable_admin (crates/consensus/rpc/src/config.rs:13), and never read by the runtime gate at crates/consensus/service/src/actors/rpc/actor.rs:120-123. The gate is if let Some(network_admin) = network_admin, where network_admin is unconditionally Some(_) for full-node mode (crates/consensus/service/src/service/node.rs:552). The admin namespace is registered on every base-consensus listener regardless of the operator's enable_admin selection, including admin_stopSequencer, which halts block production by setting self.is_active = false (crates/consensus/service/src/actors/sequencer/admin_api_impl.rs:191).
The default consensus RPC binding is 0.0.0.0:9545 (crates/client/cli/src/rpc.rs:25). The middleware stack is timeout + concurrency + load-shed + healthz (actors/rpc/actor.rs:67-89); no authentication. Any caller able to send an HTTP POST to the bound port can call any registered admin method. The defect ships in every base-consensus binary distributed to validators, third-party operators, and HA cluster deployments — not Coinbase-operated infrastructure.
Vulnerability Details
The flag's lifecycle:
// crates/client/cli/src/rpc.rs:31-32 — CLI parsing#[arg(long ="rpc.enable-admin", env ="BASE_NODE_RPC_ENABLE_ADMIN")]pubenable_admin:bool,// crates/client/cli/src/rpc.rs:72 — wired into RpcBuilderenable_admin:args.enable_admin,// crates/consensus/rpc/src/config.rs:13 — field on RpcBuilderpubenable_admin:bool,
The runtime gate that decides whether to register the admin module:
network_admin is unconditionally Some(_) in full-node mode at crates/consensus/service/src/service/node.rs:552. --rpc.enable-admin=false has no runtime effect.
Impact
Severity: HIGH (network-transaction freezing exceeding 500% of average block time).
Methods reachable unauthenticated on the bound listener (mutating only):
An unauthenticated caller who reaches the consensus RPC of a sequencer-mode node can invoke admin_stopSequencer even when the operator did not enable admin RPC. This sets is_active = false; the sequencer build loop only emits new payloads while is_active is true (actor.rs sequencer loop), so block production remains stopped until an authorized operational action restarts sequencing. On Base's 2 s block time, any stop lasting over 10 s maps directly to the 500% one-block-delay HIGH bracket. The bug also exposes admin_overrideLeader (HA conductor leader override), admin_setRecoverMode, admin_resetDerivationPipeline, and admin_startSequencer (the latter is partially gated downstream — it requires the supplied head to match the engine's current unsafe head per admin_api_impl.rs:158-170, so it is restart-only, not arbitrary-head injection).
The --rpc.enable-admin flag is the documented mechanism to separate the admin namespace from the read namespaces (optimism_* always, ws_* when ws_enabled) on the same listener. The HA Conductor Cluster topology distributed in this codebase requires inter-node admin RPC for leader election (admin_overrideLeader exists for this purpose). Operators who deploy mixed-purpose listeners — public read endpoints for downstream consumers + internal admin for HA — rely on the flag to gate the namespaces independently. The dead gate breaks that separation: the admin namespace registers on every listener regardless of operator intent.
The defect is in published binary code, code-review-discoverable, and independent of any operator's deployment. The "Base-operated infrastructure" exclusion does not apply.
4d7d12db chore(cli): full cli arg port (#475) (2026-01-14) introduced the RpcArgs.enable_admin flag. c9758ee1 feat(devnet): Three-Node HA Conductor Cluster (#1542) (2026-03-21) introduced crates/consensus/service/src/actors/rpc/actor.rs with the runtime gate that never reads the flag. Present in v0.7.0-rc.1 through v0.8.0-rc.24; never patched.
Proof of Concept
The test below replicates the runtime gating logic from RpcActor::start (crates/consensus/service/src/actors/rpc/actor.rs:120-123) verbatim, with RpcBuilder.enable_admin: false (the operator's documented opt-out) and network_admin: Some(_) (full-node mode). It launches the same jsonrpsee server RpcActor uses, then probes every admin method. Any response other than -32601 Method not found proves the method is registered on the listener despite enable_admin: false. A control method that genuinely does not exist returns -32601, isolating the registration outcome to the dead gate.
Append to the existing mod tests block of crates/consensus/service/src/actors/rpc/actor.rs. Add serde_json.workspace = true to crates/consensus/service/Cargo.toml under [dev-dependencies].
Verbatim cargo output (against v0.8.0-rc.24 HEAD 819ea306):
All nine admin methods are registered with enable_admin: false; the control returns -32601.
Fix
After this change, the PoC's admin-method probes return -32601 and the control matches. Admin methods become reachable only when the operator explicitly sets --rpc.enable-admin.
// crates/consensus/service/src/actors/rpc/actor.rs:120-123
if let Some(network_admin) = network_admin {
modules
.merge(AdminRpc::new(self.sequencer_admin_rpc_client, network_admin).into_rpc())?;
}
// self.config.enable_admin is NEVER consulted.
use alloy_primitives::B256;
use base_consensus_rpc::{AdminApiServer, AdminRpc, NetworkAdminQuery};
use jsonrpsee::{core::client::ClientT, http_client::HttpClientBuilder, rpc_params};
use tokio::sync::mpsc;
use super::*;
use crate::actors::rpc::sequencer_rpc_client::QueuedSequencerAdminAPIClient;
#[tokio::test]
async fn poc_enable_admin_false_is_a_dead_flag() {
let config = RpcBuilder {
socket: SocketAddr::from(([127, 0, 0, 1], 0)),
no_restart: true,
enable_admin: false, // documented opt-out
admin_persistence: None,
ws_enabled: false,
dev_enabled: false,
http_timeout: Duration::from_secs(10),
max_concurrent_requests: NonZeroUsize::new(1024).unwrap(),
};
let mut modules = RpcModule::new(());
// network_admin is unconditionally Some(_) in full-node mode (node.rs:552).
let (admin_tx, _admin_rx) = mpsc::channel::<NetworkAdminQuery>(1);
let network_admin: Option<mpsc::Sender<NetworkAdminQuery>> = Some(admin_tx);
let sequencer_admin_rpc_client: Option<QueuedSequencerAdminAPIClient> = None;
// Verbatim copy of the buggy gate (actor.rs:120-123).
if let Some(network_admin) = network_admin {
modules
.merge(AdminRpc::new(sequencer_admin_rpc_client, network_admin).into_rpc())
.unwrap();
}
let server = jsonrpsee::server::Server::builder()
.build(config.socket).await.unwrap();
let addr = server.local_addr().unwrap();
let _handle = server.start(modules);
let client = HttpClientBuilder::default()
.build(format!("http://{addr}")).unwrap();
// Probe every admin method. Any response other than -32601 proves registration.
let admin_methods = [
("admin_sequencerActive", rpc_params![]),
("admin_startSequencer", rpc_params![B256::ZERO]),
("admin_stopSequencer", rpc_params![]),
("admin_conductorEnabled", rpc_params![]),
("admin_adminRecoverMode", rpc_params![]),
("admin_setRecoverMode", rpc_params![true]),
("admin_overrideLeader", rpc_params![]),
("admin_resetDerivationPipeline", rpc_params![]),
("admin_postUnsafePayload", rpc_params![serde_json::json!({})]),
];
for (method, params) in admin_methods {
let res: Result<serde_json::Value, _> = client.request(method, params).await;
let s = match &res { Ok(_) => "Ok(_)".to_string(), Err(e) => e.to_string() };
assert!(
!s.contains("-32601") && !s.contains("Method not found"),
"BUG: {method} should be REGISTERED despite enable_admin=false; got: {s}",
);
}
// Control: a fabricated method must return -32601.
let res: Result<(), _> = client
.request("admin_thisMethodDoesNotExist", rpc_params![]).await;
let err = res.expect_err("expected method-not-found").to_string();
assert!(err.contains("-32601") || err.contains("Method not found"));
}
$ cargo test -p base-consensus-node --lib \
actors::rpc::actor::tests::poc_enable_admin_false_is_a_dead_flag
Finished `test` profile [unoptimized + debuginfo] target(s) in 5.24s
Running unittests src/lib.rs (target/debug/deps/base_consensus_node-b4f4d32717b80070)
running 1 test
test actors::rpc::actor::tests::poc_enable_admin_false_is_a_dead_flag ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 183 filtered out;
finished in 0.01s
- if let Some(network_admin) = network_admin {
+ if let Some(network_admin) = network_admin.filter(|_| self.config.enable_admin) {
modules.merge(AdminRpc::new(self.sequencer_admin_rpc_client, network_admin).into_rpc())?;
}