Split verification logic into separate crate.

This commit is contained in:
FiveMovesAhead 2025-05-02 05:28:32 +01:00
parent f2e67939c0
commit 0f4cf7ad4c
5 changed files with 295 additions and 225 deletions

View File

@ -6,7 +6,7 @@ members = [
"tig-protocol",
"tig-runtime",
"tig-structs",
"tig-utils",
"tig-utils", "tig-verifier",
"tig-worker",
]
exclude = []

View File

@ -1,116 +1,75 @@
use anyhow::{anyhow, Result};
use clap::{arg, ArgAction, Command};
use libloading::Library;
use std::{fs, io::Read, panic, path::PathBuf};
use std::{fs, panic, path::PathBuf};
use tig_challenges::*;
use tig_structs::core::{BenchmarkSettings, OutputData, Solution};
use tig_utils::{compress_obj, dejsonify, jsonify};
#[cfg(feature = "cuda")]
use {
cudarc::{
driver::{CudaModule, CudaStream, LaunchConfig, PushKernelArg},
driver::{CudaContext, CudaModule, CudaStream, LaunchConfig, PushKernelArg},
nvrtc::Ptx,
runtime::sys::cudaDeviceProp,
runtime::{result::device::get_device_prop, sys::cudaDeviceProp},
},
std::sync::Arc,
};
fn cli() -> Command {
Command::new("tig-runtime")
.about("Computes or verifies solutions")
.about("Executes an algorithm on a single challenge instance")
.arg_required_else_help(true)
.subcommand(
Command::new("compute_solution")
.about("Computes a solution")
.arg(
arg!(<SETTINGS> "Settings json string or path to json file")
.value_parser(clap::value_parser!(String)),
)
.arg(
arg!(<RAND_HASH> "A string used in seed generation")
.value_parser(clap::value_parser!(String)),
)
.arg(arg!(<NONCE> "Nonce value").value_parser(clap::value_parser!(u64)))
.arg(
arg!(<BINARY> "Path to a shared object (*.so) file")
.value_parser(clap::value_parser!(PathBuf)),
)
.arg(
arg!(--ptx [PTX] "Path to a CUDA ptx file")
.value_parser(clap::value_parser!(PathBuf)),
)
.arg(
arg!(--fuel [FUEL] "Optional maximum fuel parameter")
.default_value("2000000000")
.value_parser(clap::value_parser!(u64)),
)
.arg(
arg!(--output [OUTPUT_FILE] "If set, the output data will be saved to this file path (default json")
.value_parser(clap::value_parser!(PathBuf)),
)
.arg(
arg!(--compress [COMPRESS] "If output file is set, the output data will be compressed as zlib")
.action(ArgAction::SetTrue)
)
.arg(
arg!(--gpu [GPU] "Which GPU device to use")
.default_value("0")
.value_parser(clap::value_parser!(usize)),
),
.arg(
arg!(<SETTINGS> "Settings json string or path to json file")
.value_parser(clap::value_parser!(String)),
)
.subcommand(
Command::new("verify_solution")
.about("Verifies a solution")
.arg(
arg!(<SETTINGS> "Settings json string or path to json file")
.value_parser(clap::value_parser!(String)),
)
.arg(
arg!(<RAND_HASH> "A string used in seed generation")
.value_parser(clap::value_parser!(String)),
)
.arg(arg!(<NONCE> "Nonce value").value_parser(clap::value_parser!(u64)))
.arg(
arg!(<SOLUTION> "Solution json string, path to json file, or '-' for stdin")
.value_parser(clap::value_parser!(String)),
)
.arg(
arg!(--ptx [PTX] "Path to a CUDA ptx file")
.value_parser(clap::value_parser!(PathBuf)),
)
.arg(
arg!(--gpu [GPU] "Which GPU device to use")
.default_value("0")
.value_parser(clap::value_parser!(usize)),
),
.arg(
arg!(<RAND_HASH> "A string used in seed generation")
.value_parser(clap::value_parser!(String)),
)
.arg(arg!(<NONCE> "Nonce value").value_parser(clap::value_parser!(u64)))
.arg(
arg!(<BINARY> "Path to a shared object (*.so) file")
.value_parser(clap::value_parser!(PathBuf)),
)
.arg(
arg!(--ptx [PTX] "Path to a CUDA ptx file")
.value_parser(clap::value_parser!(PathBuf)),
)
.arg(
arg!(--fuel [FUEL] "Optional maximum fuel parameter")
.default_value("2000000000")
.value_parser(clap::value_parser!(u64)),
)
.arg(
arg!(--output [OUTPUT_FILE] "If set, the output data will be saved to this file path (default json")
.value_parser(clap::value_parser!(PathBuf)),
)
.arg(
arg!(--compress [COMPRESS] "If output file is set, the output data will be compressed as zlib")
.action(ArgAction::SetTrue)
)
.arg(
arg!(--gpu [GPU] "Which GPU device to use")
.default_value("0")
.value_parser(clap::value_parser!(usize)),
)
}
fn main() {
let matches = cli().get_matches();
if let Err(e) = match matches.subcommand() {
Some(("compute_solution", sub_m)) => compute_solution(
sub_m.get_one::<String>("SETTINGS").unwrap().clone(),
sub_m.get_one::<String>("RAND_HASH").unwrap().clone(),
*sub_m.get_one::<u64>("NONCE").unwrap(),
sub_m.get_one::<PathBuf>("BINARY").unwrap().clone(),
sub_m.get_one::<PathBuf>("ptx").cloned(),
*sub_m.get_one::<u64>("fuel").unwrap(),
sub_m.get_one::<PathBuf>("output").cloned(),
sub_m.get_one::<bool>("compress").unwrap().clone(),
sub_m.get_one::<usize>("gpu").unwrap().clone(),
),
Some(("verify_solution", sub_m)) => verify_solution(
sub_m.get_one::<String>("SETTINGS").unwrap().clone(),
sub_m.get_one::<String>("RAND_HASH").unwrap().clone(),
*sub_m.get_one::<u64>("NONCE").unwrap(),
sub_m.get_one::<String>("SOLUTION").unwrap().clone(),
sub_m.get_one::<PathBuf>("ptx").cloned(),
sub_m.get_one::<usize>("gpu").unwrap().clone(),
),
_ => Err(anyhow!("Invalid subcommand")),
} {
if let Err(e) = compute_solution(
matches.get_one::<String>("SETTINGS").unwrap().clone(),
matches.get_one::<String>("RAND_HASH").unwrap().clone(),
*matches.get_one::<u64>("NONCE").unwrap(),
matches.get_one::<PathBuf>("BINARY").unwrap().clone(),
matches.get_one::<PathBuf>("ptx").cloned(),
*matches.get_one::<u64>("fuel").unwrap(),
matches.get_one::<PathBuf>("output").cloned(),
matches.get_one::<bool>("compress").unwrap().clone(),
matches.get_one::<usize>("gpu").unwrap().clone(),
) {
eprintln!("Error: {}", e);
std::process::exit(1);
}
@ -200,12 +159,12 @@ pub fn compute_solution(
let max_fuel_hex = format!("0x{:016x}", max_fuel);
let modified_ptx = ptx_content.replace("0xdeadbeefdeadbeef", &max_fuel_hex);
let ptx = cudarc::nvrtc::Ptx::from_src(modified_ptx);
let ctx = cudarc::driver::CudaContext::new(gpu_device).unwrap();
let ptx = Ptx::from_src(modified_ptx);
let ctx = CudaContext::new(gpu_device).unwrap();
ctx.set_blocking_synchronize()?;
let module = ctx.load_module(ptx).unwrap();
let stream = ctx.default_stream();
let prop = cudarc::runtime::result::device::get_device_prop(gpu_device as i32).unwrap();
let prop = get_device_prop(gpu_device as i32).unwrap();
let challenge = $c::Challenge::generate_instance(
seed,
@ -308,111 +267,6 @@ pub fn compute_solution(
Ok(())
}
pub fn verify_solution(
settings: String,
rand_hash: String,
nonce: u64,
solution_path: String,
ptx_path: Option<PathBuf>,
gpu_device: usize,
) -> Result<()> {
let settings = load_settings(&settings);
let solution = load_solution(&solution_path);
let seed = settings.calc_seed(&rand_hash, nonce);
let mut err_msg = Option::<String>::None;
macro_rules! dispatch_challenges {
( $( ($c:ident, $cpu_or_gpu:tt) ),+ $(,)? ) => {{
match settings.challenge_id.as_str() {
$(
stringify!($c) => {
dispatch_challenges!(@expand $c, $cpu_or_gpu);
}
)+
_ => panic!("Unsupported challenge"),
}
}};
(@expand $c:ident, cpu) => {{
let challenge = $c::Challenge::generate_instance(
seed,
&settings.difficulty.into(),
).unwrap();
match $c::Solution::try_from(solution) {
Ok(solution) => {
match challenge.verify_solution(&solution) {
Ok(_) => println!("Solution is valid"),
Err(e) => err_msg = Some(format!("Invalid solution: {}", e)),
}
},
Err(_) => err_msg = Some(format!(
"Invalid solution. Cannot convert to {}::Solution",
stringify!($c)
)),
}
}};
(@expand $c:ident, gpu) => {{
#[cfg(not(feature = "cuda"))]
panic!("tig-runtime was not compiled with '--features cuda'");
#[cfg(feature = "cuda")]
{
if ptx_path.is_none() {
panic!("PTX file is required for GPU challenges.");
}
let ptx_path = ptx_path.unwrap();
let ptx_content = std::fs::read_to_string(&ptx_path)
.map_err(|e| anyhow!("Failed to read PTX file: {}", e))?;
let ptx = cudarc::nvrtc::Ptx::from_src(ptx_content);
let ctx = cudarc::driver::CudaContext::new(gpu_device).unwrap();
ctx.set_blocking_synchronize()?;
let module = ctx.load_module(ptx).unwrap();
let stream = ctx.default_stream();
let prop = cudarc::runtime::result::device::get_device_prop(gpu_device as i32).unwrap();
let challenge = $c::Challenge::generate_instance(
seed,
&settings.difficulty.into(),
module.clone(),
stream.clone(),
&prop,
).unwrap();
match $c::Solution::try_from(solution) {
Ok(solution) => {
match challenge.verify_solution(&solution, module.clone(), stream.clone(), &prop) {
Ok(_) => {
stream.synchronize()?;
ctx.synchronize()?;
println!("Solution is valid");
},
Err(e) => err_msg = Some(format!("Invalid solution: {}", e)),
}
},
Err(_) => err_msg = Some(format!(
"Invalid solution. Cannot convert to {}::Solution",
stringify!($c)
)),
}
}
}};
}
dispatch_challenges!((c001, cpu), (c002, cpu), (c003, cpu), (c004, gpu));
if let Some(err_msg) = err_msg {
eprintln!("Verification error: {}", err_msg);
std::process::exit(1);
}
Ok(())
}
fn load_settings(settings: &str) -> BenchmarkSettings {
let settings = if settings.ends_with(".json") {
fs::read_to_string(settings).unwrap_or_else(|_| {
@ -437,28 +291,3 @@ pub fn load_module(path: &PathBuf) -> Result<Library> {
Err(_) => Err(anyhow!("Failed to load module")),
}
}
fn load_solution(solution: &str) -> Solution {
let solution = if solution == "-" {
let mut buffer = String::new();
std::io::stdin()
.read_to_string(&mut buffer)
.unwrap_or_else(|_| {
eprintln!("Failed to read solution from stdin");
std::process::exit(1);
});
buffer
} else if solution.ends_with(".json") {
fs::read_to_string(&solution).unwrap_or_else(|_| {
eprintln!("Failed to read solution file: {}", solution);
std::process::exit(1);
})
} else {
solution.to_string()
};
dejsonify::<Solution>(&solution).unwrap_or_else(|_| {
eprintln!("Failed to parse solution");
std::process::exit(1);
})
}

22
tig-verifier/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "tig-verifier"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
edition.workspace = true
readme.workspace = true
[dependencies]
anyhow = "1.0.81"
clap = { version = "4.5.4" }
cudarc = { version = "0.16.2", features = [
"cuda-version-from-build-system",
], optional = true }
serde = { version = "1.0.196", features = ["derive"] }
serde_json = { version = "1.0.113" }
tig-challenges = { path = "../tig-challenges" }
tig-structs = { path = "../tig-structs" }
tig-utils = { path = "../tig-utils" }
[features]
cuda = ["cudarc", "tig-challenges/cuda"]

220
tig-verifier/src/main.rs Normal file
View File

@ -0,0 +1,220 @@
use anyhow::{anyhow, Result};
use clap::{arg, Command};
use std::{fs, io::Read, panic, path::PathBuf};
use tig_challenges::*;
use tig_structs::core::{BenchmarkSettings, Solution};
use tig_utils::dejsonify;
#[cfg(feature = "cuda")]
use cudarc::{driver::CudaContext, nvrtc::Ptx, runtime::result::device::get_device_prop};
fn cli() -> Command {
Command::new("tig-verifier")
.about("Verifies a solution or merkle proof")
.arg_required_else_help(true)
.subcommand(
Command::new("verify_solution")
.about("Verifies a solution")
.arg(
arg!(<SETTINGS> "Settings json string or path to json file")
.value_parser(clap::value_parser!(String)),
)
.arg(
arg!(<RAND_HASH> "A string used in seed generation")
.value_parser(clap::value_parser!(String)),
)
.arg(arg!(<NONCE> "Nonce value").value_parser(clap::value_parser!(u64)))
.arg(
arg!(<SOLUTION> "Solution json string, path to json file, or '-' for stdin")
.value_parser(clap::value_parser!(String)),
)
.arg(
arg!(--ptx [PTX] "Path to a CUDA ptx file")
.value_parser(clap::value_parser!(PathBuf)),
)
.arg(
arg!(--gpu [GPU] "Which GPU device to use")
.default_value("0")
.value_parser(clap::value_parser!(usize)),
),
)
.subcommand(
Command::new("verify_merkle_proof")
.about("Verifies a merkle proof")
.arg(arg!(<ROOT> "Merkle root").value_parser(clap::value_parser!(String)))
.arg(
arg!(<PROOF> "Merkle proof json string, path to json file, or '-' for stdin")
.value_parser(clap::value_parser!(String)),
),
)
}
fn main() {
let matches = cli().get_matches();
if let Err(e) = match matches.subcommand() {
Some(("verify_solution", sub_m)) => verify_solution(
sub_m.get_one::<String>("SETTINGS").unwrap().clone(),
sub_m.get_one::<String>("RAND_HASH").unwrap().clone(),
*sub_m.get_one::<u64>("NONCE").unwrap(),
sub_m.get_one::<String>("SOLUTION").unwrap().clone(),
sub_m.get_one::<PathBuf>("ptx").cloned(),
sub_m.get_one::<usize>("gpu").unwrap().clone(),
),
Some(("verify_merkle_proof", sub_m)) => verify_merkle_proof(
sub_m.get_one::<String>("ROOT").unwrap().clone(),
sub_m.get_one::<String>("PROOF").unwrap().clone(),
),
_ => Err(anyhow!("Invalid subcommand")),
} {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
pub fn verify_solution(
settings: String,
rand_hash: String,
nonce: u64,
solution_path: String,
ptx_path: Option<PathBuf>,
gpu_device: usize,
) -> Result<()> {
let settings = load_settings(&settings);
let solution = load_solution(&solution_path);
let seed = settings.calc_seed(&rand_hash, nonce);
let mut err_msg = Option::<String>::None;
macro_rules! dispatch_challenges {
( $( ($c:ident, $cpu_or_gpu:tt) ),+ $(,)? ) => {{
match settings.challenge_id.as_str() {
$(
stringify!($c) => {
dispatch_challenges!(@expand $c, $cpu_or_gpu);
}
)+
_ => panic!("Unsupported challenge"),
}
}};
(@expand $c:ident, cpu) => {{
let challenge = $c::Challenge::generate_instance(
seed,
&settings.difficulty.into(),
).unwrap();
match $c::Solution::try_from(solution) {
Ok(solution) => {
match challenge.verify_solution(&solution) {
Ok(_) => println!("Solution is valid"),
Err(e) => err_msg = Some(format!("Invalid solution: {}", e)),
}
},
Err(_) => err_msg = Some(format!(
"Invalid solution. Cannot convert to {}::Solution",
stringify!($c)
)),
}
}};
(@expand $c:ident, gpu) => {{
#[cfg(not(feature = "cuda"))]
panic!("tig-runtime was not compiled with '--features cuda'");
#[cfg(feature = "cuda")]
{
if ptx_path.is_none() {
panic!("PTX file is required for GPU challenges.");
}
let ptx = Ptx::from_file(ptx_path.unwrap());
let ctx = CudaContext::new(gpu_device).unwrap();
ctx.set_blocking_synchronize()?;
let module = ctx.load_module(ptx).unwrap();
let stream = ctx.default_stream();
let prop = get_device_prop(gpu_device as i32).unwrap();
let challenge = $c::Challenge::generate_instance(
seed,
&settings.difficulty.into(),
module.clone(),
stream.clone(),
&prop,
).unwrap();
match $c::Solution::try_from(solution) {
Ok(solution) => {
match challenge.verify_solution(&solution, module.clone(), stream.clone(), &prop) {
Ok(_) => {
stream.synchronize()?;
ctx.synchronize()?;
println!("Solution is valid");
},
Err(e) => err_msg = Some(format!("Invalid solution: {}", e)),
}
},
Err(_) => err_msg = Some(format!(
"Invalid solution. Cannot convert to {}::Solution",
stringify!($c)
)),
}
}
}};
}
dispatch_challenges!((c001, cpu), (c002, cpu), (c003, cpu), (c004, gpu));
if let Some(err_msg) = err_msg {
eprintln!("Verification error: {}", err_msg);
std::process::exit(1);
}
Ok(())
}
pub fn verify_merkle_proof(_merkle_root: String, _merkle_proof: String) -> Result<()> {
// TODO
Err(anyhow!("Merkle proof verification is not implemented yet"))
}
fn load_settings(settings: &str) -> BenchmarkSettings {
let settings = if settings.ends_with(".json") {
fs::read_to_string(settings).unwrap_or_else(|_| {
eprintln!("Failed to read settings file: {}", settings);
std::process::exit(1);
})
} else {
settings.to_string()
};
dejsonify::<BenchmarkSettings>(&settings).unwrap_or_else(|_| {
eprintln!("Failed to parse settings");
std::process::exit(1);
})
}
fn load_solution(solution: &str) -> Solution {
let solution = if solution == "-" {
let mut buffer = String::new();
std::io::stdin()
.read_to_string(&mut buffer)
.unwrap_or_else(|_| {
eprintln!("Failed to read solution from stdin");
std::process::exit(1);
});
buffer
} else if solution.ends_with(".json") {
fs::read_to_string(&solution).unwrap_or_else(|_| {
eprintln!("Failed to read solution file: {}", solution);
std::process::exit(1);
})
} else {
solution.to_string()
};
dejsonify::<Solution>(&solution).unwrap_or_else(|_| {
eprintln!("Failed to parse solution");
std::process::exit(1);
})
}

View File

@ -148,8 +148,7 @@ fn compute_batch(
tokio::spawn(async move {
let temp_file = NamedTempFile::new()?;
let mut cmd = std::process::Command::new(runtime_path);
cmd.arg("compute_solution")
.arg(settings)
cmd.arg(settings)
.arg(rand_hash)
.arg(nonce.to_string())
.arg(binary_path)