#![no_std]
#![deny(warnings)]
extern crate alloc;
use alloc::vec::Vec;
use miden_field::{Felt, Word};
use miden_field_repr::{FeltReader, FeltReprError, FromFeltRepr, ToFeltRepr};
const WORD_FELTS: usize = Word::NUM_ELEMENTS;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EncodedScriptArgs {
Word(Word),
Preimage(Vec<Felt>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScriptArgsError {
Decode(FeltReprError),
NonZeroPadding,
TrailingData,
NonWordMultipleLength,
AdviceProviderUnavailable,
}
impl From<FeltReprError> for ScriptArgsError {
fn from(err: FeltReprError) -> Self {
Self::Decode(err)
}
}
impl core::fmt::Display for ScriptArgsError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Decode(err) => write!(f, "failed to decode tx script args: {err}"),
Self::NonZeroPadding => f.write_str("non-zero padding felt in tx script args"),
Self::TrailingData => f.write_str("trailing data after tx script args"),
Self::NonWordMultipleLength => {
f.write_str("tx script args advice value is not a whole number of words")
}
Self::AdviceProviderUnavailable => {
f.write_str("commitment-mode tx script args can only be decoded on the Miden VM")
}
}
}
}
pub type ScriptArgsResult<T> = core::result::Result<T, ScriptArgsError>;
mod sealed {
pub trait Sealed {}
impl<T: super::FromFeltRepr + super::ToFeltRepr> Sealed for T {}
}
pub trait ScriptArgs: Sized + sealed::Sealed {
const FIXED_LEN: Option<usize>;
fn decode(arg: Word) -> ScriptArgsResult<Self>;
fn encode(&self) -> EncodedScriptArgs;
}
const fn is_word_mode(fixed_len: Option<usize>) -> bool {
match fixed_len {
Some(len) => len <= WORD_FELTS,
None => false,
}
}
#[inline(always)]
fn check_zero_padding(reader: &mut FeltReader<'_>) -> ScriptArgsResult<()> {
while reader.remaining() > 0 {
if reader.read()? != Felt::ZERO {
return Err(ScriptArgsError::NonZeroPadding);
}
}
Ok(())
}
#[cfg(all(target_family = "wasm", miden, feature = "miden-vm-guest"))]
#[inline(always)]
fn decode_commitment<T: FromFeltRepr>(arg: Word) -> ScriptArgsResult<T> {
use miden_stdlib_sys::{adv_load_preimage, intrinsics::advice::adv_push_mapvaln};
let num_felts = adv_push_mapvaln(arg).as_canonical_u64();
if num_felts % WORD_FELTS as u64 != 0 {
return Err(ScriptArgsError::NonWordMultipleLength);
}
let num_words = Felt::new(num_felts / WORD_FELTS as u64).unwrap();
let preimage = adv_load_preimage(num_words, arg);
decode_preimage(&preimage)
}
#[cfg(not(all(target_family = "wasm", miden, feature = "miden-vm-guest")))]
fn decode_commitment<T: FromFeltRepr>(_arg: Word) -> ScriptArgsResult<T> {
Err(ScriptArgsError::AdviceProviderUnavailable)
}
#[inline(always)]
pub fn decode_preimage<T: FromFeltRepr>(preimage: &[Felt]) -> ScriptArgsResult<T> {
if !preimage.len().is_multiple_of(WORD_FELTS) {
return Err(ScriptArgsError::NonWordMultipleLength);
}
let mut reader = FeltReader::new(preimage);
let value = T::from_felt_repr(&mut reader)?;
check_decoded_len::<T>(&reader)?;
if reader.remaining() >= WORD_FELTS {
return Err(ScriptArgsError::TrailingData);
}
check_zero_padding(&mut reader)?;
Ok(value)
}
#[inline(always)]
fn check_decoded_len<T: FromFeltRepr>(reader: &FeltReader<'_>) -> ScriptArgsResult<()> {
if let Some(fixed_len) = T::FIXED_LEN {
assert!(reader.pos() == fixed_len, "decoded length must match FIXED_LEN");
}
Ok(())
}
impl<T: FromFeltRepr + ToFeltRepr> ScriptArgs for T {
const FIXED_LEN: Option<usize> = <T as FromFeltRepr>::FIXED_LEN;
#[inline(always)]
fn decode(arg: Word) -> ScriptArgsResult<Self> {
if const { is_word_mode(Self::FIXED_LEN) } {
let felts = [arg[0], arg[1], arg[2], arg[3]];
let mut reader = FeltReader::new(&felts);
let value = Self::from_felt_repr(&mut reader)?;
check_decoded_len::<Self>(&reader)?;
check_zero_padding(&mut reader)?;
Ok(value)
} else {
decode_commitment(arg)
}
}
fn encode(&self) -> EncodedScriptArgs {
let mut felts = self.to_felt_repr();
if let Some(fixed_len) = Self::FIXED_LEN {
assert!(felts.len() == fixed_len, "encoding length must match FIXED_LEN");
}
if const { is_word_mode(Self::FIXED_LEN) } {
felts.resize(WORD_FELTS, Felt::ZERO);
EncodedScriptArgs::Word(Word::new([felts[0], felts[1], felts[2], felts[3]]))
} else {
felts.resize(felts.len().next_multiple_of(WORD_FELTS), Felt::ZERO);
EncodedScriptArgs::Preimage(felts)
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
fn felt(value: u64) -> Felt {
Felt::new(value).unwrap()
}
#[test]
fn word_mode_encode_pads_with_zeros() {
let EncodedScriptArgs::Word(word) = felt(7).encode() else {
panic!("expected word mode for a single felt");
};
assert_eq!(word, Word::new([felt(7), felt(0), felt(0), felt(0)]));
}
#[test]
fn word_mode_roundtrip() {
let value = felt(7);
let EncodedScriptArgs::Word(word) = value.encode() else {
panic!("expected word mode for a single felt");
};
assert_eq!(<Felt as ScriptArgs>::decode(word), Ok(value));
}
#[test]
fn word_args_are_transported_verbatim() {
let word = Word::new([felt(1), felt(2), felt(3), felt(4)]);
let EncodedScriptArgs::Word(encoded) = word.encode() else {
panic!("expected word mode for a word");
};
assert_eq!(encoded, word);
assert_eq!(<Word as ScriptArgs>::decode(encoded), Ok(word));
}
#[test]
fn word_mode_decode_rejects_nonzero_padding() {
let word = Word::new([felt(7), felt(0), felt(0), felt(1)]);
assert_eq!(<Felt as ScriptArgs>::decode(word), Err(ScriptArgsError::NonZeroPadding));
}
#[test]
fn word_mode_decode_surfaces_felt_repr_errors() {
let word = Word::new([felt(2), felt(0), felt(0), felt(0)]);
assert_eq!(
<bool as ScriptArgs>::decode(word),
Err(ScriptArgsError::Decode(FeltReprError::InvalidBool {
pos: 0,
len: 4,
value: 2
}))
);
}
#[test]
fn commitment_mode_encode_pads_to_word_multiple() {
let values = vec![felt(5), felt(6)];
let EncodedScriptArgs::Preimage(felts) = values.encode() else {
panic!("expected commitment mode for a variable-length encoding");
};
assert_eq!(felts, vec![felt(2), felt(5), felt(6), felt(0)]);
}
struct LyingFixedLen;
impl FromFeltRepr for LyingFixedLen {
const FIXED_LEN: Option<usize> = Some(1);
fn from_felt_repr(reader: &mut FeltReader<'_>) -> miden_field_repr::FeltReprResult<Self> {
reader.read()?;
reader.read()?;
Ok(Self)
}
}
impl ToFeltRepr for LyingFixedLen {
fn write_felt_repr(&self, writer: &mut miden_field_repr::FeltWriter<'_>) {
writer.write(felt(1));
writer.write(felt(2));
}
}
#[test]
#[should_panic(expected = "must match FIXED_LEN")]
fn encode_rejects_wrong_manual_fixed_len() {
let _ = LyingFixedLen.encode();
}
#[test]
#[should_panic(expected = "decoded length must match FIXED_LEN")]
fn decode_rejects_wrong_manual_fixed_len() {
struct LyingDecoder;
impl FromFeltRepr for LyingDecoder {
const FIXED_LEN: Option<usize> = Some(2);
fn from_felt_repr(
reader: &mut FeltReader<'_>,
) -> miden_field_repr::FeltReprResult<Self> {
reader.read()?;
Ok(Self)
}
}
impl ToFeltRepr for LyingDecoder {
fn write_felt_repr(&self, writer: &mut miden_field_repr::FeltWriter<'_>) {
writer.write(felt(1));
writer.write(felt(2));
}
}
let _ = LyingDecoder::decode(Word::new([felt(1), felt(2), felt(0), felt(0)]));
}
#[test]
fn commitment_mode_decode_reports_missing_advice_provider() {
let word = Word::new([felt(1), felt(2), felt(3), felt(4)]);
assert_eq!(
<Vec<Felt> as ScriptArgs>::decode(word),
Err(ScriptArgsError::AdviceProviderUnavailable)
);
}
#[test]
fn decode_preimage_accepts_canonical_padding() {
let decoded: ScriptArgsResult<Vec<Felt>> =
decode_preimage(&[felt(2), felt(5), felt(6), felt(0)]);
assert_eq!(decoded, Ok(vec![felt(5), felt(6)]));
}
#[test]
fn decode_preimage_rejects_nonzero_padding() {
let decoded: ScriptArgsResult<Vec<Felt>> =
decode_preimage(&[felt(2), felt(5), felt(6), felt(9)]);
assert_eq!(decoded, Err(ScriptArgsError::NonZeroPadding));
}
#[test]
fn decode_preimage_rejects_non_word_multiple_length() {
let decoded: ScriptArgsResult<Vec<Felt>> = decode_preimage(&[felt(2), felt(5), felt(6)]);
assert_eq!(decoded, Err(ScriptArgsError::NonWordMultipleLength));
}
#[test]
fn decode_preimage_rejects_extra_word() {
let decoded: ScriptArgsResult<Vec<Felt>> = decode_preimage(&[
felt(2),
felt(5),
felt(6),
felt(0),
felt(0),
felt(0),
felt(0),
felt(0),
]);
assert_eq!(decoded, Err(ScriptArgsError::TrailingData));
}
}