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]>
146 lines
5.7 KiB
Rust
146 lines
5.7 KiB
Rust
//! 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()
|
||
);
|
||
}
|