1use std::io::{self, Cursor};
5use std::path::Path;
6use std::sync::LazyLock;
7
8use ahash::HashMap;
9use anyhow::ensure;
10use async_compression::tokio::write::ZstdEncoder;
11use cid::Cid;
12use futures::stream::FuturesUnordered;
13use futures::{StreamExt, TryStreamExt, stream};
14use fvm_ipld_blockstore::MemoryBlockstore;
15use itertools::Itertools;
16use nunny::Vec as NonEmpty;
17use reqwest::Url;
18use serde::{Deserialize, Serialize};
19use serde_with::{DisplayFromStr, serde_as};
20use tokio::fs::File;
21use tracing::warn;
22
23use crate::daemon::bundle::{ACTOR_BUNDLE_CACHE_DIR, load_actor_bundles_from_server};
24use crate::shim::machine::BuiltinActorManifest;
25use crate::utils::db::car_stream::{CarStream, CarWriter};
26use crate::utils::net::{DownloadFileOption, download_file_with_cache};
27
28use std::str::FromStr;
29
30use super::NetworkChain;
31
32#[derive(Debug)]
33pub struct ActorBundleInfo {
34 pub manifest: Cid,
35 pub url: Url,
36 pub alt_url: Url,
40 pub network: NetworkChain,
41 pub version: String,
42}
43
44macro_rules! actor_bundle_info {
45 ($($cid:literal @ $version:literal for $network:literal),* $(,)?) => {
46 [
47 $(
48 ActorBundleInfo {
49 manifest: $cid.parse().unwrap(),
50 url: concat!(
51 "https://github.com/filecoin-project/builtin-actors/releases/download/",
52 $version,
53 "/builtin-actors-",
54 $network,
55 ".car"
56 ).parse().unwrap(),
57 alt_url: concat!(
58 "https://filecoin-actors.chainsafe.dev/",
59 $version,
60 "/builtin-actors-",
61 $network,
62 ".car"
63 ).parse().unwrap(),
64 network: NetworkChain::from_str($network).unwrap(),
65 version: $version.to_string(),
66 },
67 )*
68 ]
69 }
70}
71
72pub static ACTOR_BUNDLES: LazyLock<Box<[ActorBundleInfo]>> = LazyLock::new(|| {
73 Box::new(actor_bundle_info![
74 "bafy2bzacedrdn6z3z7xz7lx4wll3tlgktirhllzqxb766dxpaqp3ukxsjfsba" @ "8.0.0-rc.1" for "calibrationnet",
75 "bafy2bzacedbedgynklc4dgpyxippkxmba2mgtw7ecntoneclsvvl4klqwuyyy" @ "v9.0.3" for "calibrationnet",
76 "bafy2bzaced25ta3j6ygs34roprilbtb3f6mxifyfnm7z7ndquaruxzdq3y7lo" @ "v10.0.0-rc.1" for "calibrationnet",
77 "bafy2bzacedhuowetjy2h4cxnijz2l64h4mzpk5m256oywp4evarpono3cjhco" @ "v11.0.0-rc2" for "calibrationnet",
78 "bafy2bzacedrunxfqta5skb7q7x32lnp4efz2oq7fn226ffm7fu5iqs62jkmvs" @ "v12.0.0-rc.1" for "calibrationnet",
79 "bafy2bzacebl4w5ptfvuw6746w7ev562idkbf5ppq72e6zub22435ws2rukzru" @ "v12.0.0-rc.2" for "calibrationnet",
80 "bafy2bzacednzb3pkrfnbfhmoqtb3bc6dgvxszpqklf3qcc7qzcage4ewzxsca" @ "v12.0.0" for "calibrationnet",
81 "bafy2bzacea4firkyvt2zzdwqjrws5pyeluaesh6uaid246tommayr4337xpmi" @ "v13.0.0-rc.3" for "calibrationnet",
82 "bafy2bzacect4ktyujrwp6mjlsitnpvuw2pbuppz6w52sfljyo4agjevzm75qs" @ "v13.0.0" for "calibrationnet",
83 "bafy2bzacebq3hncszqpojglh2dkwekybq4zn6qpc4gceqbx36wndps5qehtau" @ "v14.0.0-rc.1" for "calibrationnet",
84 "bafy2bzaceax5zkysst7vtyup4whdxwzlpnaya3qp34rnoi6gyt4pongps7obw" @ "v15.0.0" for "calibrationnet",
85 "bafy2bzacebc7zpsrihpyd2jdcvmegbbk6yhzkifre3hxtoul5wdxxklbwitry" @ "v16.0.0-rc3" for "calibrationnet",
86 "bafy2bzacecqtwq6hjhj2zy5gwjp76a4tpcg2lt7dps5ycenvynk2ijqqyo65e" @ "v16.0.1" for "calibrationnet",
87 "bafy2bzacecn64rlb52rjsvgopnidz6w42z3zobmjxqek5s4xqjh3ly47rcurg" @ "v17.0.0" for "calibrationnet",
88 "bafy2bzacebkfatnbe6w4rj7lf6gkjh7mywlrpdh2dj6hu2dl4rmtwksszm2hs" @ "v18.0.0" for "calibrationnet",
89 "bafy2bzacedzjwguwuihh4tptzfkkwaj3naamrnklbaixn2wfzqh67twwp56pi" @ "v17.0.0" for "butterflynet",
90 "bafy2bzacedg3y3ylp3w4bqmfgyiasrikg3bgfibblc7o4gri3b7j7ftgjhyuu" @ "v18.0.0" for "butterflynet",
91 "bafy2bzacedozk3jh2j4nobqotkbofodq4chbrabioxbfrygpldgoxs3zwgggk" @ "v9.0.3" for "devnet",
92 "bafy2bzacebzz376j5kizfck56366kdz5aut6ktqrvqbi3efa2d4l2o2m653ts" @ "v10.0.0" for "devnet",
93 "bafy2bzaceay35go4xbjb45km6o46e5bib3bi46panhovcbedrynzwmm3drr4i" @ "v11.0.0" for "devnet",
94 "bafy2bzaceasjdukhhyjbegpli247vbf5h64f7uvxhhebdihuqsj2mwisdwa6o" @ "v12.0.0" for "devnet",
95 "bafy2bzacecn7uxgehrqbcs462ktl2h23u23cmduy2etqj6xrd6tkkja56fna4" @ "v13.0.0" for "devnet",
96 "bafy2bzacebwn7ymtozv5yz3x5hnxl4bds2grlgsk5kncyxjak3hqyhslb534m" @ "v14.0.0-rc.1" for "devnet",
97 "bafy2bzacedlusqjwf7chvl2ve2fum5noyqrtjzcrzkhpbzpkg7puiru7dj4ug" @ "v15.0.0-rc1" for "devnet",
98 "bafy2bzaceclp3wfrwdjgh6c3gee5smwj3zmmrhb4fdbc4yfchfaia6rlljx5o" @ "v16.0.1" for "devnet",
99 "bafy2bzaceasvgkke3j4cs3xsxnjswpcdmokkvkiehzxzcgfox3ozlehimbuqk" @ "v17.0.0" for "devnet",
100 "bafy2bzaced35gjxagazf2fne5dakbok5abmivsh7cq7huwfuptebgwjmcpcf6" @ "v18.0.0" for "devnet",
101 "bafy2bzaceb6j6666h36xnhksu3ww4kxb6e25niayfgkdnifaqi6m6ooc66i6i" @ "v9.0.3" for "mainnet",
102 "bafy2bzacecsuyf7mmvrhkx2evng5gnz5canlnz2fdlzu2lvcgptiq2pzuovos" @ "v10.0.0" for "mainnet",
103 "bafy2bzacecnhaiwcrpyjvzl4uv4q3jzoif26okl3m66q3cijp3dfwlcxwztwo" @ "v11.0.0" for "mainnet",
104 "bafy2bzaceapkgfggvxyllnmuogtwasmsv5qi2qzhc2aybockd6kag2g5lzaio" @ "v12.0.0" for "mainnet",
105 "bafy2bzacecdhvfmtirtojwhw2tyciu4jkbpsbk5g53oe24br27oy62sn4dc4e" @ "v13.0.0" for "mainnet",
106 "bafy2bzacecbueuzsropvqawsri27owo7isa5gp2qtluhrfsto2qg7wpgxnkba" @ "v14.0.0" for "mainnet",
107 "bafy2bzaceakwje2hyinucrhgtsfo44p54iw4g6otbv5ghov65vajhxgntr53u" @ "v15.0.0" for "mainnet",
108 "bafy2bzacecnepvsh4lw6pwljobvwm6zwu6mbwveatp7llhpuguvjhjiqz7o46" @ "v16.0.1" for "mainnet",
109 "bafy2bzaceai74ppsvuxs3nvpzzeuptdr3wl7vmdpbphvtz4qt5hfq2qdfvz3e" @ "v17.0.0" for "mainnet",
110 "bafy2bzacedxsqapxwj5znwy4leem5gsuenhhfyqcntews3yis5iek67thy6lc" @ "v18.0.0" for "mainnet",
111 ])
112});
113
114#[serde_as]
115#[derive(Serialize, Deserialize, Debug, PartialEq)]
116pub struct ActorBundleMetadata {
117 pub network: NetworkChain,
118 pub version: String,
119 #[serde_as(as = "DisplayFromStr")]
120 pub bundle_cid: Cid,
121 pub manifest: BuiltinActorManifest,
122}
123
124impl ActorBundleMetadata {
125 pub fn actor_major_version(&self) -> anyhow::Result<u64> {
126 self.version
127 .trim_start_matches('v')
128 .split('.')
129 .next()
130 .ok_or_else(|| anyhow::anyhow!("invalid version"))
131 .and_then(|s| s.parse().map_err(|_| anyhow::anyhow!("invalid version")))
132 }
133}
134
135type ActorBundleMetadataMap = HashMap<(NetworkChain, String), ActorBundleMetadata>;
136
137pub static ACTOR_BUNDLES_METADATA: LazyLock<ActorBundleMetadataMap> = LazyLock::new(|| {
138 let json: &str = include_str!("../../build/manifest.json");
139 let metadata_vec: Vec<ActorBundleMetadata> =
140 serde_json::from_str(json).expect("invalid manifest");
141 metadata_vec
142 .into_iter()
143 .map(|metadata| {
144 (
145 (metadata.network.clone(), metadata.version.clone()),
146 metadata,
147 )
148 })
149 .collect()
150});
151
152pub async fn get_actor_bundles_metadata() -> anyhow::Result<Vec<ActorBundleMetadata>> {
153 let store = MemoryBlockstore::new();
154 for network in [
155 NetworkChain::Mainnet,
156 NetworkChain::Calibnet,
157 NetworkChain::Butterflynet,
158 NetworkChain::Devnet(Default::default()),
159 ] {
160 load_actor_bundles_from_server(&store, &network, &ACTOR_BUNDLES).await?;
161 }
162
163 ACTOR_BUNDLES
164 .iter()
165 .map(|bundle| -> anyhow::Result<_> {
166 Ok(ActorBundleMetadata {
167 network: bundle.network.clone(),
168 version: bundle.version.clone(),
169 bundle_cid: bundle.manifest,
170 manifest: BuiltinActorManifest::load_manifest(&store, &bundle.manifest)?,
171 })
172 })
173 .collect()
174}
175
176pub async fn generate_actor_bundle(output: &Path) -> anyhow::Result<()> {
177 let (mut roots, blocks) = FuturesUnordered::from_iter(ACTOR_BUNDLES.iter().map(
178 |ActorBundleInfo {
179 manifest: root,
180 url,
181 alt_url,
182 network,
183 version,
184 }| async move {
185 let result = if let Ok(response) =
186 download_file_with_cache(url, &ACTOR_BUNDLE_CACHE_DIR, DownloadFileOption::NonResumable).await
187 {
188 response
189 } else {
190 warn!(
191 "failed to download bundle {network}-{version} from primary URL, trying alternative URL"
192 );
193 download_file_with_cache(alt_url, &ACTOR_BUNDLE_CACHE_DIR, DownloadFileOption::NonResumable).await?
194 };
195
196 let bytes = std::fs::read(&result.path)?;
197 let car = CarStream::new(Cursor::new(bytes)).await?;
198 ensure!(car.header_v1.roots.len() == 1);
199 ensure!(car.header_v1.roots.first() == root);
200 anyhow::Ok((*root, car.try_collect::<Vec<_>>().await?))
201 },
202 ))
203 .try_collect::<Vec<_>>()
204 .await?
205 .into_iter()
206 .unzip::<_, _, Vec<_>, Vec<_>>();
207
208 ensure!(roots.iter().all_unique());
209
210 roots.sort(); let mut blocks = blocks.into_iter().flatten().collect_vec();
213 blocks.sort();
214 blocks.dedup();
215
216 for block in blocks.iter() {
217 block.validate()?;
218 }
219
220 stream::iter(blocks)
221 .map(io::Result::Ok)
222 .forward(CarWriter::new_carv1(
223 NonEmpty::new(roots).map_err(|_| anyhow::Error::msg("car roots cannot be empty"))?,
224 ZstdEncoder::with_quality(
225 File::create(&output).await?,
226 async_compression::Level::Precise(17),
227 ),
228 )?)
229 .await?;
230
231 Ok(())
232}
233
234#[cfg(test)]
235mod tests {
236 use http::StatusCode;
237 use reqwest::Response;
238 use std::time::Duration;
239
240 use crate::utils::net::global_http_client;
241
242 use super::*;
243
244 #[tokio::test]
245 async fn check_bundles_are_mirrored() {
246 if std::env::var("CI").is_err() {
249 return;
250 }
251
252 FuturesUnordered::from_iter(ACTOR_BUNDLES.iter().map(
253 |ActorBundleInfo {
254 manifest,
255 url,
256 alt_url,
257 network: _,
258 version: _,
259 }| async move {
260 let (primary, alt) = match (http_get(url).await, http_get(alt_url).await) {
261 (Ok(primary), Ok(alt)) => (primary, alt),
262 (Err(_), Err(_)) => anyhow::bail!("Both sources are down"),
263 _ => return anyhow::Ok(()),
265 };
266
267 assert_ne!(
272 StatusCode::NOT_FOUND,
273 primary.status(),
274 "Could not download {url}"
275 );
276 assert_ne!(
277 StatusCode::NOT_FOUND,
278 alt.status(),
279 "Could not download {alt_url}"
280 );
281
282 if !primary.status().is_success() || !alt.status().is_success() {
285 return anyhow::Ok(());
286 }
287
288 let (primary, alt) = match (primary.bytes().await, alt.bytes().await) {
292 (Ok(primary), Ok(alt)) => (primary, alt),
293 (Err(_), Err(_)) => anyhow::bail!("Both sources are down"),
294 _ => return anyhow::Ok(()),
296 };
297
298 let (car_primary, car_secondary) = tokio::try_join!(
299 CarStream::new(Cursor::new(primary)),
300 CarStream::new(Cursor::new(alt)),
301 )?;
302
303 assert_eq!(
304 car_primary.header_v1.roots, car_secondary.header_v1.roots,
305 "Roots for {url} and {alt_url} do not match"
306 );
307 assert_eq!(
308 car_primary.header_v1.roots.first(),
309 manifest,
310 "Manifest for {url} and {alt_url} does not match"
311 );
312
313 Ok(())
314 },
315 ))
316 .try_collect::<Vec<_>>()
317 .await
318 .unwrap();
319 }
320
321 pub async fn http_get(url: &Url) -> anyhow::Result<Response> {
322 Ok(global_http_client()
323 .get(url.clone())
324 .timeout(Duration::from_secs(120))
325 .send()
326 .await?)
327 }
328
329 #[test]
330 fn test_actor_major_version_correct() {
331 let cases = [
332 ("8.0.0-rc.1", 8),
333 ("v9.0.3", 9),
334 ("v10.0.0-rc.1", 10),
335 ("v12.0.0", 12),
336 ("v13.0.0-rc.3", 13),
337 ("v13.0.0", 13),
338 ("v14.0.0-rc.1", 14),
339 ];
340
341 for (version, expected) in cases.iter() {
342 let metadata = ActorBundleMetadata {
343 network: NetworkChain::Mainnet,
344 version: version.to_string(),
345 bundle_cid: Default::default(),
346 manifest: Default::default(),
347 };
348
349 assert_eq!(metadata.actor_major_version().unwrap(), *expected);
350 }
351 }
352
353 #[test]
354 fn test_actor_major_version_invalid() {
355 let cases = ["cthulhu", "vscode", ".02", "-42"];
356
357 for version in cases.iter() {
358 let metadata = ActorBundleMetadata {
359 network: NetworkChain::Mainnet,
360 version: version.to_string(),
361 bundle_cid: Default::default(),
362 manifest: Default::default(),
363 };
364
365 assert!(metadata.actor_major_version().is_err());
366 }
367 }
368}