open-standards/sandbox/sandbox-rs/crates/sandbox-exec-microvm/examples/boot_microvm.rs
Transaction Science 490a592223 sandbox-exec-microvm, sandbox-exec-wasm, sandbox-meter: grade nominal-cost figures Estimator
Three reference estimators priced work by a coefficient no host
calibrated and graded the figure ModelBased:
- sandbox-exec-microvm: with no live CPU counter, vCPU-seconds × the
  reference 12,000 µJ/vCPU-second (a nominal work-unit cost), and the
  accelerator's "coarse proxy" (that cost × 10);
- sandbox-exec-wasm: fuel × the reference 20,000 µJ per million fuel;
- sandbox-meter StorageActivity (from_samples, from_volume): ops and
  bytes × nominal NVMe-class pJ coefficients documented as "calibrated".
By the family's ladder (JCP SPEC §5.1) a model earns ModelBased by a
calibration on the host; a nominal coefficient is the whole of such a
model, so the figure is an estimate.

- MicroVmConfig.calibrated and WasmConfig.fuel_cost_calibrated (both
  false by default) keep a ModelBased path for a cost the operator
  calibrated on the host, as spec/receipts.md's "calibrated table"
  describes; the accelerator proxy is Estimator either way. Neither config
  is constructed outside its crate.
- Tests that pinned the over-claim now pin Estimator:
  cpu_only_run_meters_modelbased (renamed ..._an_estimate),
  a_cpu_counter_that_held_across_a_run_is_not_a_measured_zero, power.rs's
  two RAPL fallback tests, runs_a_unit_and_meters_cpu_energy,
  storage_globs_enforced_through_execution, contention_is_loaded_minus_
  baseline. New tests: a fallback is a model only at a host-calibrated
  cost (CPU) and never for the proxy (accelerator); the same for the fuel
  cost; both storage constructors are estimates.
- boot_microvm example's 15 W/vCPU fallback is Estimator too.
- SPEC §6.3, spec/receipts.md, SECURITY.md, README and sandbox-meter's
  Provenance docs say a nominal or assumed coefficient is Estimator.
No vector changes: sandbox-conformance passes every class, file-backed
vectors included, and conformance/v1 is untouched (its builders use
fixed constructors, none of these estimators).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 10:11:54 -04:00

