use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt::Display;
use miden_core::mast::MastNodeExt;
use miden_crypto::merkle::InnerNodeInfo;
use miden_crypto_derive::WordWrapper;
use miden_mast_package::Package;
use miden_mast_package::debug_info::PackageDebugInfo;
use miden_processor::LoadedMastForest;
use super::{Felt, Hasher, Word};
use crate::account::auth::{PublicKeyCommitment, Signature};
use crate::assembly::{Library, Path};
use crate::errors::TransactionScriptError;
use crate::note::{NoteId, NoteRecipient};
use crate::package::{loaded_mast_forest, package_debug_info};
use crate::utils::create_external_node_forest;
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use crate::vm::{AdviceInputs, AdviceMap, Program};
use crate::{EMPTY_WORD, MastForest, MastNodeId};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransactionArgs {
tx_script: Option<TransactionScript>,
tx_script_args: Word,
note_args: BTreeMap<NoteId, Word>,
advice_inputs: AdviceInputs,
auth_args: Word,
}
impl TransactionArgs {
pub fn new(advice_map: AdviceMap) -> Self {
let advice_inputs = AdviceInputs { map: advice_map, ..Default::default() };
Self {
tx_script: None,
tx_script_args: EMPTY_WORD,
note_args: Default::default(),
advice_inputs,
auth_args: EMPTY_WORD,
}
}
#[must_use]
pub fn with_tx_script(mut self, tx_script: TransactionScript) -> Self {
self.tx_script = Some(tx_script);
self
}
#[must_use]
pub fn with_tx_script_and_args(
mut self,
tx_script: TransactionScript,
tx_script_args: Word,
) -> Self {
self.tx_script = Some(tx_script);
self.tx_script_args = tx_script_args;
self
}
#[must_use]
pub fn with_note_args(mut self, note_args: BTreeMap<NoteId, Word>) -> Self {
self.note_args = note_args;
self
}
#[must_use]
pub fn with_auth_args(mut self, auth_args: Word) -> Self {
self.auth_args = auth_args;
self
}
pub fn tx_script(&self) -> Option<&TransactionScript> {
self.tx_script.as_ref()
}
pub fn tx_script_args(&self) -> Word {
self.tx_script_args
}
pub fn get_note_args(&self, note_id: NoteId) -> Option<&Word> {
self.note_args.get(¬e_id)
}
pub fn advice_inputs(&self) -> &AdviceInputs {
&self.advice_inputs
}
pub fn auth_args(&self) -> Word {
self.auth_args
}
pub fn add_output_note_recipient<T: AsRef<NoteRecipient>>(&mut self, note_recipient: T) {
let note_recipient = note_recipient.as_ref();
let storage = note_recipient.storage();
let script = note_recipient.script();
let script_encoded: Vec<Felt> = script.into();
let script_root: Word = script.root().into();
let sn_hash = Hasher::merge(&[note_recipient.serial_num(), Word::empty()]);
let sn_script_hash = Hasher::merge(&[sn_hash, script_root]);
let new_elements = vec![
(sn_hash, concat_words(note_recipient.serial_num(), Word::empty())),
(sn_script_hash, concat_words(sn_hash, script_root)),
(note_recipient.digest(), concat_words(sn_script_hash, storage.commitment())),
(storage.commitment(), storage.to_elements()),
(script_root, script_encoded),
];
self.advice_inputs.extend(AdviceInputs::default().with_map(new_elements));
}
pub fn add_signature(
&mut self,
pub_key: PublicKeyCommitment,
message: Word,
signature: Signature,
) {
let pk_word: Word = pub_key.into();
self.advice_inputs
.map
.insert(Hasher::merge(&[pk_word, message]), signature.to_prepared_signature(message));
}
pub fn extend_output_note_recipients<T, L>(&mut self, notes: L)
where
L: IntoIterator<Item = T>,
T: AsRef<NoteRecipient>,
{
for note in notes {
self.add_output_note_recipient(note);
}
}
pub fn extend_advice_map<T: IntoIterator<Item = (Word, Vec<Felt>)>>(&mut self, iter: T) {
self.advice_inputs.map.extend(iter);
}
pub fn extend_merkle_store<I: Iterator<Item = InnerNodeInfo>>(&mut self, iter: I) {
self.advice_inputs.store.extend(iter);
}
pub fn extend_advice_inputs(&mut self, advice_inputs: AdviceInputs) {
self.advice_inputs.extend(advice_inputs);
}
}
fn concat_words(first: Word, second: Word) -> Vec<Felt> {
let mut result = Vec::with_capacity(8);
result.extend(first);
result.extend(second);
result
}
impl Default for TransactionArgs {
fn default() -> Self {
Self::new(AdviceMap::default())
}
}
impl Serializable for TransactionArgs {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.tx_script.write_into(target);
self.tx_script_args.write_into(target);
self.note_args.write_into(target);
self.advice_inputs.write_into(target);
self.auth_args.write_into(target);
}
}
impl Deserializable for TransactionArgs {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let tx_script = Option::<TransactionScript>::read_from(source)?;
let tx_script_args = Word::read_from(source)?;
let note_args = BTreeMap::<NoteId, Word>::read_from(source)?;
let advice_inputs = AdviceInputs::read_from(source)?;
let auth_args = Word::read_from(source)?;
Ok(Self {
tx_script,
tx_script_args,
note_args,
advice_inputs,
auth_args,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, WordWrapper)]
pub struct TransactionScriptRoot(Word);
impl From<TransactionScriptRoot> for Word {
fn from(root: TransactionScriptRoot) -> Self {
root.0
}
}
impl Display for TransactionScriptRoot {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl Serializable for TransactionScriptRoot {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write(self.0);
}
fn get_size_hint(&self) -> usize {
self.0.get_size_hint()
}
}
impl Deserializable for TransactionScriptRoot {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let word: Word = source.read()?;
Ok(Self::from_raw(word))
}
}
pub const TRANSACTION_SCRIPT_ATTRIBUTE: &str = "transaction_script";
#[derive(Clone, Debug)]
pub struct TransactionScript {
mast: Arc<MastForest>,
entrypoint: MastNodeId,
package_debug_info: Option<Arc<PackageDebugInfo>>,
}
impl TransactionScript {
pub fn new(code: Program) -> Self {
Self::from_parts(code.mast_forest().clone(), code.entrypoint())
}
pub fn from_parts(mast: Arc<MastForest>, entrypoint: MastNodeId) -> Self {
assert!(mast.get_node_by_id(entrypoint).is_some());
Self {
mast,
entrypoint,
package_debug_info: None,
}
}
pub fn from_package(package: &Package) -> Result<Self, TransactionScriptError> {
let program =
package.try_into_program().map_err(TransactionScriptError::PackageNotProgram)?;
Ok(Self {
mast: program.mast_forest().clone(),
entrypoint: program.entrypoint(),
package_debug_info: package_debug_info(package),
})
}
pub fn from_library(library: &Library) -> Result<Self, TransactionScriptError> {
let mut entrypoint = None;
for export in library.manifest.exports() {
if let Some(proc_export) = export.as_procedure()
&& proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE)
{
if entrypoint.is_some() {
return Err(TransactionScriptError::MultipleProceduresWithAttribute);
}
entrypoint =
Some(proc_export.node.ok_or(TransactionScriptError::NoProcedureWithAttribute)?);
}
}
let entrypoint = entrypoint.ok_or(TransactionScriptError::NoProcedureWithAttribute)?;
Ok(Self {
mast: library.mast_forest().clone(),
entrypoint,
package_debug_info: package_debug_info(library),
})
}
pub fn from_library_reference(
library: &Library,
path: &Path,
) -> Result<Self, TransactionScriptError> {
let export =
library.manifest.exports().find(|e| e.path().as_ref() == path).ok_or_else(|| {
TransactionScriptError::ProcedureNotFound(path.to_string().into())
})?;
let proc_export = export
.as_procedure()
.ok_or_else(|| TransactionScriptError::ProcedureNotFound(path.to_string().into()))?;
if !proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE) {
return Err(TransactionScriptError::ProcedureMissingAttribute(path.to_string().into()));
}
let digest = proc_export.digest;
let (mast, entrypoint) = create_external_node_forest(digest);
Ok(Self {
mast: Arc::new(mast),
entrypoint,
package_debug_info: package_debug_info(library),
})
}
pub fn mast(&self) -> Arc<MastForest> {
self.mast.clone()
}
pub fn loaded_mast_forest(&self) -> LoadedMastForest {
loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone())
}
pub fn root(&self) -> TransactionScriptRoot {
TransactionScriptRoot::from_raw(self.mast[self.entrypoint].digest())
}
pub fn with_advice_map(self, advice_map: AdviceMap) -> Self {
if advice_map.is_empty() {
return self;
}
let mast = (*self.mast).clone().with_advice_map(advice_map);
Self {
mast: Arc::new(mast),
entrypoint: self.entrypoint,
package_debug_info: self.package_debug_info,
}
}
}
impl PartialEq for TransactionScript {
fn eq(&self, other: &Self) -> bool {
self.mast == other.mast && self.entrypoint == other.entrypoint
}
}
impl Eq for TransactionScript {}
impl Serializable for TransactionScript {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.mast.write_into(target);
target.write_u32(u32::from(self.entrypoint));
}
}
impl Deserializable for TransactionScript {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let mast = MastForest::read_from(source)?;
let entrypoint = MastNodeId::from_u32_safe(source.read_u32()?, &mast)?;
Ok(Self::from_parts(Arc::new(mast), entrypoint))
}
}
#[cfg(test)]
mod tests {
use miden_core::advice::AdviceMap;
use crate::transaction::TransactionArgs;
use crate::utils::serde::{Deserializable, Serializable};
#[test]
fn test_tx_args_serialization() {
let tx_args = TransactionArgs::new(AdviceMap::default());
let bytes: std::vec::Vec<u8> = tx_args.to_bytes();
let decoded = TransactionArgs::read_from_bytes(&bytes).unwrap();
assert_eq!(tx_args, decoded);
}
#[test]
fn test_transaction_script_preserves_package_debug_info() {
use super::TransactionScript;
use crate::assembly::Assembler;
let assembler = Assembler::default();
let package =
assembler.assemble_program("test-transaction-script", "begin nop end").unwrap();
let script = TransactionScript::from_package(&package).unwrap();
assert!(script.loaded_mast_forest().package_debug_info().unwrap().is_some());
}
#[test]
fn test_transaction_script_with_advice_map() {
use miden_core::{Felt, Word};
use super::TransactionScript;
use crate::assembly::Assembler;
let assembler = Assembler::default();
let program = assembler
.assemble_program("test-transaction-script", "begin nop end")
.unwrap()
.try_into_program()
.unwrap();
let script = TransactionScript::new(program);
assert!(script.mast().advice_map().is_empty());
let original_root = script.root();
let script = script.with_advice_map(AdviceMap::default());
assert_eq!(original_root, script.root());
let key = Word::from([1u32, 2, 3, 4]);
let value = vec![Felt::new_unchecked(42), Felt::new_unchecked(43)];
let mut advice_map = AdviceMap::default();
advice_map.insert(key, value.clone());
let script = script.with_advice_map(advice_map);
let mast = script.mast();
let stored = mast.advice_map().get(&key).expect("entry should be present");
assert_eq!(stored.as_ref(), value.as_slice());
}
#[test]
fn test_transaction_script_from_library() {
use assert_matches::assert_matches;
use super::TransactionScript;
use crate::errors::TransactionScriptError;
use crate::testing::assembler::assemble_test_library;
use crate::utils::serde::{Deserializable, Serializable};
let source = "
@transaction_script
pub proc main
push.1 drop
end
";
let library = assemble_test_library("test-tx-script", "test::tx_script", source);
let script = TransactionScript::from_library(&library).unwrap();
let bytes = script.to_bytes();
let decoded = TransactionScript::read_from_bytes(&bytes).unwrap();
assert_eq!(script, decoded);
let no_attr = assemble_test_library(
"test-tx-script-no-attr",
"test::tx_script_no_attr",
"pub proc main push.1 drop end",
);
assert_matches!(
TransactionScript::from_library(&no_attr),
Err(TransactionScriptError::NoProcedureWithAttribute)
);
let multiple = assemble_test_library(
"test-tx-script-multiple",
"test::tx_script_multiple",
"@transaction_script pub proc main_a push.1 drop end
@transaction_script pub proc main_b push.2 drop end",
);
assert_matches!(
TransactionScript::from_library(&multiple),
Err(TransactionScriptError::MultipleProceduresWithAttribute)
);
}
#[test]
fn test_transaction_script_from_library_reference() {
use alloc::string::ToString;
use assert_matches::assert_matches;
use super::TransactionScript;
use crate::Word;
use crate::assembly::Path;
use crate::errors::TransactionScriptError;
use crate::testing::assembler::assemble_test_library;
let source = "
@transaction_script
pub proc main_a
push.1 drop
end
@transaction_script
pub proc main_b
push.2 drop
end
pub proc helper
push.3 drop
end
";
let library =
assemble_test_library("test-tx-script-reference", "test::tx_script_reference", source);
for proc_name in ["main_a", "main_b"] {
let export = library
.manifest
.exports()
.find(|e| e.path().as_ref().to_string().ends_with(proc_name))
.unwrap();
let digest = export.as_procedure().unwrap().digest;
let script =
TransactionScript::from_library_reference(&library, export.path().as_ref())
.unwrap();
assert_eq!(Word::from(script.root()), digest);
}
assert_matches!(
TransactionScript::from_library_reference(&library, Path::new("::foo::bar::main")),
Err(TransactionScriptError::ProcedureNotFound(_))
);
let helper = library
.manifest
.exports()
.find(|e| e.path().as_ref().to_string().ends_with("helper"))
.unwrap();
assert_matches!(
TransactionScript::from_library_reference(&library, helper.path().as_ref()),
Err(TransactionScriptError::ProcedureMissingAttribute(_))
);
}
}