1use std::{
6 collections::BTreeMap,
7 ffi::OsString,
8 fmt, fs,
9 net::TcpListener,
10 path::{Path, PathBuf},
11 process::Command,
12 sync::{Arc, RwLock},
13};
14
15use anyhow::{anyhow, bail};
16use axum::{
17 Extension, Json, Router,
18 extract::{Query, State},
19 middleware::{self, Next},
20 response::{IntoResponse, Response},
21 routing::get,
22};
23use hyper::{
24 HeaderMap, StatusCode,
25 http::{HeaderName, HeaderValue, Method},
26};
27use iota_metrics::RegistryService;
28use iota_move::manage_package::resolve_lock_file_path;
29use iota_move_build::{BuildConfig, IotaPackageHooks, ProtocolBuildConfig, implicit_deps};
30use iota_package_management::system_package_versions::latest_system_packages;
31use iota_sdk::{IotaClientBuilder, rpc_types::IotaTransactionBlockEffects};
32use iota_sdk_types::ObjectId;
33use iota_source_validation::{BytecodeSourceVerifier, ValidationMode};
34use move_core_types::account_address::AccountAddress;
35use move_package::{BuildConfig as MoveBuildConfig, LintFlag};
36use move_symbol_pool::Symbol;
37use prometheus_filtered::{IntCounter, MetricLevel, Registry, register_int_counter_with_registry};
38use serde::{Deserialize, Serialize};
39use tokio::sync::oneshot::Sender;
40use tower::ServiceBuilder;
41use tracing::{debug, info};
42use url::Url;
43
44pub const HOST_PORT_ENV: &str = "HOST_PORT";
45pub const IOTA_SOURCE_VALIDATION_VERSION_HEADER: &str = "x-iota-source-validation-version";
46pub const IOTA_SOURCE_VALIDATION_VERSION: &str = "0.1";
47
48pub const MAINNET_URL: &str = "https://api.mainnet.iota.cafe:443";
49pub const TESTNET_URL: &str = "https://api.testnet.iota.cafe:443";
50pub const DEVNET_URL: &str = "https://api.devnet.iota.cafe:443";
51pub const LOCALNET_URL: &str = "http://127.0.0.1:9000";
52
53pub const METRICS_ROUTE: &str = "/metrics";
54pub const METRICS_HOST_PORT: &str = "0.0.0.0:9184";
55
56pub fn host_port() -> String {
57 match option_env!("HOST_PORT") {
58 Some(v) => v.to_string(),
59 None => String::from("0.0.0.0:8000"),
60 }
61}
62
63#[derive(Clone, Deserialize, Debug)]
64pub struct Config {
65 pub packages: Vec<PackageSource>,
66}
67
68#[derive(Clone, Deserialize, Debug)]
69#[serde(tag = "source", content = "values")]
70pub enum PackageSource {
71 Repository(RepositorySource),
72 Directory(DirectorySource),
73}
74
75#[derive(Clone, Deserialize, Debug)]
76pub struct RepositorySource {
77 pub repository: String,
78 pub network: Option<Network>,
79 pub branches: Vec<Branch>,
80}
81
82#[derive(Clone, Deserialize, Debug)]
83pub struct Branch {
84 pub branch: String,
85 pub paths: Vec<Package>,
86}
87
88#[derive(Clone, Deserialize, Debug)]
89pub struct DirectorySource {
90 pub paths: Vec<Package>,
91 pub network: Option<Network>,
92}
93
94#[derive(Clone, Deserialize, Debug)]
95pub struct Package {
96 pub path: String,
97 pub watch: Option<ObjectId>,
101}
102
103#[derive(Clone, Serialize, Debug)]
104pub struct SourceInfo {
105 pub path: PathBuf,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub source: Option<String>,
109}
110
111#[derive(Eq, PartialEq, Clone, Default, Serialize, Deserialize, Debug, Ord, PartialOrd)]
112#[serde(rename_all = "lowercase")]
113pub enum Network {
114 #[default]
115 #[serde(alias = "Mainnet")]
116 Mainnet,
117 #[serde(alias = "Testnet")]
118 Testnet,
119 #[serde(alias = "Devnet")]
120 Devnet,
121 #[serde(alias = "Localnet")]
122 Localnet,
123}
124
125impl fmt::Display for Network {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 write!(
128 f,
129 "{}",
130 match self {
131 Network::Mainnet => "mainnet",
132 Network::Testnet => "testnet",
133 Network::Devnet => "devnet",
134 Network::Localnet => "localnet",
135 }
136 )
137 }
138}
139
140pub type SourceLookup = BTreeMap<Symbol, SourceInfo>;
142pub type AddressLookup = BTreeMap<AccountAddress, SourceLookup>;
144pub type NetworkLookup = BTreeMap<Network, AddressLookup>;
147
148pub async fn verify_package(
149 network: &Network,
150 package_path: impl AsRef<Path>,
151) -> anyhow::Result<(Network, AddressLookup)> {
152 move_package::package_hooks::register_package_hooks(Box::new(IotaPackageHooks));
153 let network_url = match network {
155 Network::Mainnet => MAINNET_URL,
156 Network::Testnet => TESTNET_URL,
157 Network::Devnet => DEVNET_URL,
158 Network::Localnet => LOCALNET_URL,
159 };
160 let client = IotaClientBuilder::default().build(network_url).await?;
161 let chain_id = client.read_api().get_chain_identifier().await?;
162 let protocol_config = client.read_api().get_protocol_config(None).await?;
163 let mut config =
164 resolve_lock_file_path(MoveBuildConfig::default(), Some(package_path.as_ref()))?;
165 config.lint_flag = LintFlag::LEVEL_NONE;
166 config.silence_warnings = true;
167 config.implicit_dependencies = implicit_deps(latest_system_packages());
168 let build_config = BuildConfig {
169 config,
170 run_bytecode_verifier: false, print_diags_to_stderr: false,
172 chain_id: Some(chain_id),
173 protocol_build_config: ProtocolBuildConfig::from(&protocol_config),
174 };
175 let compiled_package = build_config.build(package_path.as_ref())?;
176
177 BytecodeSourceVerifier::new(client.read_api())
178 .verify(&compiled_package, ValidationMode::root())
179 .await
180 .map_err(|e| anyhow!("Network {network}: {e}"))?;
181
182 let mut address_map = AddressLookup::new();
183 let address = compiled_package
184 .published_at
185 .as_ref()
186 .map(|id| AccountAddress::new(id.into_bytes()))
187 .map_err(|_| anyhow!("could not resolve published-at field in package manifest"))?;
188 info!("verifying {} at {address}", package_path.as_ref().display());
189 for v in &compiled_package.package.root_compiled_units {
190 let path = v.source_path.to_path_buf();
191 let source = Some(fs::read_to_string(path.as_path())?);
192 let name = v.unit.name;
193 if let Some(existing) = address_map.get_mut(&address) {
194 existing.insert(name, SourceInfo { path, source });
195 } else {
196 let mut source_map = SourceLookup::new();
197 source_map.insert(name, SourceInfo { path, source });
198 address_map.insert(address, source_map);
199 }
200 }
201 Ok((network.clone(), address_map))
202}
203
204pub fn parse_config(config_path: impl AsRef<Path>) -> anyhow::Result<Config> {
205 let contents = fs::read_to_string(config_path)?;
206 Ok(toml::from_str(&contents)?)
207}
208
209pub fn repo_name_from_url(url: &str) -> anyhow::Result<String> {
210 let repo_url = Url::parse(url)?;
211 let mut components = repo_url
212 .path_segments()
213 .ok_or_else(|| anyhow!("Could not discover repository path in url {url}"))?;
214 let repo_name = components
215 .next_back()
216 .ok_or_else(|| anyhow!("Could not discover repository name in url {url}"))?;
217 Ok(repo_name.to_string())
218}
219
220#[derive(Debug)]
221pub struct CloneCommand {
224 args: Vec<Vec<OsString>>,
226 repo_url: String,
228}
229
230impl CloneCommand {
231 pub fn new(p: &RepositorySource, b: &Branch, dest: &Path) -> anyhow::Result<CloneCommand> {
232 let repo_name = repo_name_from_url(&p.repository)?;
233 let network = p.network.clone().unwrap_or_default().to_string();
234 let sanitized_branch = b.branch.replace('/', "__");
235 let dest = dest
236 .join(network)
237 .join(format!("{repo_name}__{sanitized_branch}"))
238 .into_os_string();
239
240 macro_rules! ostr {
241 ($arg:expr) => {
242 OsString::from($arg)
243 };
244 }
245
246 let mut args = vec![];
247 let cmd_args: Vec<OsString> = vec![
249 ostr!("clone"),
250 ostr!("--no-checkout"),
251 ostr!("--depth=1"), ostr!("--filter=tree:0"),
253 ostr!(format!("--branch={}", b.branch)),
254 ostr!(&p.repository),
255 ostr!(dest.clone()),
256 ];
257 args.push(cmd_args);
258
259 let mut cmd_args: Vec<OsString> = vec![
261 ostr!("-C"),
262 dest.clone(),
263 ostr!("sparse-checkout"),
264 ostr!("set"),
265 ostr!("--no-cone"),
266 ];
267 let path_args: Vec<OsString> = b
268 .paths
269 .iter()
270 .map(|p| OsString::from(p.path.clone()))
271 .collect();
272 cmd_args.extend_from_slice(&path_args);
273 args.push(cmd_args);
274
275 let cmd_args: Vec<OsString> = vec![ostr!("-C"), dest, ostr!("checkout")];
277 args.push(cmd_args);
278
279 Ok(Self {
280 args,
281 repo_url: p.repository.clone(),
282 })
283 }
284
285 pub async fn run(&self) -> anyhow::Result<()> {
286 for args in &self.args {
287 let result = Command::new("git").args(args).output().map_err(|_| {
288 anyhow!(
289 "Error cloning {} with command `git {:#?}`",
290 self.repo_url,
291 args
292 )
293 })?;
294 if !result.status.success() {
295 bail!(
296 "Nonzero exit status when cloning {} with command `git {:#?}`. \
297 Stderr: {}",
298 self.repo_url,
299 args,
300 String::from_utf8_lossy(&result.stderr)
301 )
302 }
303 }
304 Ok(())
305 }
306}
307
308pub async fn clone_repositories(repos: Vec<&RepositorySource>, dir: &Path) -> anyhow::Result<()> {
311 let mut tasks = vec![];
312 for p in &repos {
313 for b in &p.branches {
314 let command = CloneCommand::new(p, b, dir)?;
315 info!(
316 "cloning {}:{} to {}",
317 &p.repository,
318 &b.branch,
319 dir.display()
320 );
321 let t = tokio::spawn(async move { command.run().await });
322 tasks.push(t);
323 }
324 }
325
326 for t in tasks {
327 t.await.unwrap()?;
328 }
329 Ok(())
330}
331
332pub async fn initialize(
333 config: &Config,
334 dir: &Path,
335) -> anyhow::Result<(NetworkLookup, NetworkLookup)> {
336 let mut repos = vec![];
337 for s in &config.packages {
338 match s {
339 PackageSource::Repository(r) => repos.push(r),
340 PackageSource::Directory(_) => (), }
342 }
343 clone_repositories(repos, dir).await?;
344 let sources = verify_packages(config, dir).await?;
345 let sources_list = sources_list(&sources).await;
346 Ok((sources, sources_list))
347}
348
349pub async fn sources_list(sources: &NetworkLookup) -> NetworkLookup {
350 let mut sources_list = NetworkLookup::new();
351 for (network, addresses) in sources {
352 let mut address_map = AddressLookup::new();
353 for (address, symbols) in addresses {
354 let mut symbol_map = SourceLookup::new();
355 for (symbol, source_info) in symbols {
356 symbol_map.insert(
357 *symbol,
358 SourceInfo {
359 path: source_info.path.file_name().unwrap().into(),
360 source: None,
361 },
362 );
363 }
364 address_map.insert(*address, symbol_map);
365 }
366 sources_list.insert(network.clone(), address_map);
367 }
368 sources_list
369}
370
371pub async fn verify_packages(config: &Config, dir: &Path) -> anyhow::Result<NetworkLookup> {
372 let mut tasks = vec![];
373 for p in &config.packages {
374 match p {
375 PackageSource::Repository(r) => {
376 let repo_name = repo_name_from_url(&r.repository)?;
377 let network_name = r.network.clone().unwrap_or_default().to_string();
378 for b in &r.branches {
379 for p in &b.paths {
380 let sanitized_branch = b.branch.replace('/', "__");
381 let package_path = dir
382 .join(network_name.clone())
383 .join(format!("{repo_name}__{sanitized_branch}"))
384 .join(p.path.clone())
385 .clone();
386 let network = r.network.clone().unwrap_or_default();
387 let t =
388 tokio::spawn(
389 async move { verify_package(&network, package_path).await },
390 );
391 tasks.push(t)
392 }
393 }
394 }
395 PackageSource::Directory(packages_dir) => {
396 for p in &packages_dir.paths {
397 let package_path = PathBuf::from(p.path.clone());
398 let network = packages_dir.network.clone().unwrap_or_default();
399 let t =
400 tokio::spawn(async move { verify_package(&network, package_path).await });
401 tasks.push(t)
402 }
403 }
404 }
405 }
406
407 let mut mainnet_lookup = AddressLookup::new();
408 let mut testnet_lookup = AddressLookup::new();
409 let mut devnet_lookup = AddressLookup::new();
410 let mut localnet_lookup = AddressLookup::new();
411 for t in tasks {
412 let (network, new_lookup) = t.await.unwrap()?;
413 match network {
414 Network::Mainnet => mainnet_lookup.extend(new_lookup),
415 Network::Testnet => testnet_lookup.extend(new_lookup),
416 Network::Devnet => devnet_lookup.extend(new_lookup),
417 Network::Localnet => localnet_lookup.extend(new_lookup),
418 }
419 }
420 let mut lookup = NetworkLookup::new();
421 lookup.insert(Network::Mainnet, mainnet_lookup);
422 lookup.insert(Network::Testnet, testnet_lookup);
423 lookup.insert(Network::Devnet, devnet_lookup);
424 lookup.insert(Network::Localnet, localnet_lookup);
425 Ok(lookup)
426}
427
428pub async fn watch_for_upgrades(
436 _packages: Vec<PackageSource>,
437 _app_state: Arc<RwLock<AppState>>,
438 _network: Network,
439 _channel: Option<Sender<IotaTransactionBlockEffects>>,
440) -> anyhow::Result<()> {
441 Err(anyhow!(
442 "Fatal: JsonRPC Subscriptions no longer supported. Reimplement without using subscriptions."
443 ))
444}
445
446pub struct AppState {
447 pub sources: NetworkLookup,
448 pub metrics: Option<SourceServiceMetrics>,
449 pub sources_list: NetworkLookup,
450}
451
452pub async fn serve(app_state: Arc<RwLock<AppState>>) -> anyhow::Result<()> {
453 let app = Router::new()
454 .route("/api", get(api_route))
455 .route("/api/list", get(list_route))
456 .layer(
457 ServiceBuilder::new()
458 .layer(
459 tower_http::cors::CorsLayer::new()
460 .allow_methods([Method::GET])
461 .allow_origin(tower_http::cors::Any),
462 )
463 .layer(middleware::from_fn(check_version_header)),
464 )
465 .with_state(app_state);
466 let listener = TcpListener::bind(host_port())?;
467 listener.set_nonblocking(true).unwrap();
468 let listener = tokio::net::TcpListener::from_std(listener)?;
469 axum::serve(listener, app).await?;
470 Ok(())
471}
472
473#[derive(Deserialize)]
474pub struct Request {
475 #[serde(default)]
476 network: Network,
477 address: String,
478 module: String,
479}
480
481#[derive(Serialize, Deserialize)]
482pub struct SourceResponse {
483 pub source: String,
484}
485
486#[derive(Serialize, Deserialize)]
487pub struct ErrorResponse {
488 pub error: String,
489}
490
491async fn api_route(
492 State(app_state): State<Arc<RwLock<AppState>>>,
493 Query(Request {
494 network,
495 address,
496 module,
497 }): Query<Request>,
498) -> impl IntoResponse {
499 debug!("request network={network}&address={address}&module={module}");
500 let symbol = Symbol::from(module);
501 let Ok(address) = AccountAddress::from_hex_literal(&address) else {
502 let error = format!("Invalid hex address {address}");
503 return (
504 StatusCode::BAD_REQUEST,
505 Json(ErrorResponse { error }).into_response(),
506 );
507 };
508
509 let app_state = app_state.read().unwrap();
510 if let Some(metrics) = &app_state.metrics {
511 metrics.total_requests_received.inc();
512 }
513 let source_result = app_state
514 .sources
515 .get(&network)
516 .and_then(|n| n.get(&address))
517 .and_then(|a| a.get(&symbol));
518 if let Some(SourceInfo {
519 source: Some(source),
520 ..
521 }) = source_result
522 {
523 (
524 StatusCode::OK,
525 Json(SourceResponse {
526 source: source.to_owned(),
527 })
528 .into_response(),
529 )
530 } else {
531 (
532 StatusCode::NOT_FOUND,
533 Json(ErrorResponse {
534 error: format!(
535 "No source found for {symbol} at address {address} on network {network}"
536 ),
537 })
538 .into_response(),
539 )
540 }
541}
542
543async fn check_version_header(
544 headers: HeaderMap,
545 req: hyper::Request<axum::body::Body>,
546 next: Next,
547) -> Response {
548 let version = headers
549 .get(IOTA_SOURCE_VALIDATION_VERSION_HEADER)
550 .as_ref()
551 .and_then(|h| h.to_str().ok())
552 .map(|s| s.to_string());
553
554 match version {
555 Some(v) if v != IOTA_SOURCE_VALIDATION_VERSION => {
556 let error = format!(
557 "Unsupported version '{v}' specified in header \
558 {IOTA_SOURCE_VALIDATION_VERSION_HEADER}"
559 );
560 let mut headers = HeaderMap::new();
561 headers.insert(
562 HeaderName::from_static(IOTA_SOURCE_VALIDATION_VERSION_HEADER),
563 HeaderValue::from_static(IOTA_SOURCE_VALIDATION_VERSION),
564 );
565 return (
566 StatusCode::BAD_REQUEST,
567 headers,
568 Json(ErrorResponse { error }).into_response(),
569 )
570 .into_response();
571 }
572 _ => (),
573 }
574 let mut response = next.run(req).await;
575 response.headers_mut().insert(
576 HeaderName::from_static(IOTA_SOURCE_VALIDATION_VERSION_HEADER),
577 HeaderValue::from_static(IOTA_SOURCE_VALIDATION_VERSION),
578 );
579 response
580}
581
582async fn list_route(State(app_state): State<Arc<RwLock<AppState>>>) -> impl IntoResponse {
583 let app_state = app_state.read().unwrap();
584 (
585 StatusCode::OK,
586 Json(app_state.sources_list.clone()).into_response(),
587 )
588}
589
590pub struct SourceServiceMetrics {
591 pub total_requests_received: IntCounter,
592}
593
594impl SourceServiceMetrics {
595 pub fn new(registry: &Registry) -> Self {
596 Self {
597 total_requests_received: register_int_counter_with_registry!(
598 "total_requests",
599 "Total number of requests received by Source Service",
600 registry;
601 MetricLevel::Info
602 )
603 .unwrap(),
604 }
605 }
606}
607
608pub fn start_prometheus_server(listener: TcpListener) -> RegistryService {
609 let registry = Registry::new();
610
611 let registry_service = RegistryService::new(registry);
612
613 let app = Router::new()
614 .route(METRICS_ROUTE, get(iota_metrics::metrics))
615 .layer(Extension(registry_service.clone()));
616
617 tokio::spawn(async move {
618 listener.set_nonblocking(true).unwrap();
619 let listener = tokio::net::TcpListener::from_std(listener).unwrap();
620 axum::serve(listener, app).await.unwrap();
621 });
622
623 registry_service
624}