Skip to content

Commit 3fd967b

Browse files
Akagi201Copilot
andcommitted
feat: add HPSVM Mollusk Capability Absorption Design document
- Introduced a new design document outlining the absorption of Mollusk capabilities into the HPSVM product layer. - Defined goals, problem statements, design principles, and recommended approaches for integrating result assertions, fixture handling, CLI tools, and compute-unit reporting. - Proposed workspace structure with new crates: `hpsvm-fixture`, `hpsvm-cli`, and `hpsvm-fixture-fd`. - Detailed the design and public API for `hpsvm-result`, `hpsvm-fixture`, and `hpsvm-cli`, including feature flags and data models. - Outlined a phased rollout plan for implementation and testing expectations. Co-authored-by: Copilot <copilot@github.com>
1 parent 1928fd8 commit 3fd967b

46 files changed

Lines changed: 8171 additions & 3 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[workspace]
22
resolver = "3"
3-
members = ["crates/*"]
3+
members = ["bin/*", "crates/*"]
44

55
[workspace.package]
66
version = "0.1.2"
@@ -11,6 +11,9 @@ repository = "https://github.com/longcipher/hpsvm"
1111
[workspace.dependencies]
1212
# local crates
1313
hpsvm = { path = "crates/hpsvm", version = "0.1.2" }
14+
hpsvm-cli = { path = "bin/hpsvm-cli", version = "0.1.2" }
15+
hpsvm-fixture = { path = "crates/hpsvm-fixture", version = "0.1.2" }
16+
hpsvm-fixture-fd = { path = "crates/hpsvm-fixture-fd", version = "0.1.2" }
1417
hpsvm-loader = { path = "crates/hpsvm-loader", version = "0.1.2" }
1518
hpsvm-token = { path = "crates/token", version = "0.1.2" }
1619

@@ -20,6 +23,8 @@ agave-precompiles = "3.1.14"
2023
agave-reserved-account-keys = "3.1.14"
2124
agave-syscalls = "3.1.14"
2225
ansi_term = "0.12.1"
26+
bincode = "1.3.3"
27+
clap = "4.6.1"
2328
criterion = "0.8.2"
2429
cucumber = "0.23.0"
2530
ed25519-dalek = "=1.0.1"
@@ -30,9 +35,13 @@ indexmap = "2.14.0"
3035
itertools = "0.14.0"
3136
libsecp256k1 = "0.7.2"
3237
log = "0.4.29"
38+
mollusk-svm-fuzz-fixture-firedancer = "0.12.0"
3339
parking_lot = "0.12.5"
40+
prost = "0.14"
3441
qualifier_attr = "0.2.2"
3542
serde = "1.0.228"
43+
serde_json = "1.0.149"
44+
serde_yaml = "0.9.34"
3645
sha2 = "0.11.0"
3746
smallvec = "1.15.1"
3847
solana-account = "3.4.0"

bin/hpsvm-cli/Cargo.toml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[package]
2+
name = "hpsvm-cli"
3+
description = "Command-line fixture tools for hpsvm"
4+
license.workspace = true
5+
version.workspace = true
6+
edition.workspace = true
7+
repository.workspace = true
8+
9+
[[bin]]
10+
name = "hpsvm"
11+
path = "src/main.rs"
12+
13+
[features]
14+
default = []
15+
fd-compat = ["dep:hpsvm-fixture-fd"]
16+
17+
[dependencies]
18+
clap = { workspace = true, features = ["derive"] }
19+
hpsvm.workspace = true
20+
hpsvm-fixture = { workspace = true, features = ["bin-codec", "json-codec", "markdown"] }
21+
hpsvm-fixture-fd = { workspace = true, optional = true }
22+
serde = { workspace = true, features = ["derive"] }
23+
serde_json.workspace = true
24+
serde_yaml.workspace = true
25+
solana-address.workspace = true
26+
thiserror.workspace = true
27+
28+
[dev-dependencies]
29+
mollusk-svm-fuzz-fixture-firedancer.workspace = true
30+
solana-account.workspace = true
31+
solana-keypair.workspace = true
32+
solana-message.workspace = true
33+
solana-sdk-ids.workspace = true
34+
solana-signer.workspace = true
35+
solana-system-interface.workspace = true
36+
solana-transaction.workspace = true
37+
38+
[lints]
39+
workspace = true

