use std::process::Stdio;
use neptune_job_queue::channels::JobCancelReceiver;
use neptune_job_queue::traits::Job;
use neptune_job_queue::JobCompletion;
use neptune_job_queue::JobResultWrapper;
use neptune_primitives::network::Network;
use tasm_lib::maybe_write_debuggable_vm_state_to_disk;
use tasm_lib::triton_vm::error::InstructionError;
use tasm_lib::triton_vm::vm::VMState;
use tokio::io::AsyncWriteExt;
use crate::macros::fn_name;
use crate::macros::log_scope_duration;
use crate::proof_abstractions::tasm::neptune_prover_job::NeptuneProverJob;
use crate::proof_abstractions::triton_vm_env_vars::TritonVmEnvVars;
use crate::proof_abstractions::tx_proving_capability::TxProvingCapability;
use crate::proof_abstractions::Claim;
use crate::proof_abstractions::NonDeterminism;
use crate::proof_abstractions::Program;
use crate::transaction::transaction_proof::TransactionProofType;
use crate::transaction::validity::neptune_proof::Proof;
pub const PROOF_PADDED_HEIGHT_TOO_BIG_PROCESS_OFFSET_ERROR_CODE: i32 = 200;
#[derive(Debug, thiserror::Error)]
pub enum ProverJobError {
#[error("triton-vm program complexity limit exceeded. result: {result}, limit: {limit}")]
ProofComplexityLimitExceeded { limit: u32, result: u32 },
#[error("external proving process failed: {0}")]
TritonVmProverFailed(#[from] VmProcessError),
#[error("machine's capability {capability} is not sufficient to produce proof: {proof_type}")]
TooWeak {
capability: TxProvingCapability,
proof_type: TransactionProofType,
},
}
#[derive(Debug, thiserror::Error)]
pub enum VmProcessError {
#[error("parameter serialization failed")]
ParameterSerializationFailed(#[from] serde_json::Error),
#[error("could not determine path of own executable: {0}")]
CouldNotDetermineExePath(#[from] std::io::Error),
#[error("stdin unavailable")]
StdinUnavailable,
#[error("result deserialization failed")]
ResultDeserializationFailed(#[from] Box<bincode::ErrorKind>),
#[error("proving process returned non-zero exit code: {0}")]
NonZeroExitCode(i32),
#[error(
"`neptune-prover` program complexity limit exceeded. result: {result}, limit: {limit}"
)]
ProofComplexityLimitExceeded { limit: u32, result: u32 },
#[error(
"out-of-process `neptune-prover` proving job terminated without exit code. \
Possibly killed by OS. You might not have enough RAM to construct this \
proof."
)]
NoExitCode,
#[error("Triton VM failed: {0}")]
TritonVmFailed(InstructionError),
}
#[derive(Debug)]
pub enum ProverProcessCompletion {
Finished(Proof),
Cancelled,
}
impl From<ProverProcessCompletion> for JobCompletion {
fn from(ppc: ProverProcessCompletion) -> Self {
match ppc {
ProverProcessCompletion::Finished(proof) => ProverJobResult::new(Ok(proof)).into(),
ProverProcessCompletion::Cancelled => Self::Cancelled,
}
}
}
fn job_completion_from_prover_result(
result: Result<ProverProcessCompletion, VmProcessError>,
) -> JobCompletion {
match result {
Ok(ppc) => ppc.into(),
Err(e) => ProverJobResult::new(Err(e.into())).into(),
}
}
pub(super) type ProverJobResult = JobResultWrapper<Result<Proof, ProverJobError>>;
#[derive(Debug, Clone)]
pub struct ProverJobSettings {
pub(crate) max_log2_padded_height_for_proofs: Option<u8>,
pub(crate) network: Network,
pub(crate) tx_proving_capability: TxProvingCapability,
pub(crate) proof_type: TransactionProofType,
pub triton_vm_env_vars: TritonVmEnvVars,
}
impl Default for ProverJobSettings {
fn default() -> Self {
Self {
max_log2_padded_height_for_proofs: None,
network: Network::default(),
tx_proving_capability: TxProvingCapability::SingleProof,
proof_type: TxProvingCapability::SingleProof.into(),
triton_vm_env_vars: TritonVmEnvVars::default(),
}
}
}
impl ProverJobSettings {
pub fn new(
max_log2_padded_height_for_proofs: Option<u8>,
network: Network,
tx_proving_capability: TxProvingCapability,
proof_type: TransactionProofType,
triton_vm_env_vars: TritonVmEnvVars,
) -> Self {
Self {
max_log2_padded_height_for_proofs,
network,
tx_proving_capability,
proof_type,
triton_vm_env_vars,
}
}
pub fn set_network(&mut self, network: Network) {
self.network = network;
}
pub fn max_log2_padded_height_for_proofs(&self) -> Option<u8> {
self.max_log2_padded_height_for_proofs
}
pub fn network(&self) -> Network {
self.network
}
pub fn tx_proving_capability(&self) -> TxProvingCapability {
self.tx_proving_capability
}
pub fn proof_type(&self) -> TransactionProofType {
self.proof_type
}
pub fn set_proof_type(&mut self, proof_type: TransactionProofType) {
self.proof_type = proof_type;
}
pub fn set_tx_proving_capability(&mut self, tx_proving_capability: TxProvingCapability) {
self.tx_proving_capability = tx_proving_capability;
}
}
#[derive(Debug, Clone)]
pub struct ProverJob {
pub(super) program: Program,
pub(super) claim: Claim,
pub(super) nondeterminism: NonDeterminism,
pub(super) job_settings: ProverJobSettings,
}
impl ProverJob {
pub fn new(
program: Program,
claim: Claim,
nondeterminism: NonDeterminism,
job_settings: ProverJobSettings,
) -> Self {
Self {
program,
claim,
nondeterminism,
job_settings,
}
}
async fn check_if_allowed(&self) -> Result<(), ProverJobError> {
tracing::debug!("job settings: {:?}", self.job_settings);
let capability = self.job_settings.tx_proving_capability;
let proof_type = self.job_settings.proof_type;
if !capability.can_prove(proof_type) {
return Err(ProverJobError::TooWeak {
capability,
proof_type,
});
}
tracing::debug!("executing VM program to determine complexity (padded-height)");
assert_eq!(self.program.hash(), self.claim.program_digest);
let mut vm_state = VMState::new(
self.program.clone(),
self.claim.input.clone().into(),
self.nondeterminism.clone(),
);
maybe_write_debuggable_vm_state_to_disk(&vm_state);
vm_state = {
let join_result = tokio::task::spawn_blocking(move || {
log_scope_duration!(fn_name!() + "::vm_state.run()");
let r = vm_state.run();
(vm_state, r)
})
.await;
let (vm_state_moved, run_result) = match join_result {
Ok(r) => r,
Err(e) if e.is_panic() => std::panic::resume_unwind(e.into_panic()),
Err(e) if e.is_cancelled() => {
panic!("VM::run() task was cancelled unexpectedly. error: {e}")
}
Err(e) => panic!("unexpected error from VM::run() spawn-blocking task. {e}"),
};
if let Err(e) = run_result {
return Err(ProverJobError::TritonVmProverFailed(
VmProcessError::TritonVmFailed(e),
));
}
vm_state_moved
};
assert_eq!(self.claim.program_digest, self.program.hash());
assert_eq!(self.claim.output, vm_state.public_output);
let padded_height_processor_table = vm_state.cycle_count.next_power_of_two();
tracing::debug!(
"VM program execution finished: padded-height (processor table): {}",
padded_height_processor_table
);
match self.job_settings.max_log2_padded_height_for_proofs {
Some(limit) if 2u32.pow(limit.into()) < padded_height_processor_table => {
let ph_limit = 2u32.pow(limit.into());
tracing::warn!(
"proof-complexity-limit-exceeded. ({} > {}) The proof will not be generated",
padded_height_processor_table,
ph_limit
);
Err(ProverJobError::ProofComplexityLimitExceeded {
result: padded_height_processor_table,
limit: ph_limit,
})
}
_ => Ok(()),
}
}
async fn prove(&self, rx: JobCancelReceiver) -> JobCompletion {
if self.job_settings.network.use_mock_proof() {
let proof = Proof::valid_mock();
return ProverProcessCompletion::Finished(proof).into();
}
#[cfg(any(test, feature = "test-helpers"))]
let result = self.prove_for_unit_testing(rx).await;
#[cfg(not(any(test, feature = "test-helpers")))]
let result = self.prove_out_of_process(rx).await;
job_completion_from_prover_result(result)
}
pub async fn prove_out_of_process(
&self,
mut rx: JobCancelReceiver,
) -> Result<ProverProcessCompletion, VmProcessError> {
let mut child = {
let job_payload = serde_json::to_vec(&NeptuneProverJob::from(self.clone()))?;
let mut child = tokio::process::Command::new(Self::path_to_neptune_prover()?)
.kill_on_drop(true) .stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
if let Some(stderr) = child.stderr.take() {
tokio::spawn(async move {
use tokio::io::AsyncBufReadExt;
let mut reader = tokio::io::BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
let msg = line.trim();
if let Some(suffix) = msg.strip_prefix("DEBUG:") {
tracing::debug!("neptune-prover: {}", &suffix);
} else if let Some(suffix) = msg.strip_prefix("INFO:") {
tracing::info!("neptune-prover: {}", &suffix);
} else if let Some(suffix) = msg.strip_prefix("TRACE:") {
tracing::trace!("neptune-prover: {}", &suffix);
} else if let Some(suffix) = msg.strip_prefix("ERROR:") {
tracing::error!("neptune-prover: {}", &suffix);
} else if let Some(suffix) = msg.strip_prefix("WARN:") {
tracing::warn!("neptune-prover: {}", &suffix);
} else {
tracing::trace!("neptune-prover (raw): {}", msg);
}
}
});
}
let mut child_stdin = child.stdin.take().ok_or(VmProcessError::StdinUnavailable)?;
child_stdin.write_all(&job_payload).await?;
drop(child_stdin);
child
};
let child_process_id = match child.id() {
Some(id) => id.to_string(),
None => "??".to_string(),
};
tracing::debug!("prover job started child process. id: {}", child_process_id);
tokio::select! {
result = process_util::wait_with_output(&mut child) => {
let output = result?;
match output.status.code() {
Some(0) => {
let proof: Proof = bincode::deserialize(&output.stdout)?;
tracing::debug!(
"Generated proof, with padded height: {}",
proof.padded_height()
.map(|x| x.to_string())
.unwrap_or_else(|e| format!("could not get padded height from proof.\nGot: {e}"))
);
Ok(ProverProcessCompletion::Finished(proof))
}
Some(code) => {
const LOG2_PADDED_HEIGHT_RANGE: i32 = 32;
if (PROOF_PADDED_HEIGHT_TOO_BIG_PROCESS_OFFSET_ERROR_CODE
..=PROOF_PADDED_HEIGHT_TOO_BIG_PROCESS_OFFSET_ERROR_CODE
+ LOG2_PADDED_HEIGHT_RANGE)
.contains(&code)
{
let limit = self
.job_settings
.max_log2_padded_height_for_proofs
.expect(
"Must have max log2 padded height set if this error reported",
)
.into();
let observed_log_padded_height = (code
- PROOF_PADDED_HEIGHT_TOO_BIG_PROCESS_OFFSET_ERROR_CODE)
.try_into()
.unwrap();
Err(VmProcessError::ProofComplexityLimitExceeded {
limit,
result: observed_log_padded_height,
})
} else {
Err(VmProcessError::NonZeroExitCode(code))
}
}
None => Err(VmProcessError::NoExitCode),
}
},
_ = rx.changed() => {
tracing::debug!("prover job got cancel message. killing child process. id: {}", child_process_id);
child.kill().await.expect("external proving process should be killed");
tracing::debug!("prover job: kill succeeded for child process. id: {}, cancelling job", child_process_id);
Ok(ProverProcessCompletion::Cancelled)
}
}
}
fn path_to_neptune_prover() -> Result<std::path::PathBuf, VmProcessError> {
let mut path = std::env::current_exe()?;
let extension = std::env::consts::EXE_EXTENSION;
let bin_name = if extension.is_empty() {
"neptune-prover".to_string()
} else {
format!("neptune-prover.{extension}")
};
if let Some(parent) = path.parent() {
if parent.ends_with("deps") {
path = parent.parent().unwrap_or(parent).to_path_buf();
} else {
path = parent.to_path_buf();
}
}
path.push(bin_name);
Ok(path)
}
}
#[async_trait::async_trait]
impl Job for ProverJob {
fn is_async(&self) -> bool {
true
}
async fn run_async_cancellable(&self, mut rx: JobCancelReceiver) -> JobCompletion {
tokio::select!(
result = self.check_if_allowed() => {
if let Err(e) = result {
return ProverJobResult::new(Err(e)).into()
}
}
_ = rx.changed() => {
tracing::debug!("prover job got cancel message while checking program complexity");
return JobCompletion::Cancelled
}
);
self.prove(rx).await }
}
mod process_util {
use std::process::Output;
use tokio::io::AsyncRead;
use tokio::io::AsyncReadExt;
use tokio::process::Child;
pub async fn wait_with_output(child: &mut Child) -> tokio::io::Result<Output> {
async fn read_to_end<A: AsyncRead + Unpin>(
io: &mut Option<A>,
) -> tokio::io::Result<Vec<u8>> {
let mut vec = Vec::new();
if let Some(io) = io.as_mut() {
io.read_to_end(&mut vec).await?;
}
Ok(vec)
}
let mut stdout_pipe = child.stdout.take();
let mut stderr_pipe = child.stderr.take();
let stdout_fut = read_to_end(&mut stdout_pipe);
let stderr_fut = read_to_end(&mut stderr_pipe);
let (status, stdout, stderr) = tokio::try_join!(child.wait(), stdout_fut, stderr_fut)?;
drop(stdout_pipe);
drop(stderr_pipe);
Ok(Output {
status,
stdout,
stderr,
})
}
}
#[cfg(any(test, feature = "test-helpers"))]
pub(crate) mod unit_testing_prover {
use super::*;
use crate::proof_abstractions::tasm::program::proof_cache::load_proof_or_produce_and_save;
impl ProverJob {
pub(crate) async fn prove_for_unit_testing(
&self,
mut rx: JobCancelReceiver,
) -> Result<ProverProcessCompletion, VmProcessError> {
let claim = self.claim.clone();
let program = self.program.clone();
let nondeterminism = self.nondeterminism.clone();
let prove_jh = tokio::task::spawn_blocking(move || {
let proof = load_proof_or_produce_and_save(&claim, program, nondeterminism);
ProverProcessCompletion::Finished(proof)
});
tokio::select! {
result = prove_jh => Ok(result.unwrap()),
_ = rx.changed() => {
tracing::debug!("prover job got cancel message. cancelling.");
Ok(ProverProcessCompletion::Cancelled)
}
}
}
}
}