use crate::error::TrishaError;
use clap::Args;
use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use trident::runtime::ProgramInput;
const MAX_BYTES: u64 = 64 * 1024 * 1024;
const MODULUS: u64 = 18_446_744_069_414_584_321;
pub(crate) struct Field(u64);
impl Serialize for Field {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0.to_string())
}
}
impl<'de> Deserialize<'de> for Field {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Word;
impl serde::de::Visitor<'_> for Word {
type Value = Field;
fn expecting(&self, out: &mut std::fmt::Formatter) -> std::fmt::Result {
out.write_str("a canonical Goldilocks integer or unsigned decimal string")
}
fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Field, E> {
if value < MODULUS {
Ok(Field(value))
} else {
Err(E::custom("noncanonical field"))
}
}
fn visit_str<E: serde::de::Error>(self, text: &str) -> Result<Field, E> {
if text.is_empty() || text.len() > 20 || !text.bytes().all(|b| b.is_ascii_digit()) {
return Err(E::custom("invalid decimal field"));
}
self.visit_u64(text.parse().map_err(E::custom)?)
}
}
deserializer.deserialize_any(Word)
}
}
impl Field {
pub(crate) fn value(self) -> Result<u64, TrishaError> {
Ok(self.0)
}
fn from_value(value: u64) -> Self {
Self(value)
}
}
fn bounded_sequence<'de, D, T, const LIMIT: usize>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
struct Sequence<T, const N: usize>(std::marker::PhantomData<T>);
impl<'de, T: Deserialize<'de>, const N: usize> serde::de::Visitor<'de> for Sequence<T, N> {
type Value = Vec<T>;
fn expecting(&self, out: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(out, "a sequence of at most {N} items")
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut access: A,
) -> Result<Vec<T>, A::Error> {
use serde::de::Error;
if access.size_hint().is_some_and(|length| length > N) {
return Err(A::Error::custom("input sequence exceeds field limit"));
}
let mut items = Vec::new();
while let Some(item) = access.next_element()? {
if items.len() == N {
return Err(A::Error::custom("input sequence exceeds field limit"));
}
items.push(item);
}
Ok(items)
}
}
deserializer.deserialize_seq(Sequence::<T, LIMIT>(std::marker::PhantomData))
}
const MAX_WORDS: usize = trisha_rs::convert::MAX_INPUT_WORDS;
fn fields<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<Field>, D::Error> {
bounded_sequence::<D, Field, MAX_WORDS>(d)
}
fn claim_fields<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<Field>, D::Error> {
bounded_sequence::<D, Field, { trisha_rs::recursive::MAX_CLAIM_WORDS }>(d)
}
fn hash_fields<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<Field>, D::Error> {
bounded_sequence::<D, Field, 5>(d)
}
fn digests<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<[Field; 5]>, D::Error> {
bounded_sequence::<D, [Field; 5], { MAX_WORDS / 5 }>(d)
}
fn invalid() -> TrishaError {
TrishaError::Io(
"invalid version 1 input/claim file: require canonical fields and the documented schema"
.into(),
)
}
fn values(fields: Vec<Field>) -> Result<Vec<u64>, TrishaError> {
fields.into_iter().map(Field::value).collect()
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct InputDocument {
schema_version: u32,
#[serde(deserialize_with = "fields")]
public: Vec<Field>,
#[serde(deserialize_with = "fields")]
secret: Vec<Field>,
#[serde(deserialize_with = "digests")]
digests: Vec<[Field; 5]>,
}
pub(crate) fn read_regular_bounded(path: &Path) -> Result<Vec<u8>, String> {
let before = std::fs::symlink_metadata(path).map_err(|_| "file cannot be inspected")?;
if !before.is_file() {
return Err("file must be a regular file (no symbolic links)".into());
}
if before.len() > MAX_BYTES {
return Err("file exceeds 64MiB limit".into());
}
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(target_os = "macos")]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(0x4 | 0x100);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(0x800 | 0x20000);
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(0x800 | 0x8000);
}
let file = options.open(path).map_err(|_| "file cannot be opened")?;
let after = file.metadata().map_err(|_| "file cannot be inspected")?;
if !after.is_file() {
return Err("file must be a regular file".into());
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if before.dev() != after.dev() || before.ino() != after.ino() {
return Err("file identity changed while opening".into());
}
}
if after.len() > MAX_BYTES {
return Err("file exceeds 64MiB limit".into());
}
let mut bytes = Vec::new();
file.take(MAX_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|_| "file cannot be read")?;
if bytes.len() as u64 > MAX_BYTES {
return Err("file exceeds 64MiB limit".into());
}
Ok(bytes)
}
pub(crate) fn read_bounded(path: &Path) -> Result<Vec<u8>, TrishaError> {
read_regular_bounded(path).map_err(|error| TrishaError::Io(format!("input/claim {error}")))
}
pub(crate) fn load(path: &Path) -> Result<ProgramInput, TrishaError> {
let document: InputDocument =
serde_json::from_slice(&read_bounded(path)?).map_err(|_| invalid())?;
if document.schema_version != 1 {
return Err(invalid());
}
if document
.public
.len()
.saturating_add(document.secret.len())
.saturating_add(document.digests.len().saturating_mul(5))
> MAX_WORDS
{
return Err(TrishaError::Io("input exceeds8Mi field-word limit".into()));
}
let digests = document
.digests
.into_iter()
.map(|digest| {
let values = digest
.into_iter()
.map(Field::value)
.collect::<Result<Vec<_>, _>>()?;
Ok(values
.try_into()
.expect("serde enforces five fields per digest"))
})
.collect::<Result<Vec<[u64; 5]>, TrishaError>>()?;
Ok(ProgramInput {
public: values(document.public)?,
secret: values(document.secret)?,
digests,
})
}
fn save(path: &Path, input: ProgramInput) -> Result<(), TrishaError> {
trisha_rs::convert::validate_input(&input).map_err(TrishaError::Io)?;
let document = InputDocument {
schema_version: 1,
public: input.public.into_iter().map(Field::from_value).collect(),
secret: input.secret.into_iter().map(Field::from_value).collect(),
digests: input
.digests
.into_iter()
.map(|digest| digest.map(Field::from_value))
.collect(),
};
let bytes = serde_json::to_vec(&document).map_err(|_| invalid())?;
if bytes.len() as u64 > MAX_BYTES {
return Err(TrishaError::Io(
"encoded witness exceeds 64MiB limit".into(),
));
}
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
options.open(path)?.write_all(&bytes)?;
Ok(())
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ExpectedClaim {
schema_version: u32,
#[serde(deserialize_with = "hash_fields")]
program_hash: Vec<Field>,
#[serde(deserialize_with = "claim_fields")]
public_input: Vec<Field>,
#[serde(deserialize_with = "claim_fields")]
public_output: Vec<Field>,
}
#[derive(Args)]
pub struct WitnessArgs {
pub proof: PathBuf,
#[arg(long, required_unless_present = "policy", conflicts_with_all = ["policy", "commitments"])]
pub expected_claim: Option<PathBuf>,
#[arg(
long,
value_enum,
requires = "commitments",
conflicts_with = "expected_claim"
)]
pub policy: Option<crate::policy_witness::Policy>,
#[arg(long, requires = "policy", conflicts_with = "expected_claim")]
pub commitments: Option<PathBuf>,
#[arg(long)]
pub output: PathBuf,
}
pub fn cmd_witness(args: WitnessArgs) -> Result<(), TrishaError> {
let (claim, policy_public) = if let Some(policy) = args.policy {
let path = args.commitments.as_deref().ok_or_else(invalid)?;
let (claim, public) = crate::policy_witness::expected(policy, path)?;
(claim, Some(public))
} else {
let path = args.expected_claim.as_deref().ok_or_else(invalid)?;
let expected: ExpectedClaim =
serde_json::from_slice(&read_bounded(path)?).map_err(|_| invalid())?;
if expected.schema_version != 1 {
return Err(invalid());
}
let claim = trisha_rs::convert::to_triton_claim_native(
&values(expected.program_hash)?,
&values(expected.public_input)?,
&values(expected.public_output)?,
)
.map_err(TrishaError::Verify)?;
(claim, None)
};
let bytes = read_bounded(&args.proof)?;
let proof_file: crate::proof_file::ProofFile =
toml::from_str(std::str::from_utf8(&bytes).map_err(|_| invalid())?)
.map_err(|_| TrishaError::Verify("invalid proof file".into()))?;
let artifact = trident::runtime::ProofData {
claim: trident::field::proof::Claim {
program_hash: proof_file.claim.program_hash,
public_input: proof_file.claim.public_input,
public_output: proof_file.claim.public_output,
},
proof_bytes: crate::proof_file::ProofFile::decode_proof_bytes(&proof_file.data.proof)?,
format: proof_file.proof.format,
};
let mut input =
trisha_rs::recursive::encode_proof_data(&claim, &artifact).map_err(TrishaError::Verify)?;
if let Some(public) = policy_public {
input.public = public;
}
save(&args.output, input)?;
eprintln!(
"Private recursive witness written to {}",
args.output.display()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn field_decoder_accepts_only_canonical_words_without_string_storage() {
assert_eq!(std::mem::size_of::<Field>(), 8);
for text in ["0", "18446744069414584320", "\"18446744069414584320\""] {
assert!(serde_json::from_str::<Field>(text).is_ok(), "{text}");
}
for text in [
"-1",
"1.5",
"18446744069414584321",
"18446744073709551615",
"\"+1\"",
"\"-1\"",
"\"\"",
"\"000000000000000000001\"",
] {
assert!(serde_json::from_str::<Field>(text).is_err(), "{text}");
}
}
#[test]
fn sequences_reject_before_reading_the_remaining_payload() {
let mut reader = serde_json::Deserializer::from_str("[0,1,2,3,INVALID]");
let error = bounded_sequence::<_, Field, 3>(&mut reader).err().unwrap();
assert!(error
.to_string()
.contains("input sequence exceeds field limit"));
let mut reader = serde_json::Deserializer::from_str("[0,1,2]");
assert_eq!(
bounded_sequence::<_, Field, 3>(&mut reader).unwrap().len(),
3
);
}
}