From 67e84385484756721da155aadc667e1b1e7587e9 Mon Sep 17 00:00:00 2001 From: Nic Date: Tue, 11 Aug 2026 13:42:01 +0100 Subject: [PATCH] fix(devnet): bind Anvil to --host so LAN clients can reach the manifest rpc_url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --host LAN-exposes the nodes and the manifest server, but --enable-evm left Anvil on localhost and published that loopback URL in the manifest's rpc_url — other LAN devices could join and download but not pay, which is the exact audience --host serves. evmlib already honors ANVIL_IP_ADDR for both the bind and the published URL, so when --host and --enable-evm are set and ANVIL_IP_ADDR is not, default it to the host IP (an explicit override still wins). Verified live on a LAN box: the manifest published http://:/ and eth_chainId answered from another machine with no tunnel. Co-Authored-By: Claude Fable 5 --- src/bin/ant-devnet/cli.rs | 8 +++-- src/bin/ant-devnet/main.rs | 69 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/bin/ant-devnet/cli.rs b/src/bin/ant-devnet/cli.rs index 55a34717..114503a4 100644 --- a/src/bin/ant-devnet/cli.rs +++ b/src/bin/ant-devnet/cli.rs @@ -59,13 +59,17 @@ pub struct Cli { /// Start a local Anvil blockchain for EVM payment verification. /// Starts Anvil, deploys contracts, and configures all nodes to verify - /// payments against the local chain. + /// payments against the local chain. With `--host`, Anvil binds the same + /// LAN IP (unless `ANVIL_IP_ADDR` overrides it) so the manifest's + /// `rpc_url` is reachable from other devices — external signers on the + /// LAN can pay, not just download. #[arg(long)] pub enable_evm: bool, /// Advertise this IPv4 to peers/clients and bind 0.0.0.0, so the devnet is /// reachable from other devices on the LAN. When omitted, nodes bind - /// loopback (127.0.0.1) as before (single-machine only). + /// loopback (127.0.0.1) as before (single-machine only). Also becomes + /// Anvil's bind/publish address under `--enable-evm` (see there). #[arg(long)] pub host: Option, diff --git a/src/bin/ant-devnet/main.rs b/src/bin/ant-devnet/main.rs index a55a0fa0..49dea28c 100644 --- a/src/bin/ant-devnet/main.rs +++ b/src/bin/ant-devnet/main.rs @@ -98,8 +98,13 @@ async fn main() -> color_eyre::Result<()> { )); } config.advertise_ip = cli.host; - let evm_info = - resolve_evm_info(cli.evm_network.as_deref(), cli.enable_evm, &mut config).await?; + let evm_info = resolve_evm_info( + cli.evm_network.as_deref(), + cli.enable_evm, + cli.host, + &mut config, + ) + .await?; let mut devnet = Devnet::new(config).await?; devnet.start().await?; @@ -141,6 +146,7 @@ async fn main() -> color_eyre::Result<()> { async fn resolve_evm_info( evm_network: Option<&str>, enable_evm: bool, + host: Option, config: &mut DevnetConfig, ) -> color_eyre::Result> { if let Some(net_name) = evm_network { @@ -166,6 +172,19 @@ async fn resolve_evm_info( payment_vault_address: vault_addr, })) } else if enable_evm { + // Anvil binds — and evmlib publishes in the manifest's `rpc_url` — + // the address in `ANVIL_IP_ADDR`, defaulting to localhost. A LAN + // devnet must expose the chain the way it exposes the nodes, or the + // published `rpc_url` is unreachable from every other device — + // exactly the audience `--host` serves (external signers can join + // and download, but not pay). An explicit `ANVIL_IP_ADDR` wins. + if let Some(anvil_ip) = anvil_ip_for_lan(host, std::env::var_os("ANVIL_IP_ADDR")) { + ant_node::logging::info!( + "Binding Anvil to {anvil_ip} (via ANVIL_IP_ADDR) so LAN clients \ + can reach the manifest's rpc_url" + ); + std::env::set_var("ANVIL_IP_ADDR", anvil_ip); + } ant_node::logging::info!("Starting local Anvil blockchain for EVM payment enforcement..."); let testnet = evmlib::testnet::Testnet::new() .await @@ -319,3 +338,49 @@ fn spawn_manifest_server( } }); } + +/// The Anvil bind/publish address a LAN devnet should use: the `--host` IP, +/// unless the operator set `ANVIL_IP_ADDR` explicitly (their override wins), +/// or there is no `--host` (loopback devnet — keep Anvil's localhost +/// default). Returns the value to write into `ANVIL_IP_ADDR`, or `None` to +/// leave the environment untouched. +fn anvil_ip_for_lan( + host: Option, + existing_override: Option, +) -> Option { + match (host, existing_override) { + (Some(host), None) => Some(host.to_string()), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::anvil_ip_for_lan; + use std::net::Ipv4Addr; + + #[test] + fn lan_host_becomes_anvil_ip() { + assert_eq!( + anvil_ip_for_lan(Some(Ipv4Addr::new(192, 168, 0, 61)), None), + Some("192.168.0.61".to_string()) + ); + } + + #[test] + fn explicit_override_wins() { + assert_eq!( + anvil_ip_for_lan( + Some(Ipv4Addr::new(192, 168, 0, 61)), + Some("10.0.0.9".into()) + ), + None + ); + } + + #[test] + fn loopback_devnet_keeps_anvil_default() { + assert_eq!(anvil_ip_for_lan(None, None), None); + assert_eq!(anvil_ip_for_lan(None, Some("10.0.0.9".into())), None); + } +}