fix: zero-amount invoices, identity.verify DID extraction, tor service permissions

- Allow zero-amount Lightning invoices (BOLT11 "any amount") by changing
  validation from amount_sats < 1 to amount_sats < 0
- identity.verify now extracts pubkey directly from did:key format instead
  of requiring the DID to belong to a local identity
- tor.create-service writes config to data_dir/tor-config/ instead of
  /var/lib/archipelago/tor/ (owned by debian-tor, not archipelago user)
- Add E2E test script (scripts/run-e2e-tests.sh) covering 47 RPC endpoints
- Add testing plan with results (loop/testing.md)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 09:53:36 +00:00
parent e3aa95a103
commit 0cf71c4115
5 changed files with 862 additions and 25 deletions

View File

@@ -387,8 +387,8 @@ impl RpcHandler {
.and_then(|v| v.as_str())
.unwrap_or("");
if amount_sats < 1 {
return Err(anyhow::anyhow!("Amount must be at least 1 sat"));
if amount_sats < 0 {
return Err(anyhow::anyhow!("Amount must be non-negative"));
}
info!(amount_sats = amount_sats, "Creating Lightning invoice");

View File

@@ -36,7 +36,8 @@ impl RpcHandler {
pub(super) async fn handle_tor_list_services(
&self,
) -> Result<serde_json::Value> {
let services = list_services().await?;
let config_dir = self.config.data_dir.join("tor-config");
let services = list_services(&config_dir).await?;
Ok(serde_json::json!({ "services": services }))
}
@@ -60,7 +61,8 @@ impl RpcHandler {
return Err(anyhow::anyhow!("Invalid service name (alphanumeric, hyphens, underscores only)"));
}
let mut config = load_services_config().await;
let config_dir = self.config.data_dir.join("tor-config");
let mut config = load_services_config(&config_dir).await;
if config.services.iter().any(|s| s.name == name) {
return Err(anyhow::anyhow!("Service '{}' already exists", name));
}
@@ -70,7 +72,7 @@ impl RpcHandler {
local_port,
enabled: true,
});
save_services_config(&config).await?;
save_services_config(&config_dir, &config).await?;
debug!("Tor service created: {} -> port {}", name, local_port);
Ok(serde_json::json!({ "created": true, "name": name }))
@@ -87,13 +89,14 @@ impl RpcHandler {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing name"))?;
let mut config = load_services_config().await;
let config_dir = self.config.data_dir.join("tor-config");
let mut config = load_services_config(&config_dir).await;
let before = config.services.len();
config.services.retain(|s| s.name != name);
if config.services.len() == before {
return Err(anyhow::anyhow!("Service '{}' not found", name));
}
save_services_config(&config).await?;
save_services_config(&config_dir, &config).await?;
debug!("Tor service deleted: {}", name);
Ok(serde_json::json!({ "deleted": true, "name": name }))
@@ -116,9 +119,9 @@ impl RpcHandler {
}
/// List all hidden services by scanning the filesystem and merging with config.
async fn list_services() -> Result<Vec<TorService>> {
async fn list_services(config_dir: &std::path::Path) -> Result<Vec<TorService>> {
let base = tor_data_dir();
let config = load_services_config().await;
let config = load_services_config(config_dir).await;
let mut services = Vec::new();
let mut seen = std::collections::HashSet::new();
@@ -186,18 +189,17 @@ fn tor_data_dir() -> String {
std::env::var("TOR_DATA_DIR").unwrap_or_else(|_| TOR_DATA_DIR.to_string())
}
async fn load_services_config() -> ServicesConfig {
let path = std::path::Path::new(&tor_data_dir()).join(SERVICES_CONFIG);
async fn load_services_config(config_dir: &std::path::Path) -> ServicesConfig {
let path = config_dir.join(SERVICES_CONFIG);
match tokio::fs::read_to_string(&path).await {
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
Err(_) => ServicesConfig::default(),
}
}
async fn save_services_config(config: &ServicesConfig) -> Result<()> {
let dir = tor_data_dir();
tokio::fs::create_dir_all(&dir).await.context("Failed to create tor data dir")?;
let path = std::path::Path::new(&dir).join(SERVICES_CONFIG);
async fn save_services_config(config_dir: &std::path::Path, config: &ServicesConfig) -> Result<()> {
tokio::fs::create_dir_all(config_dir).await.context("Failed to create tor config dir")?;
let path = config_dir.join(SERVICES_CONFIG);
let content = serde_json::to_string_pretty(config).context("Failed to serialize services config")?;
tokio::fs::write(&path, content).await.context("Failed to write services config")?;
Ok(())