146 lines
5.7 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Boot a real Firecracker microVM through the reference [`FirecrackerVmm`]
//! driver, meter the boot's CPU energy on real hardware (RAPL via MSR), and
//! seal the reading as a Sandbox JCR-1 receipt — the S5 executor's full path,
//! end to end, on a Linux KVM host.
//!
//! ```bash
//! # on a Linux KVM host (e.g. AWS *.metal), as root for MSR access:
//! sudo FC_BIN=/usr/local/bin/firecracker \
//! KERNEL=/tmp/vmlinux.bin ROOTFS=/tmp/rootfs.ext4 \
//! ./boot_microvm
//! ```
//!
//! It spawns `firecracker --api-sock <sock>`, drives `PUT /boot-source`,
//! `/drives/rootfs`, `/machine-config`, and the `InstanceStart` action through
//! our unix-socket HTTP/1.1 client, lets the guest run briefly, and reads the
//! package energy counters (every package's, and every die's) before and
//! after. A genuine `HwShunt` figure where the MSRs read and every package's
//! counter proved live; an honest vCPU-seconds `Estimator` figure otherwise
//! (a per-vCPU-second power no host calibrated).
use sandbox_exec_microvm::firecracker::{FirecrackerConfig, FirecrackerVmm, VmState, Vmm};
use sandbox_exec_microvm::power::PowerMeter;
use sandbox_meter::{DeviceEnergy, Provenance, Receipt};
use sandbox_schema::{ContentId, DeviceClass, Tier};
use std::path::Path;
use std::process::Command;
use std::time::{Duration, Instant};
fn env(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.into())
}
fn main() {
let fc_bin = env("FC_BIN", "/usr/local/bin/firecracker");
let kernel = env("KERNEL", "/tmp/vmlinux.bin");
let rootfs = env("ROOTFS", "/tmp/rootfs.ext4");
let run_secs: f64 = env("RUN_SECS", "4").parse().unwrap_or(4.0);
let sock = "/tmp/fc-demo.sock";
let _ = std::fs::remove_file(sock);
// Spawn the Firecracker process listening on the API socket.
let mut child = Command::new(&fc_bin)
.args(["--api-sock", sock])
.spawn()
.unwrap_or_else(|e| panic!("spawn {fc_bin}: {e}"));
for _ in 0..100 {
if Path::new(sock).exists() {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
// The meter: real RAPL via MSR where available (root on Nitro metal),
// every package's and every die's, once each has proved live; else None
// → an honest vCPU-seconds estimate (Estimator).
#[cfg(target_os = "linux")]
let meter: Option<sandbox_exec_microvm::power::MsrRaplMeter> =
match sandbox_exec_microvm::power::MsrRaplMeter::host() {
Ok(m) => Some(m),
Err(why) => {
println!("no MSR meter: {why}");
None
}
};
#[cfg(not(target_os = "linux"))]
let meter: Option<sandbox_exec_microvm::power::NoMeter> = None;
let before = meter.as_ref().and_then(|m| m.sample_uj());
println!(
"meter: {}",
if before.is_some() { "RAPL via MSR (HwShunt)" } else { "none — Estimator fallback" }
);
let cfg = FirecrackerConfig {
api_socket: sock.into(),
kernel_image_path: kernel,
rootfs_path: rootfs,
boot_args: "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw".into(),
vcpus: 2,
mem_mib: 256,
};
let t0 = Instant::now();
let outcome = FirecrackerVmm.run(&cfg);
match &outcome {
Ok(o) => println!("boot: {:?} ({} vcpu-s attributed)", o.state, o.vcpu_seconds),
Err(e) => println!("boot error: {e}"),
}
// Let the guest run so the metered window covers real execution.
std::thread::sleep(Duration::from_secs_f64(run_secs));
let after = meter.as_ref().and_then(|m| m.sample_uj());
let wall = t0.elapsed();
let _ = child.kill();
let _ = child.wait();
let _ = std::fs::remove_file(sock);
let booted = matches!(outcome.map(|o| o.state), Ok(VmState::Booted | VmState::Completed));
println!("guest ran {:.2}s; booted={booted}", wall.as_secs_f64());
// Build the energy record: HwShunt from the MSR delta, else an estimate.
let device = match (before, after) {
(Some(a), Some(b)) => {
let uj = b.saturating_sub(a);
println!("CPU energy (RAPL, every package, MSR): {:.3} J over the window", uj as f64 / 1e6);
DeviceEnergy {
class: DeviceClass::Cpu,
measured_uj: Some(uj),
estimated_uj: uj,
provenance: Provenance::HwShunt,
}
}
_ => {
// No hardware: vCPU-seconds × a coarse per-vCPU-second figure (15 W),
// assumed, not calibrated on this host: an estimate.
let est = (cfg.vcpus as u64).saturating_mul(wall.as_secs().max(1)).saturating_mul(15_000_000);
println!("CPU energy (estimate): {:.3} J", est as f64 / 1e6);
DeviceEnergy {
class: DeviceClass::Cpu,
measured_uj: None,
estimated_uj: est,
provenance: Provenance::Estimator,
}
}
};
// Seal it as a Sandbox S5 receipt.
let receipt = Receipt::build(
ContentId::of_bytes(b"firecracker microvm boot: vmlinux + bionic rootfs"),
Tier::S5,
vec![device],
1,
[9u8; 8],
)
.expect("receipt builds");
let sk = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]);
let envelope = receipt.seal("did:sandbox:metal-demo", &sk).expect("seal");
receipt
.verify_sealed(&envelope, &sk.verifying_key(), Some("did:sandbox:metal-demo"))
.expect("verify");
std::fs::write("/tmp/microvm-receipt.cose", &envelope).ok();
println!(
"sealed S5 receipt {} ({} bytes) — verified ✓; written to /tmp/microvm-receipt.cose",
receipt.content_id().0,
envelope.len()
);
}