Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/api/tx_initiation/builder/transaction_proof_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ use crate::triton_vm::proof::Claim;
use crate::triton_vm::vm::NonDeterminism;
use crate::triton_vm_job_queue::vm_job_queue;
use crate::triton_vm_job_queue::TritonVmJobQueue;
use crate::util_types::log_vm_state;
use crate::util_types::log_vm_state::LogProofInputsType;

/// a builder for [TransactionProof]
///
Expand Down Expand Up @@ -344,8 +346,16 @@ async fn gen_single<'a, F>(
valid_mock: bool,
) -> Result<TransactionProof, CreateProofError>
where
F: FnOnce() -> NonDeterminism + Send + Sync + 'a,
F: Clone + FnOnce() -> NonDeterminism + Send + Sync + 'a,
{
// log proof inputs if matching env var is set. (does not expose witness secrets)
// maybe_write() logs warning if error occurs; we ignore any error.
let _ = log_vm_state::maybe_write(
LogProofInputsType::NoWitness,
&claim,
nondeterminism.clone(),
);

Ok(TransactionProof::SingleProof(
ProofBuilder::new()
.program(SingleProof.program())
Expand Down
8 changes: 8 additions & 0 deletions src/models/proof_abstractions/tasm/prover_job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ use crate::models::proof_abstractions::NonDeterminism;
use crate::models::proof_abstractions::Program;
use crate::models::state::vm_proving_capability::VmProvingCapability;
use crate::models::state::vm_proving_capability::VmProvingCapabilityError;
use crate::util_types::log_vm_state;
use crate::util_types::log_vm_state::LogProofInputsType;

/// represents an error running a [ProverJob]
#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -174,6 +176,12 @@ impl ProverJob {
/// If we are in a test environment, try reading it from disk. If it is not
/// there, generate it and store it to disk.
async fn prove(&self, rx: JobCancelReceiver) -> JobCompletion {
// log proof inputs if matching env var is set (may expose witness secrets)
// maybe_write() logs warning if error occurs; we ignore any error.
let _ = log_vm_state::maybe_write(LogProofInputsType::Proof, &self.claim, || {
self.nondeterminism.clone()
});

// produce mock proofs if network so requires. (ie RegTest)
if self.job_settings.network.use_mock_proof() {
let proof = Proof::valid_mock(self.claim.clone());
Expand Down
55 changes: 0 additions & 55 deletions src/models/state/vm_proving_capability.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
use std::fmt::Display;
use std::io::Write;
use std::path::PathBuf;
use std::str::FromStr;

use num_traits::Zero;
Expand Down Expand Up @@ -172,7 +170,6 @@ impl VmProvingCapability {
debug_assert_eq!(program.hash(), claim.program_digest);

let mut vmstate = VMState::new(program, claim.input.into(), nondeterminism);
let _ = Self::maybe_write_debuggable_vm_state_to_disk(&vmstate);

let method = std::env::var("NEPTUNE_LOG2_PADDED_HEIGHT_METHOD")
.unwrap_or_else(|_| "run".to_string());
Expand Down Expand Up @@ -202,37 +199,6 @@ impl VmProvingCapability {
}
}

/// If the environment variable NEPTUNE_WRITE_VM_STATE_DIR is set,
/// write the initial VM state to file `<DIR>/vm_state.<pid>.<random>.json`.
///
/// This file can be used to debug the program using the [Triton TUI]:
/// ```sh
/// triton-tui --initial-state <file>
/// ```
///
/// [Triton TUI]: https://crates.io/crates/triton-tui
pub fn maybe_write_debuggable_vm_state_to_disk(
vm_state: &VMState,
) -> Result<Option<PathBuf>, LogVmStateError> {
let Ok(dir) = std::env::var("NEPTUNE_WRITE_VM_STATE_DIR") else {
return Ok(None);
};

let filename = format!(
"vm_state.{}.{}.json",
std::process::id(),
rand::random::<u32>()
);

let path = PathBuf::from(dir).join(filename);

let mut state_file =
std::fs::File::create(&path).map_err(|e| LogVmStateError::from((path.clone(), e)))?;
let state = serde_json::to_string(&vm_state)?;
write!(state_file, "{}", state).map_err(|e| LogVmStateError::from((path.clone(), e)))?;
Ok(Some(path))
}

/// automatically detect the log2_padded_height for this device.
///
/// for now this just:
Expand Down Expand Up @@ -313,24 +279,3 @@ pub enum VmProvingCapabilityError {
#[error("device capability {capability} is insufficient to generate proof that requires capability {attempted}")]
DeviceNotCapable { capability: u32, attempted: u32 },
}

#[derive(Debug, thiserror::Error, strum::EnumIs)]
#[non_exhaustive]
pub enum LogVmStateError {
#[error("could not obtain padded-height due to program execution error")]
IoError {
path: PathBuf,
#[source]
source: std::io::Error,
},

#[error(transparent)]
SerializeError(#[from] serde_json::Error),
}

impl From<(PathBuf, std::io::Error)> for LogVmStateError {
fn from(v: (PathBuf, std::io::Error)) -> Self {
let (path, source) = v;
Self::IoError { path, source }
}
}
145 changes: 145 additions & 0 deletions src/util_types/log_vm_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use std::io::Write;
use std::path::PathBuf;

use crate::models::blockchain::shared::Hash as NeptuneHash;
use crate::models::blockchain::transaction::validity::single_proof::SingleProof;
use crate::models::proof_abstractions::tasm::program::ConsensusProgram;
use crate::triton_vm::proof::Claim;
use crate::triton_vm::vm::NonDeterminism;
use crate::triton_vm::vm::VMState;

/// enumerates types of proofs that can be logged.
///
/// this type facilitates offering distinct logging modes for:
///
/// 1. `Proof` (any kind of Proof)
/// 2. `NoWitness`
///
/// The `Proof` mode is useful for logging inputs to every single Proof that is
/// generated. These include proofs generated from `PrimitiveWitness` meaning
/// that the claims expose secrets and should not be shared. This mode of
/// logging is considered a security risk, but can be useful for investigating
/// or researching alone, or on testnet(s), etc.
///
/// The `NoWitness` mode is useful when logging proof inputs for purposes of
/// sharing with others, eg neptune-core developers for debugging. However it
/// does not log any Proofs generated from a `PrimitiveWitness`.
pub enum LogProofInputsType {
/// log any inputs of any kind of proof, possibly leaking witness secrets
Proof,

/// log inputs of proof, without leaking witness secrets
NoWitness,
}

impl LogProofInputsType {
/// returns name of logging environment variable
///
/// each variant has an environment variable that specifies the
/// directory in which to write proof files.
pub const fn env_var_name(&self) -> &str {
match *self {
Self::Proof => "NEPTUNE_VM_STATE_DIR",
Self::NoWitness => "NEPTUNE_VM_STATE_NO_WITNESS_DIR",
}
}

/// returns file name prefix
pub const fn file_prefix(&self) -> &str {
match *self {
Self::Proof => "vm_state.claim",
Self::NoWitness => "vm_state.no_witness_claim",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the no_witness_claim to indicate also in the filename that the claim does not contain a primitive witness. I'm less sure about vm_state.claim: the state file does not only contain the claim but also the non-determinism and the proof (neither of which is part of the claim). Maybe just drop the .claim suffix of the prefix?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the state file does not only contain the claim but also the non-determinism and the proof

I'm confused about how the file can contain the proof.

My understanding has been that VMState::run() just runs the program and does not generate a proof. As such it can run in seconds while the real proof generation with triton_vm::prove() takes sometimes minutes for the same program, claim/input, nondeterminism triple.

Can you clarify about that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, I noticed that the proof caching mechanism used for neptune-core testing identifies the cached data by the hash of the claim. Here is the proof_filename() fn from program.rs.

    fn proof_filename(claim: &Claim) -> String {
        let base_name = Hash::hash(claim).to_hex();

        format!("{base_name}.proof")
    }

So I was essentially working from this precedent (which I did not author).

It does seem logical to me that hash(program, claim, nondeterminism) would be a better identifier, but from above the claim seems to have been deemed sufficient.

Copy link
Collaborator Author

@dan-da dan-da Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self::Proof => "vm_state.claim",
Self::NoWitness => "vm_state.no_witness_claim",

I also was considering that perhaps the Self::Proof case should somehow warn not to share, eg: "vm_state.do_not_share" or "vm_state.unsafe_to_share". what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused about how the file can contain the proof.

And rightfully so. I was trying to say that the VM state also contains the non-determinism and the program (not proof).

My understanding has been […]. Can you clarify about that?

What you describe here is accurate.

also, I noticed that the proof caching mechanism used for neptune-core testing identifies the cached data by the hash of the claim.

I concur that a hash of the claim is sufficient for identifying a proof.1 I think I left the comment more because I think that the filename vm_state.claim suggests that the file contains only the claim, while in truth, it's the entire VM state.

I also was considering that perhaps the Self::Proof case should somehow warn not to share

I like that idea and your suggestion. How about commenting on the share-safety in both cases?

Self::Proof => "vm_state.do_not_share",
Self::NoWitness => "vm_state.safe_to_share",

Footnotes

  1. In case you're interested in the “why”: A claim contains the hash of the program, so the program is pinned down that way. The exact non-determinism is irrelevant for the proofs validity. In general, there can be many different non-determinisms with which it's possible to generate a proof. The point is that the non-determinism doesn't have to be pinned down.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vm_state.claim suggests that the file contains only the claim, while in truth, it's the entire VM state.

The problem with abbreviating things is it can have different readings. I was intending it to mean "vm_state for claim X" where X is included in the filename.

In terms of symmetry, I like this better/best:

Self::Proof => "vm_state.unsafe_to_share",
Self::NoWitness => "vm_state.safe_to_share"

I'm hesitant to label it "safe_to_share" though. Seems like we are then making some kind of guarantee. If there's consensus amongst team members it is "safe" and we should name it that I'm ok with it, I just don't want to make the claim on my own.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's hear from people with more knowledge of the architecture whether these VM states are actually safe to share. 👍 (@aszepieniec, @Sword-Smith)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20705dd changes it to:

            Self::MayContainWalletSecrets => "vm_state.unsafe_to_share",
            Self::DoesNotContainWalletSecrets => "vm_state.safe_to_share",

}
}
}

/// If the environment variable specified by [LogProofInputsType::env_var_name()] is set,
/// write the initial VM state to file `<DIR>/<prefix>.<pid>.<claim>.json`.
///
/// where:
/// DIR = value of environment variable.
/// prefix = LogProofInputsType::file_prefix()
/// pid = process-id
/// claim = hex-encoded hash of input Claim
///
/// This file can be used to debug the program using the [Triton TUI]:
/// ```sh
/// triton-tui --initial-state <file>
/// ```
///
/// [Triton TUI]: https://crates.io/crates/triton-tui
///
/// Security:
///
/// Files of type [LogProofInputsType::Proof] should only be used for debugging
/// by the wallet owner as they may contain wallet secrets.
///
/// Files of type [LogProofInputsType::NoWitness] can be shared with others
/// eg, neptune-core developers, for purposes of debugging/assistance.
///
/// It is the *callers* responsibility to ensure that the provided claim matches
/// the `log_proof_inputs_type`
pub fn maybe_write<'a, F>(
log_proof_inputs_type: LogProofInputsType,
claim: &Claim,
nondeterminism: F,
) -> Result<Option<PathBuf>, LogVmStateError>
where
F: FnOnce() -> NonDeterminism + Send + Sync + 'a,
{
let Ok(dir) = std::env::var(log_proof_inputs_type.env_var_name()) else {
return Ok(None);
};
let prefix = log_proof_inputs_type.file_prefix();

write(&dir, prefix, claim, nondeterminism()).inspect_err(|e| tracing::warn!("{}", e))
}

fn write(
dir: &str,
file_prefix: &str,
claim: &Claim,
nondeterminism: NonDeterminism,
) -> Result<Option<PathBuf>, LogVmStateError> {
let vm_state = VMState::new(
SingleProof.program(),
claim.input.clone().into(),
nondeterminism,
);

let filename = format!(
"{}.{}.{}.json",
file_prefix,
std::process::id(),
NeptuneHash::hash(claim).to_hex(),
);

let path = PathBuf::from(dir).join(filename);

let mut state_file =
std::fs::File::create(&path).map_err(|e| LogVmStateError::from((path.clone(), e)))?;
let state = serde_json::to_string(&vm_state)?;
write!(state_file, "{}", state).map_err(|e| LogVmStateError::from((path.clone(), e)))?;
Ok(Some(path))
}

#[derive(Debug, thiserror::Error, strum::EnumIs)]
#[non_exhaustive]
pub enum LogVmStateError {
#[error("could not obtain padded-height due to program execution error")]
IoError {
path: PathBuf,
#[source]
source: std::io::Error,
},

#[error(transparent)]
SerializeError(#[from] serde_json::Error),
}

impl From<(PathBuf, std::io::Error)> for LogVmStateError {
fn from(v: (PathBuf, std::io::Error)) -> Self {
let (path, source) = v;
Self::IoError { path, source }
}
}
1 change: 1 addition & 0 deletions src/util_types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod archival_mmr;
pub(crate) mod log_vm_state;
pub mod mutator_set;
pub mod rusty_archival_block_mmr;

Expand Down