bin/hpsvm-cli/src/config.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use std::path::Path;
2+
3+
use hpsvm_fixture::Compare;
4+
5+
use crate::error::CliError;
6+
7+
#[derive(Debug, serde::Deserialize)]
8+
pub(crate) struct CompareConfigFile {
9+
compares: Vec<Compare>,
10+
}
11+
12+
pub(crate) fn load_compares(
13+
path: Option<&Path>,
14+
fallback: &[Compare],
15+
ignore_compute_units: bool,
16+
) -> Result<Vec<Compare>, CliError> {
17+
let mut compares = if let Some(path) = path {
18+
let file = std::fs::read_to_string(path)?;
19+
match path.extension().and_then(|value| value.to_str()) {
20+
Some("yaml" | "yml") => serde_yaml::from_str::<CompareConfigFile>(&file)
21+
.map(|config| config.compares)
22+
.map_err(|error| CliError::ConfigParse {
23+
path: path.display().to_string(),
24+
reason: error.to_string(),
25+
})?,
26+
Some("json") => serde_json::from_str::<CompareConfigFile>(&file)
27+
.map(|config| config.compares)
28+
.map_err(|error| CliError::ConfigParse {
29+
path: path.display().to_string(),
30+
reason: error.to_string(),
31+
})?,
32+
_ => return Err(CliError::UnsupportedConfigFormat { path: path.display().to_string() }),
33+
}
34+
} else {
35+
fallback.to_vec()
36+
};
37+
38+
if ignore_compute_units {
39+
compares.retain(|compare| !matches!(compare, Compare::ComputeUnits));
40+
}
41+
42+
Ok(compares)
43+
}
44+
45+
#[cfg(test)]
46+
mod tests {
47+
use std::path::PathBuf;
48+
49+
use hpsvm_fixture::Compare;
50+
use solana_address::Address;
51+
52+
use super::load_compares;
53+
use crate::error::CliError;
54+
55+
fn temp_config_path(extension: &str) -> PathBuf {
56+
std::env::temp_dir().join(format!(
57+
"hpsvm-cli-compare-config-{}.{}",
58+
Address::new_unique(),
59+
extension
60+
))
61+
}
62+
63+
#[test]
64+
fn load_compares_uses_fallback_and_can_ignore_compute_units() {
65+
let compares = load_compares(None, &[Compare::Status, Compare::ComputeUnits], true)
66+
.expect("fallback compares should load");
67+
68+
assert_eq!(compares, vec![Compare::Status]);
69+
}
70+
71+
#[test]
72+
fn load_compares_reads_yaml_config() {
73+
let path = temp_config_path("yaml");
74+
std::fs::write(&path, "compares:\n - Status\n - ComputeUnits\n")
75+
.expect("yaml config should write");
76+
77+
let compares =
78+
load_compares(Some(&path), &[Compare::Fee], false).expect("yaml compares should load");
79+
80+
assert_eq!(compares, vec![Compare::Status, Compare::ComputeUnits]);
81+
82+
std::fs::remove_file(path).ok();
83+
}
84+
85+
#[test]
86+
fn load_compares_reads_json_config() {
87+
let path = temp_config_path("json");
88+
std::fs::write(&path, r#"{"compares":["Status","Logs"]}"#)
89+
.expect("json config should write");
90+
91+
let compares =
92+
load_compares(Some(&path), &[Compare::Fee], false).expect("json compares should load");
93+
94+
assert_eq!(compares, vec![Compare::Status, Compare::Logs]);
95+
96+
std::fs::remove_file(path).ok();
97+
}
98+
99+
#[test]
100+
fn load_compares_rejects_unsupported_config_format() {
101+
let path = temp_config_path("txt");
102+
std::fs::write(&path, "compares: []").expect("text config should write");
103+
104+
let error = load_compares(Some(&path), &[Compare::Fee], false)
105+
.expect_err("unsupported extension should fail");
106+
107+
assert!(matches!(error, CliError::UnsupportedConfigFormat { .. }));
108+
109+
std::fs::remove_file(path).ok();
110+
}
111+
}

bin/hpsvm-cli/src/cu.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use std::{collections::HashMap, path::Path};
2+
3+
use hpsvm::HPSVM;
4+
use hpsvm_fixture::{ComputeUnitBencher, Fixture, FixtureError};
5+
use solana_address::Address;
6+
7+
use crate::{
8+
FixtureFormatArg,
9+
error::CliError,
10+
fixture::load_fixture,
11+
program_map::{fixture_programs, parse_program_map},
12+
};
13+
14+
pub(crate) fn report_compute_units(
15+
path: &Path,
16+
format: FixtureFormatArg,
17+
output_dir: &Path,
18+
baseline_dir: Option<&Path>,
19+
program_args: &[String],
20+
must_pass: bool,
21+
) -> Result<(), CliError> {
22+
let fixture = load_fixture(path, format)?;
23+
let programs = parse_program_map(program_args)?;
24+
let vm = preload_vm(HPSVM::new(), &fixture, &programs)?;
25+
let case_name = fixture.header.name.clone();
26+
27+
let mut bencher = ComputeUnitBencher::new(vm)
28+
.case((case_name.as_str(), &fixture))
29+
.output_dir(output_dir)
30+
.must_pass(must_pass);
31+
32+
if let Some(baseline_dir) = baseline_dir {
33+
bencher = bencher.baseline_dir(baseline_dir);
34+
}
35+
36+
let report = bencher.execute()?;
37+
38+
if report.rows.iter().all(|row| row.pass) {
39+
println!("PASS: {}", fixture.header.name);
40+
return Ok(());
41+
}
42+
43+
eprintln!("FAIL: {}", fixture.header.name);
44+
std::process::exit(1)
45+
}
46+
47+
fn preload_vm(
48+
mut vm: HPSVM,
49+
fixture: &Fixture,
50+
programs: &HashMap<Address, Vec<u8>>,
51+
) -> Result<HPSVM, CliError> {
52+
for binding in fixture_programs(fixture) {
53+
let Some(bytes) = programs.get(&binding.program_id) else {
54+
return Err(FixtureError::MissingProgramElf { program_id: binding.program_id }.into());
55+
};
56+
vm.add_program_with_loader(binding.program_id, bytes, binding.loader_id)?;
57+
}
58+
59+
Ok(vm)
60+
}

bin/hpsvm-cli/src/error.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use hpsvm::error::HPSVMError;
2+
use hpsvm_fixture::BenchError;
3+
use thiserror::Error;
4+
5+
#[derive(Error, Debug)]
6+
pub(crate) enum CliError {
7+
#[error(transparent)]
8+
Bench(#[from] BenchError),
9+
#[error(transparent)]
10+
Hpsvm(#[from] HPSVMError),
11+
#[error(transparent)]
12+
Fixture(#[from] hpsvm_fixture::FixtureError),
13+
#[cfg(feature = "fd-compat")]
14+
#[error(transparent)]
15+
Firedancer(#[from] hpsvm_fixture_fd::AdapterError),
16+
#[error(transparent)]
17+
Io(#[from] std::io::Error),
18+
#[error(transparent)]
19+
Json(#[from] serde_json::Error),
20+
#[error("failed to parse config {path}: {reason}")]
21+
ConfigParse { path: String, reason: String },
22+
#[error("unsupported config format for {path}")]
23+
UnsupportedConfigFormat { path: String },
24+
#[error("invalid --program value {value}, expected <program-id>=<path>")]
25+
InvalidProgramMapping { value: String },
26+
#[error("invalid program id {value}: {reason}")]
27+
InvalidProgramId { value: String, reason: String },
28+
#[error("no fixture files found in directory {path}")]
29+
NoFixturesInDirectory { path: String },
30+
#[cfg(not(feature = "fd-compat"))]
31+
#[error("firedancer fixture compatibility requires the hpsvm-cli `fd-compat` feature")]
32+
FiredancerCompatDisabled,
33+
}

0 commit comments

Comments
 (0)