diff --git a/src/vm/op_contract.rs b/src/vm/op_contract/mod.rs similarity index 99% rename from src/vm/op_contract.rs rename to src/vm/op_contract/mod.rs index a1c58b0d..8409af21 100644 --- a/src/vm/op_contract.rs +++ b/src/vm/op_contract/mod.rs @@ -672,3 +672,6 @@ impl Bytecode for ContractOp { }) } } + +#[cfg(test)] +mod tests; diff --git a/src/vm/op_contract/tests.rs b/src/vm/op_contract/tests.rs new file mode 100644 index 00000000..33399382 --- /dev/null +++ b/src/vm/op_contract/tests.rs @@ -0,0 +1,1782 @@ +use std::borrow::Borrow; +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet}; +use std::num::NonZeroU32; +use std::rc::Rc; + +use aluvm::data::{ByteStr, MaybeNumber}; +use aluvm::isa::ExecStep; +use aluvm::library::LibSite; +use aluvm::reg::{CoreRegs, Reg16, Reg32, RegA, RegS}; +use amplify::confinement::{NonEmptyOrdSet, NonEmptyVec, SmallBlob, SmallOrdMap}; +use amplify::num::u24; +use amplify::Wrapper; +use bp::{Outpoint, Txid}; +use strict_encoding::StrictDumb; + +use super::*; +use crate::operation::assignments::AssignVec; +use crate::operation::Operation; +use crate::vm::{ + ContractOp, ContractStateAccess, GlobalContractState, GlobalOrd, GlobalStateIter, OpInfo, + OrdOpRef, UnknownGlobalStateType, VmContext, WitnessOrd, WitnessPos, +}; +use crate::{ + schema, seal, Assign, AssignmentType, Assignments, BundleId, ChainNet, ContractId, Ffv, + FungibleState, Genesis, GenesisSeal, GlobalState, GlobalStateType, GraphSeal, Identity, Inputs, + MetaType, MetaValue, Metadata, Opout, RevealedData, RevealedValue, SchemaId, + SealClosingStrategy, Signature, Transition, TypedAssigns, +}; + +const DUMMY_ASSIGN_TYPE_FUNGIBLE: AssignmentType = AssignmentType::with(1000); +const DUMMY_ASSIGN_TYPE_DATA: AssignmentType = AssignmentType::with(1001); +const DUMMY_ASSIGN_TYPE_RIGHTS: AssignmentType = AssignmentType::with(1002); +const DUMMY_ASSIGN_TYPE_UNUSED: AssignmentType = AssignmentType::with(1003); + +const DUMMY_GLOBAL_TYPE_A: GlobalStateType = GlobalStateType::with(2000); +const DUMMY_GLOBAL_TYPE_B: GlobalStateType = GlobalStateType::with(2001); +const DUMMY_GLOBAL_TYPE_UNUSED: GlobalStateType = GlobalStateType::with(2002); + +const DUMMY_META_TYPE_A: MetaType = MetaType::with(3000); + +#[derive(Debug, Default, Clone, PartialEq)] +struct MockContractState { + global_data: BTreeMap>, + rights_data: BTreeMap<(Outpoint, AssignmentType), u32>, + fungible_data: BTreeMap<(Outpoint, AssignmentType), Vec>, + structured_data: BTreeMap<(Outpoint, AssignmentType), Vec>, + fail_global_access: bool, +} + +/// A mock implementation of `GlobalStateIter` initialized from a `Vec`. +pub struct MockGlobalStateIter<'a> { + data: &'a Vec<(GlobalOrd, RevealedData)>, + current_pos: usize, + last_item_cache: Option<(GlobalOrd, &'a RevealedData)>, + total_size: usize, +} + +impl<'a> MockGlobalStateIter<'a> { + pub fn new(initial_data: &'a Vec<(GlobalOrd, RevealedData)>) -> Self { + MockGlobalStateIter { + data: initial_data, + current_pos: 0, + last_item_cache: None, + total_size: initial_data.len(), + } + } +} + +impl<'a> GlobalStateIter for MockGlobalStateIter<'a> { + type Data = &'a RevealedData; + + fn size(&mut self) -> u24 { + u24::try_from(self.total_size as u32).expect("MockGlobalStateIter data size must fit u24") + } + + fn prev(&mut self) -> Option<(GlobalOrd, Self::Data)> { + if self.current_pos < self.total_size { + let (ord_ref, data_ref) = &self.data[self.current_pos]; + let item_to_return = (*ord_ref, data_ref); + + self.last_item_cache = Some(item_to_return); + self.current_pos += 1; + Some(item_to_return) + } else { + self.last_item_cache = None; + None + } + } + + fn last(&mut self) -> Option<(GlobalOrd, Self::Data)> { self.last_item_cache } + + fn reset(&mut self, depth_1based: u24) { + if depth_1based == u24::ZERO { + panic!("MockGlobalStateIter cannot be reset to depth 0"); + } + + let tgt_internal_idx = depth_1based.to_usize() - 1; + + if tgt_internal_idx < self.total_size { + // The item at internal_idx should become the "last" item. + let (ord_ref, data_ref) = &self.data[tgt_internal_idx]; + self.last_item_cache = Some((*ord_ref, data_ref)); + // The next call to prev() should yield the item *after* this one. + self.current_pos = tgt_internal_idx + 1; + } else { + // Requested 1-based depth is out of bounds. + // e.g., total_size=1. reset(depth_1based=2). internal_idx=1. + // 1 < 1 is false. Comes here. + self.last_item_cache = None; + self.current_pos = self.total_size; + } + } +} + +static EMPTY_GLOBAL_VEC: Vec<(GlobalOrd, RevealedData)> = Vec::new(); + +impl ContractStateAccess for MockContractState { + fn global( + &self, + ty: GlobalStateType, + ) -> Result, UnknownGlobalStateType> { + if self.fail_global_access { + return Err(UnknownGlobalStateType(ty)); + } + + let data_ref: &Vec<(GlobalOrd, RevealedData)> = self + .global_data + .get(&ty) + .map_or(&EMPTY_GLOBAL_VEC, |vec| vec); + + let iter = MockGlobalStateIter::new(data_ref); + + Ok(GlobalContractState::new(iter)) + } + + fn rights(&self, outpoint: Outpoint, ty: AssignmentType) -> u32 { + self.rights_data.get(&(outpoint, ty)).cloned().unwrap_or(0) + } + + fn fungible( + &self, + outpoint: Outpoint, + ty: AssignmentType, + ) -> impl DoubleEndedIterator { + self.fungible_data + .get(&(outpoint, ty)) + .cloned() + .unwrap_or_default() + .into_iter() + } + + fn data( + &self, + outpoint: Outpoint, + ty: AssignmentType, + ) -> impl DoubleEndedIterator> { + self.structured_data + .get(&(outpoint, ty)) + .cloned() + .unwrap_or_default() + .into_iter() + } +} +fn dummy_genesis() -> Genesis { + Genesis { + ffv: Ffv::default(), + schema_id: SchemaId::strict_dumb(), + timestamp: 0, + issuer: Identity::strict_dumb(), + chain_net: ChainNet::BitcoinRegtest, + seal_closing_strategy: SealClosingStrategy::default(), + metadata: Metadata::default(), + globals: GlobalState::default(), + assignments: Assignments::::default(), + } +} + +fn dummy_witness_pos() -> WitnessPos { + WitnessPos::bitcoin(NonZeroU32::new(1).unwrap(), 1231006505).unwrap() +} + +fn dummy_witness_ord_mined() -> WitnessOrd { WitnessOrd::Mined(dummy_witness_pos()) } + +fn dummy_transition(contract_id: ContractId, signature: Option) -> Transition { + let dummy_opout = Opout::strict_dumb(); + let mut opout_set = BTreeSet::new(); + opout_set.insert(dummy_opout); + let nonempty_opout_set = NonEmptyOrdSet::try_from(opout_set).expect("Should not be empty"); + let inputs = Inputs::from(nonempty_opout_set); + + Transition { + ffv: Ffv::default(), + contract_id, + nonce: 0, + transition_type: schema::TransitionType::strict_dumb(), + metadata: Metadata::default(), + globals: GlobalState::default(), + inputs, + assignments: Assignments::::default(), + signature, + } +} + +fn exec_op_and_assert_st0( + op: ContractOp, + regs: &mut CoreRegs, + context: &VmContext, + expected_st0_ok: bool, +) { + let step = op.exec(regs, LibSite::default(), context); + assert_eq!( + regs.status(), + expected_st0_ok, + "ST0 flag mismatch for op {:?}. Expected {}, got {}", + op, + expected_st0_ok, + regs.status() + ); + if !expected_st0_ok { + assert_eq!(step, ExecStep::Stop, "ExecStep should be Stop on failure for op {:?}", op); + } else { + assert_eq!(step, ExecStep::Next, "ExecStep should be Next on success for op {:?}", op); + } +} + +fn create_vm_context( + contract_id: ContractId, + op_info: OpInfo<'_>, + contract_state: Rc>, +) -> VmContext<'_, S> { + VmContext { + contract_id, + op_info, + contract_state, + } +} + +fn assignments_from_typed( + map: BTreeMap>, +) -> Assignments { + let mut confined_map = SmallOrdMap::new(); + for (k, v) in map { + confined_map.insert(k, v).unwrap(); + } + Assignments::from_inner(confined_map) +} + +fn create_fungible_assign_vec( + seal: GraphSeal, + values: Vec, +) -> AssignVec> { + let assigns = values + .into_iter() + .map(|v| Assign::Revealed { + seal, + state: RevealedValue::from(v), + }) + .collect::>(); + if assigns.is_empty() { + panic!("create_fungible_assignments called with empty values"); + } + AssignVec::with(NonEmptyVec::try_from(assigns).unwrap()) +} + +fn create_structured_assign_vec( + seal: GraphSeal, + data_items: Vec>, +) -> AssignVec> { + let assigns = data_items + .into_iter() + .map(|d| Assign::Revealed { + seal, + state: RevealedData::new(SmallBlob::try_from(d).unwrap()), + }) + .collect::>(); + if assigns.is_empty() { + panic!("create_structured_assignments called with empty data"); + } + AssignVec::with(NonEmptyVec::try_from(assigns).unwrap()) +} + +struct TestEnv { + contract_id: ContractId, + genesis_val: Option, + transition_val: Option, + prev_assignments_val: Assignments, + owned_assignments_val: Assignments, + globals_val: GlobalState, + metadata_val: Metadata, + mock_contract_state_rc: Rc>, + regs: CoreRegs, +} + +impl TestEnv { + fn for_genesis() -> Self { + let genesis = dummy_genesis(); + let contract_id = genesis.contract_id(); + Self { + contract_id, + genesis_val: Some(genesis), + transition_val: None, + prev_assignments_val: Assignments::default(), + owned_assignments_val: Assignments::default(), + globals_val: GlobalState::default(), + metadata_val: Metadata::default(), + mock_contract_state_rc: Rc::new(RefCell::new(MockContractState::default())), + regs: CoreRegs::default(), + } + } + + fn for_transition() -> Self { + let contract_id = ContractId::strict_dumb(); + let transition = dummy_transition(contract_id, None); + Self { + contract_id, + genesis_val: None, + transition_val: Some(transition), + prev_assignments_val: Assignments::default(), + owned_assignments_val: Assignments::default(), + globals_val: GlobalState::default(), + metadata_val: Metadata::default(), + mock_contract_state_rc: Rc::new(RefCell::new(MockContractState::default())), + regs: CoreRegs::default(), + } + } + + fn add_global_current_op(mut self, global_type: GlobalStateType, data: Vec) -> Self { + let revealed_data = RevealedData::new(SmallBlob::try_from(data).unwrap()); + if let Some(g) = self.genesis_val.as_mut() { + g.globals.add_state(global_type, revealed_data).unwrap(); + } else if let Some(t) = self.transition_val.as_mut() { + t.globals.add_state(global_type, revealed_data).unwrap(); + } + self.globals_val = if self.genesis_val.is_some() { + self.genesis_val.as_ref().unwrap().globals.clone() + } else { + self.transition_val.as_ref().unwrap().globals.clone() + }; + self + } + + fn add_metadata_current_op(mut self, meta_type: MetaType, data: Vec) -> Self { + let meta_value = MetaValue::from(SmallBlob::try_from(data).unwrap()); + if let Some(g) = self.genesis_val.as_mut() { + g.metadata.insert(meta_type, meta_value).unwrap(); + } else if let Some(t) = self.transition_val.as_mut() { + t.metadata.insert(meta_type, meta_value).unwrap(); + } + self.metadata_val = if self.genesis_val.is_some() { + self.genesis_val.as_ref().unwrap().metadata.clone() + } else { + self.transition_val.as_ref().unwrap().metadata.clone() + }; + self + } + + fn add_prev_assign_fungible(mut self, assign_type: AssignmentType, values: Vec) -> Self { + let typed_assigns = + TypedAssigns::Fungible(create_fungible_assign_vec(GraphSeal::strict_dumb(), values)); + self.prev_assignments_val + .insert(assign_type, typed_assigns) + .unwrap(); + self + } + + fn add_prev_assign_structured( + mut self, + assign_type: AssignmentType, + data_items: Vec>, + ) -> Self { + let typed_assigns = TypedAssigns::Structured(create_structured_assign_vec( + GraphSeal::strict_dumb(), + data_items, + )); + self.prev_assignments_val + .insert(assign_type, typed_assigns) + .unwrap(); + self + } + + fn add_owned_assign_fungible(mut self, assign_type: AssignmentType, values: Vec) -> Self { + let typed_assigns = + TypedAssigns::Fungible(create_fungible_assign_vec(GraphSeal::strict_dumb(), values)); + if let Some(g) = self.genesis_val.as_mut() { + panic!( + "add_owned_assign_fungible for Genesis not fully implemented in TestEnv due to \ + Seal type mismatch" + ); + } else if let Some(t) = self.transition_val.as_mut() { + t.assignments.insert(assign_type, typed_assigns).unwrap(); + } + self.owned_assignments_val = self.transition_val.as_ref().unwrap().assignments.clone(); + self + } + + fn add_owned_assign_structured( + mut self, + assign_type: AssignmentType, + data_items: Vec>, + ) -> Self { + let typed_assigns = TypedAssigns::Structured(create_structured_assign_vec( + GraphSeal::strict_dumb(), + data_items, + )); + if let Some(g) = self.genesis_val.as_mut() { + panic!( + "add_owned_assign_structured for Genesis not fully implemented in TestEnv due to \ + Seal type mismatch" + ); + } else if let Some(t) = self.transition_val.as_mut() { + t.assignments.insert(assign_type, typed_assigns).unwrap(); + } + self.owned_assignments_val = self.transition_val.as_ref().unwrap().assignments.clone(); + self + } + + fn set_mock_global_state_history( + self, + global_type: GlobalStateType, + history: Vec<(GlobalOrd, RevealedData)>, + ) -> Self { + self.mock_contract_state_rc + .borrow_mut() + .global_data + .insert(global_type, history); + self + } + + fn set_mock_fail_global_access(self, fail: bool) -> Self { + self.mock_contract_state_rc.borrow_mut().fail_global_access = fail; + self + } + + fn execute<'this_env>( + &'this_env mut self, + op_code: ContractOp, + expected_st0_ok: bool, + ) where + MockContractState: 'this_env, + { + let op_info: OpInfo; + let ord_op_ref_val_owned: OrdOpRef; + + if let Some(genesis) = &self.genesis_val { + ord_op_ref_val_owned = OrdOpRef::Genesis(genesis); + op_info = OpInfo { + id: genesis.id(), + prev_state: &self.prev_assignments_val, + op: &ord_op_ref_val_owned, + }; + } else if let Some(transition) = &self.transition_val { + let txid = Txid::strict_dumb(); + let bundle_id = BundleId::strict_dumb(); + ord_op_ref_val_owned = + OrdOpRef::Transition(transition, txid, dummy_witness_ord_mined(), bundle_id); + op_info = OpInfo { + id: transition.id(), + prev_state: &self.prev_assignments_val, + op: &ord_op_ref_val_owned, + }; + } else { + panic!("TestEnv not initialized with an operation"); + } + + let context = + create_vm_context(self.contract_id, op_info, self.mock_contract_state_rc.clone()); + exec_op_and_assert_st0(op_code, &mut self.regs, &context, expected_st0_ok); + } +} + +mod count_ops { + use aluvm::data::{MaybeNumber, Number}; + use aluvm::reg::{Reg32, RegA}; + + use super::*; + + // CnP (Count Previous state) + #[test] + fn test_cnp_found_multiple() { + let mut env = TestEnv::for_transition().add_prev_assign_fungible( + DUMMY_ASSIGN_TYPE_FUNGIBLE, + vec![10, 20, 30], // 3 items + ); + let op_code = ContractOp::CnP(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A16, Reg32::Reg0), MaybeNumber::from(Number::from(3u16))); + } + + #[test] + fn test_cnp_found_single() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10]); + let op_code = ContractOp::CnP(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A16, Reg32::Reg0), MaybeNumber::from(Number::from(1u16))); + } + + #[test] + fn test_cnp_type_not_found_in_prev_state() { + let mut env = TestEnv::for_transition(); + let op_code = ContractOp::CnP(DUMMY_ASSIGN_TYPE_UNUSED, Reg32::Reg0); + env.execute(op_code, true); + // CnP sets target register to None if the type is not found in prev_state + assert_eq!(env.regs.get_n(RegA::A16, Reg32::Reg0), MaybeNumber::none()); + } + + // CnS (Count Same [owned] state) + #[test] + fn test_cns_found_multiple_transition() { + let mut env = TestEnv::for_transition().add_owned_assign_structured( + DUMMY_ASSIGN_TYPE_DATA, + vec![vec![1], vec![2]], // 2 items + ); + let op_code = ContractOp::CnS(DUMMY_ASSIGN_TYPE_DATA, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A16, Reg32::Reg0), MaybeNumber::from(Number::from(2u16))); + } + + #[test] + fn test_cns_found_single_transition() { + let mut env = TestEnv::for_transition() + .add_owned_assign_structured(DUMMY_ASSIGN_TYPE_DATA, vec![vec![1]]); + let op_code = ContractOp::CnS(DUMMY_ASSIGN_TYPE_DATA, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A16, Reg32::Reg0), MaybeNumber::from(Number::from(1u16))); + } + + #[test] + fn test_cns_type_not_found_in_owned_state_transition() { + let mut env = TestEnv::for_transition(); + let op_code = ContractOp::CnS(DUMMY_ASSIGN_TYPE_UNUSED, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A16, Reg32::Reg0), MaybeNumber::none()); + } + + // CnS for Genesis + #[test] + fn test_cns_genesis_type_not_found() { + let mut env = TestEnv::for_genesis(); + let op_code = ContractOp::CnS(DUMMY_ASSIGN_TYPE_UNUSED, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A16, Reg32::Reg0), MaybeNumber::none()); + } + + // CnG (Count Next [current op's] Global state) + #[test] + fn test_cng_found_multiple() { + let mut env = TestEnv::for_genesis() + .add_global_current_op(DUMMY_GLOBAL_TYPE_A, vec![1]) + .add_global_current_op(DUMMY_GLOBAL_TYPE_A, vec![2]); + let op_code = ContractOp::CnG(DUMMY_GLOBAL_TYPE_A, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A8, Reg32::Reg0), MaybeNumber::from(Number::from(2u8))); + } + + #[test] + fn test_cng_found_single() { + let mut env = TestEnv::for_genesis().add_global_current_op(DUMMY_GLOBAL_TYPE_A, vec![1]); + let op_code = ContractOp::CnG(DUMMY_GLOBAL_TYPE_A, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A8, Reg32::Reg0), MaybeNumber::from(Number::from(1u8))); + } + + #[test] + fn test_cng_type_not_found_in_globals() { + let mut env = TestEnv::for_genesis(); + let op_code = ContractOp::CnG(DUMMY_GLOBAL_TYPE_UNUSED, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A8, Reg32::Reg0), MaybeNumber::none()); + } + + // CnC (Count Contract's [historical] Global state) + #[test] + fn test_cnc_found_multiple() { + let history = vec![ + (GlobalOrd::genesis(0), RevealedData::new(SmallBlob::try_from(vec![1]).unwrap())), + (GlobalOrd::genesis(1), RevealedData::new(SmallBlob::try_from(vec![2]).unwrap())), + ]; + let mut env = + TestEnv::for_genesis().set_mock_global_state_history(DUMMY_GLOBAL_TYPE_A, history); + let op_code = ContractOp::CnC(DUMMY_GLOBAL_TYPE_A, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A32, Reg32::Reg0), MaybeNumber::from(Number::from(2u32))); + } + + #[test] + fn test_cnc_found_single() { + let history = + vec![(GlobalOrd::genesis(0), RevealedData::new(SmallBlob::try_from(vec![1]).unwrap()))]; + let mut env = + TestEnv::for_genesis().set_mock_global_state_history(DUMMY_GLOBAL_TYPE_A, history); + let op_code = ContractOp::CnC(DUMMY_GLOBAL_TYPE_A, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A32, Reg32::Reg0), MaybeNumber::from(Number::from(1u32))); + } + + #[test] + fn test_cnc_type_not_found_in_history() { + let mut env = TestEnv::for_genesis(); + let op_code = ContractOp::CnC(DUMMY_GLOBAL_TYPE_UNUSED, Reg32::Reg0); + env.execute(op_code, true); + // If type not found in BTreeMap, unwrap_or_default gives empty vec, size 0. + assert_eq!(env.regs.get_n(RegA::A32, Reg32::Reg0), MaybeNumber::from(Number::from(0u32))); + } + + #[test] + fn test_cnc_fail_on_global_access_error() { + let mut env = TestEnv::for_genesis().set_mock_fail_global_access(true); + let op_code = ContractOp::CnC(DUMMY_GLOBAL_TYPE_A, Reg32::Reg0); + env.execute(op_code, true); + assert_eq!(env.regs.get_n(RegA::A32, Reg32::Reg0), MaybeNumber::none()); + } +} + +mod load_ops { + use aluvm::data::Number; + use amplify::confinement::SmallBlob; + use bp::seals::SecretSeal; + + use super::*; + use crate::{OpId, TransitionType}; + + // LdP (Load Previous structured state) + #[test] + fn test_ldp_success_revealed() { + let data_vec = vec![0xAB, 0xCD, 0xEF]; + let mut env = TestEnv::for_transition() + .add_prev_assign_structured(DUMMY_ASSIGN_TYPE_DATA, vec![data_vec.clone()]); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + + let op_code = ContractOp::LdP(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(0)); + env.execute(op_code, true); + + assert_eq!(env.regs.s16(RegS::from(0)).unwrap().as_ref(), data_vec.as_slice()); + } + + #[test] + fn test_ldp_success_concealed_seal_loads_state() { + let data_vec = vec![0xAA, 0xBB]; + let revealed_data = RevealedData::new(SmallBlob::try_from(data_vec.clone()).unwrap()); + let concealed_assign = Assign::ConfidentialSeal { + seal: SecretSeal::strict_dumb(), + state: revealed_data, + }; + + let mut prev_map = BTreeMap::new(); + prev_map.insert( + DUMMY_ASSIGN_TYPE_DATA, + TypedAssigns::Structured(AssignVec::with(NonEmptyVec::with(concealed_assign))), + ); + let mut env = TestEnv::for_transition(); + env.prev_assignments_val = assignments_from_typed(prev_map); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + + let op_code = ContractOp::LdP(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(0)); + env.execute(op_code, true); + assert_eq!(env.regs.s16(RegS::from(0)).unwrap().as_ref(), data_vec.as_slice()); + } + + #[test] + fn test_ldp_fail_index_reg_none() { + let mut env = TestEnv::for_transition(); + let op_code = ContractOp::LdP(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(0)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(0)).is_none()); + } + + #[test] + fn test_ldp_fail_state_type_missing_in_prev() { + let mut env = TestEnv::for_transition(); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + let op_code = ContractOp::LdP(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(0)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(0)).is_none()); + } + + #[test] + fn test_ldp_fail_index_oob() { + // 1 item at index 0 + let mut env = TestEnv::for_transition() + .add_prev_assign_structured(DUMMY_ASSIGN_TYPE_DATA, vec![vec![1]]); + // Request index 1 + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(1u16)); + + let op_code = ContractOp::LdP(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(0)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(0)).is_none()); + } + + #[test] + fn test_ldp_fail_wrong_state_type_in_prev() { + // prev_state has DUMMY_ASSIGN_TYPE_DATA, but it's Fungible, not structured for LdP + let mut env = + TestEnv::for_transition().add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_DATA, vec![100]); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + + let op_code = ContractOp::LdP(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(0)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(0)).is_none()); + } + + // LdS (Load Same/owned structured state) + #[test] + fn test_lds_success_revealed() { + let data_vec = vec![0xBE, 0xEF]; + let mut env = TestEnv::for_transition() + .add_owned_assign_structured(DUMMY_ASSIGN_TYPE_DATA, vec![data_vec.clone()]); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + + let op_code = ContractOp::LdS(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(1)); + env.execute(op_code, true); + assert_eq!(env.regs.s16(RegS::from(1)).unwrap().as_ref(), data_vec.as_slice()); + } + + #[test] + fn test_lds_fail_index_reg_none() { + let mut env = TestEnv::for_transition(); + let op_code = ContractOp::LdS(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(1)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(1)).is_none()); + } + + #[test] + fn test_lds_fail_state_type_missing_in_owned() { + let mut env = TestEnv::for_transition(); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + let op_code = ContractOp::LdS(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(1)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(1)).is_none()); + } + + #[test] + fn test_lds_fail_index_oob() { + let mut env = TestEnv::for_transition() + .add_owned_assign_structured(DUMMY_ASSIGN_TYPE_DATA, vec![vec![1]]); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(1u16)); + let op_code = ContractOp::LdS(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(1)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(1)).is_none()); + } + + #[test] + fn test_lds_fail_wrong_state_type_in_owned() { + // Data is fungible + let mut env = + TestEnv::for_transition().add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_DATA, vec![100]); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + let op_code = ContractOp::LdS(DUMMY_ASSIGN_TYPE_DATA, Reg16::Reg0, RegS::from(1)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(1)).is_none()); + } + + // LdF (Load Same/owned Fungible state) + #[test] + fn test_ldf_success_revealed() { + let fungible_val = 777u64; + let mut env = TestEnv::for_transition() + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![fungible_val]); + // index_reg for source; destination is a64[Reg16::Reg0] + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + + let op_code = ContractOp::LdF(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg16::Reg0, Reg16::Reg0); + env.execute(op_code, true); + assert_eq!( + env.regs.get_n(RegA::A64, Reg32::Reg0), + MaybeNumber::from(Number::from(fungible_val)) + ); + } + + #[test] + fn test_ldf_fail_index_reg_none() { + let mut env = TestEnv::for_transition(); + let op_code = ContractOp::LdF(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg16::Reg0, Reg16::Reg0); + env.execute(op_code, false); + assert!(env.regs.get_n(RegA::A64, Reg32::Reg0).is_none()); + } + + #[test] + fn test_ldf_fail_state_type_missing_in_owned() { + let mut env = TestEnv::for_transition(); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + let op_code = ContractOp::LdF(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg16::Reg0, Reg16::Reg0); + env.execute(op_code, false); + assert!(env.regs.get_n(RegA::A64, Reg32::Reg0).is_none()); + } + + #[test] + fn test_ldf_fail_index_oob() { + let mut env = TestEnv::for_transition() + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![100]); + // Request index 1 + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(1u16)); + let op_code = ContractOp::LdF(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg16::Reg0, Reg16::Reg0); + env.execute(op_code, false); + assert!(env.regs.get_n(RegA::A64, Reg32::Reg0).is_none()); + } + + #[test] + fn test_ldf_fail_wrong_state_type_in_owned() { + // Data is structured + let mut env = TestEnv::for_transition() + .add_owned_assign_structured(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![vec![1]]); + env.regs.set_n(RegA::A16, Reg32::Reg0, Number::from(0u16)); + let op_code = ContractOp::LdF(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg16::Reg0, Reg16::Reg0); + env.execute(op_code, false); + assert!(env.regs.get_n(RegA::A64, Reg32::Reg0).is_none()); + } + + // LdG (Load Global state from current op) + #[test] + fn test_ldg_success() { + let data_vec = vec![0xC0, 0xDE]; + let mut env = + TestEnv::for_genesis().add_global_current_op(DUMMY_GLOBAL_TYPE_A, data_vec.clone()); + // index_reg for source (a8) + env.regs.set_n(RegA::A8, Reg32::Reg0, Number::from(0u8)); + + let op_code = ContractOp::LdG(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(2)); + env.execute(op_code, true); + assert_eq!(env.regs.s16(RegS::from(2)).unwrap().as_ref(), data_vec.as_slice()); + } + + #[test] + fn test_ldg_success_multiple_items_correct_index() { + let data_vec1 = vec![0xC0]; + let data_vec2 = vec![0xDE]; + let mut env = TestEnv::for_genesis() + .add_global_current_op(DUMMY_GLOBAL_TYPE_A, data_vec1.clone()) + .add_global_current_op(DUMMY_GLOBAL_TYPE_A, data_vec2.clone()); + // index = 1 (for the second item) + env.regs.set_n(RegA::A8, Reg32::Reg0, Number::from(1u8)); + + let op_code = ContractOp::LdG(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(2)); + env.execute(op_code, true); + assert_eq!(env.regs.s16(RegS::from(2)).unwrap().as_ref(), data_vec2.as_slice()); + } + + #[test] + fn test_ldg_fail_index_reg_none() { + let mut env = TestEnv::for_genesis().add_global_current_op(DUMMY_GLOBAL_TYPE_A, vec![1, 2]); + // Index register a8[0] is deliberately not set (i.e., None) + let op_code = ContractOp::LdG(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(2)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(2)).is_none()); + } + + #[test] + fn test_ldg_fail_state_type_missing_in_globals() { + // Globals are empty for DUMMY_GLOBAL_TYPE_A + let mut env = TestEnv::for_genesis(); + env.regs.set_n(RegA::A8, Reg32::Reg0, Number::from(0u8)); + let op_code = ContractOp::LdG(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(2)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(2)).is_none()); + } + + #[test] + fn test_ldg_fail_index_oob() { + // 1 item + let mut env = TestEnv::for_genesis().add_global_current_op(DUMMY_GLOBAL_TYPE_A, vec![1, 2]); + // Request index 1 (OOB for 1 item) + env.regs.set_n(RegA::A8, Reg32::Reg0, Number::from(1u8)); + + let op_code = ContractOp::LdG(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(2)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(2)).is_none()); + } + + // LdC (Load Global state from Contract history) + #[test] + fn test_ldc_success() { + let data_vec_hist = vec![0x12, 0x34]; + let history = vec![( + GlobalOrd::genesis(0), // Dummy GlobalOrd + RevealedData::new(SmallBlob::try_from(data_vec_hist.clone()).unwrap()), + )]; + // LdC uses contract_state, not current op's state + let mut env = + TestEnv::for_genesis().set_mock_global_state_history(DUMMY_GLOBAL_TYPE_A, history); + // let the depth eq 1 + env.regs.set_n(RegA::A32, Reg32::Reg0, Number::from(1u32)); + + let op_code = ContractOp::LdC(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(3)); + env.execute(op_code, true); + assert_eq!(env.regs.s16(RegS::from(3)).unwrap().as_ref(), data_vec_hist.as_slice()); + } + + #[test] + fn test_ldc_success_at_depth_one_and_two() { + let data_vec_hist1 = vec![0x01, 0x02]; + let data_vec_hist2 = vec![0x03, 0x04]; + let history = vec![ + ( + // d = 1 + GlobalOrd::genesis(1), + RevealedData::new(SmallBlob::try_from(data_vec_hist1.clone()).unwrap()), + ), + ( + // d = 2 + GlobalOrd::transition( + OpId::from([0; 32]), + 0, + TransitionType::with(0), + 0, + WitnessOrd::Ignored, + ), + RevealedData::new(SmallBlob::try_from(data_vec_hist2.clone()).unwrap()), + ), + ]; + let mut env = + TestEnv::for_genesis().set_mock_global_state_history(DUMMY_GLOBAL_TYPE_A, history); + env.regs.set_n(RegA::A32, Reg32::Reg0, Number::from(1u32)); + + let op_code = ContractOp::LdC(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(3)); + env.execute(op_code, true); + assert_eq!(env.regs.s16(RegS::from(3)).unwrap().as_ref(), data_vec_hist1.as_slice()); + + env.regs.set_n(RegA::A32, Reg32::Reg0, Number::from(2u32)); + let op_code = ContractOp::LdC(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(4)); + env.execute(op_code, true); + assert_eq!(env.regs.s16(RegS::from(4)).unwrap().as_ref(), data_vec_hist2.as_slice()); + } + + #[test] + fn test_ldc_fail_mock_global_access_error() { + let mut env = TestEnv::for_genesis().set_mock_fail_global_access(true); + env.regs.set_n(RegA::A32, Reg32::Reg0, Number::from(0u32)); + let op_code = ContractOp::LdC(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(3)); + // fail!() is called if contract_state.global() errors + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(3)).is_none()); + } + + #[test] + fn test_ldc_fail_depth_reg_none() { + let mut env = TestEnv::for_genesis(); + let op_code = ContractOp::LdC(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(3)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(3)).is_none()); + } + + #[test] + fn test_ldc_fail_depth_oob_in_history() { + let history = + vec![(GlobalOrd::genesis(0), RevealedData::new(SmallBlob::try_from(vec![1]).unwrap()))]; + let mut env = + TestEnv::for_genesis().set_mock_global_state_history(DUMMY_GLOBAL_TYPE_A, history); + env.regs.set_n(RegA::A32, Reg32::Reg0, Number::from(2u32)); + + let op_code = ContractOp::LdC(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(3)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(3)).is_none()); + } + + #[test] + fn test_ldc_fail_depth_too_large_for_u24() { + let mut env = TestEnv::for_genesis(); + // Set depth register to a value greater than u24::MAX to test saturation/error handling + env.regs + .set_n(RegA::A32, Reg32::Reg0, Number::from(u24::MAX.to_u32() + 1)); + + let op_code = ContractOp::LdC(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(3)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(3)).is_none()); + } + + // LdM (Load Metadata from current op) + #[test] + fn test_ldm_success() { + let meta_bytes = vec![0xDA, 0x7A]; + let mut env = + TestEnv::for_genesis().add_metadata_current_op(DUMMY_META_TYPE_A, meta_bytes.clone()); + + let op_code = ContractOp::LdM(DUMMY_META_TYPE_A, RegS::from(4)); + env.execute(op_code, true); + assert_eq!(env.regs.s16(RegS::from(4)).unwrap().as_ref(), meta_bytes.as_slice()); + } + + #[test] + fn test_ldm_fail_meta_type_missing() { + let mut env = TestEnv::for_genesis(); + let op_code = ContractOp::LdM(DUMMY_META_TYPE_A, RegS::from(4)); + env.execute(op_code, false); + assert!(env.regs.s16(RegS::from(4)).is_none()); + } +} + +mod sum_verification_ops { + use aluvm::data::Number; + use aluvm::reg::{Reg32, RegA}; + + use super::*; + + // Svs (Sum Verify Same state) + #[test] + fn test_svs_success_equal_sum() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10, 20]) // Prev sum = 30 + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![5, 25]); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, true); + } + + #[test] + fn test_svs_success_zero_sum_both_empty() { + let mut env = TestEnv::for_transition(); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, true); + } + + #[test] + fn test_svs_success_zero_sum_with_zero_values() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![0, 0]) + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![0]); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, true); + } + + #[test] + fn test_svs_fail_unequal_sum() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10, 20]) // Prev sum = 30 + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![5, 20]); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_svs_fail_only_inputs_present() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10, 20]); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_svs_fail_only_outputs_present() { + let mut env = TestEnv::for_transition() + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![5, 25]); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_svs_fail_input_not_fungible() { + let mut env = TestEnv::for_transition() + .add_prev_assign_structured(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![vec![1]]) // Wrong type for prev + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10]); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_svs_fail_output_not_fungible() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10]) + .add_owned_assign_structured(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![vec![1]]); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_svs_fail_input_sum_overflow() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![u64::MAX, 1]) // Overflow + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10]); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_svs_fail_output_sum_overflow() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10]) + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![u64::MAX, 1]); + let op_code = ContractOp::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + // SaS (Sum verify Assigned state) + #[test] + fn test_sas_success() { + let mut env = TestEnv::for_transition() + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10, 20]); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(30u64)); + + let op_code = ContractOp::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, true); + } + + #[test] + fn test_sas_fail_sum_reg_none() { + let mut env = TestEnv::for_transition() + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10, 20]); + // a64[0] is None + let op_code = ContractOp::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_sas_fail_sum_mismatch() { + let mut env = TestEnv::for_transition() + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10, 20]); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(31u64)); + + let op_code = ContractOp::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_sas_fail_owned_state_type_missing() { + let mut env = TestEnv::for_transition(); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(1u64)); + let op_code = ContractOp::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE); + // owned sum is 0, a64[0] is 1 -> fail + env.execute(op_code, false); + } + + #[test] + fn test_sas_success_owned_state_type_missing_and_sum_reg_zero() { + let mut env = TestEnv::for_transition(); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(0u64)); + + let op_code = ContractOp::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE); + // owned sum is 0, a64[0] is 0 -> success + env.execute(op_code, true); + } + + #[test] + fn test_sas_fail_owned_state_not_fungible() { + let mut env = TestEnv::for_transition() + .add_owned_assign_structured(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![vec![1]]); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(0u64)); + + let op_code = ContractOp::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_sas_fail_owned_state_contains_zero_value() { + // SaS specifically fails if any of the outputted fungible values are zero + let mut env = TestEnv::for_transition() + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![10, 0, 20]); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(30u64)); + + let op_code = ContractOp::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_sas_fail_owned_sum_overflow() { + let mut env = TestEnv::for_transition() + .add_owned_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![u64::MAX, 1]); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(10u64)); + + let op_code = ContractOp::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + // SpS (Sum verify Previous state) + #[test] + fn test_sps_success() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![15, 25]); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(40u64)); + + let op_code = ContractOp::Sps(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, true); + } + + #[test] + fn test_sps_fail_sum_reg_none() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![15, 25]); + // a64[0] is None + let op_code = ContractOp::Sps(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_sps_fail_sum_mismatch() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![15, 25]); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(41u64)); + + let op_code = ContractOp::Sps(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_sps_fail_prev_state_type_missing() { + let mut env = TestEnv::for_transition(); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(1u64)); + + let op_code = ContractOp::Sps(DUMMY_ASSIGN_TYPE_FUNGIBLE); + // prev sum is 0, a64[0] is 1 -> fail + env.execute(op_code, false); + } + + #[test] + fn test_sps_success_prev_state_type_missing_and_sum_reg_zero() { + let mut env = TestEnv::for_transition(); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(0u64)); + + let op_code = ContractOp::Sps(DUMMY_ASSIGN_TYPE_FUNGIBLE); + // prev sum is 0, a64[0] is 0 -> success + env.execute(op_code, true); + } + + #[test] + fn test_sps_fail_prev_state_not_fungible() { + let mut env = TestEnv::for_transition() + .add_prev_assign_structured(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![vec![1]]); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(0u64)); + + let op_code = ContractOp::Sps(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } + + #[test] + fn test_sps_fail_prev_sum_overflow() { + let mut env = TestEnv::for_transition() + .add_prev_assign_fungible(DUMMY_ASSIGN_TYPE_FUNGIBLE, vec![u64::MAX, 1]); + env.regs.set_n(RegA::A64, Reg32::Reg0, Number::from(10u64)); + + let op_code = ContractOp::Sps(DUMMY_ASSIGN_TYPE_FUNGIBLE); + env.execute(op_code, false); + } +} + +mod vts_op { + use aluvm::data::ByteStr; + use amplify::Bytes64; + use secp256k1::{generate_keypair, rand, Message as SecpMessage, Secp256k1}; + + use super::*; + + #[test] + fn test_vts_success() { + let mut env = TestEnv::for_transition(); + let secp = Secp256k1::new(); + let (secret_key, public_key) = generate_keypair(&mut rand::thread_rng()); + let public_key_bytes_compact = public_key.serialize(); + env.regs + .set_s16(u4::with(0), ByteStr::with(&public_key_bytes_compact[..])); + + let transition_op_val_mut = env.transition_val.as_mut().unwrap(); + let transition_id_bytes = transition_op_val_mut.id().into_inner().into_inner(); + let message = SecpMessage::from_digest_slice(&transition_id_bytes).expect("32 bytes"); + let sig = secp.sign_ecdsa(&message, &secret_key); + transition_op_val_mut.signature = Some(Bytes64::from_array(sig.serialize_compact()).into()); + + let op_code = ContractOp::Vts(RegS::from(0)); + env.execute(op_code, true); + } + + #[test] + fn test_vts_fail_no_signature_in_transition() { + let mut env = TestEnv::for_transition(); + let (_secret_key, public_key) = generate_keypair(&mut rand::thread_rng()); + let public_key_bytes_compact = public_key.serialize(); + env.regs + .set_s16(u4::with(0), ByteStr::with(&public_key_bytes_compact[..])); + // env.transition_val.as_mut().unwrap().signature is already None by dummy_transition + + let op_code = ContractOp::Vts(RegS::from(0)); + env.execute(op_code, false); + } + + #[test] + fn test_vts_fail_pubkey_reg_none() { + let mut env = TestEnv::for_transition(); + let secp = Secp256k1::new(); + let (secret_key, _public_key) = generate_keypair(&mut rand::thread_rng()); + + let transition_op_val_mut = env.transition_val.as_mut().unwrap(); + let transition_id_bytes = transition_op_val_mut.id().into_inner().into_inner(); + let message = SecpMessage::from_digest_slice(&transition_id_bytes).expect("32 bytes"); + let sig = secp.sign_ecdsa(&message, &secret_key); + transition_op_val_mut.signature = Some(Bytes64::from_array(sig.serialize_compact()).into()); + + let op_code = ContractOp::Vts(RegS::from(0)); + env.execute(op_code, false); + } + + #[test] + fn test_vts_fail_invalid_pubkey_format() { + let mut env = TestEnv::for_transition(); + env.regs + .set_s16(u4::with(0), ByteStr::with([0x00, 0x01, 0x02])); + let secp = Secp256k1::new(); + let (secret_key, _public_key) = generate_keypair(&mut rand::thread_rng()); + let transition_op_val_mut = env.transition_val.as_mut().unwrap(); + let transition_id_bytes = transition_op_val_mut.id().into_inner().into_inner(); + let message = SecpMessage::from_digest_slice(&transition_id_bytes).expect("32 bytes"); + let sig = secp.sign_ecdsa(&message, &secret_key); + transition_op_val_mut.signature = Some(Bytes64::from_array(sig.serialize_compact()).into()); + + let op_code = ContractOp::Vts(RegS::from(0)); + env.execute(op_code, false); + } + + #[test] + fn test_vts_fail_invalid_signature_format() { + let mut env = TestEnv::for_transition(); + let (_secret_key, public_key) = generate_keypair(&mut rand::thread_rng()); + let public_key_bytes_compact = public_key.serialize(); + env.regs + .set_s16(u4::with(0), ByteStr::with(&public_key_bytes_compact[..])); + let transition_op_val_mut = env.transition_val.as_mut().unwrap(); + transition_op_val_mut.signature = Some(Bytes64::from_array([0u8; 64]).into()); + + let op_code = ContractOp::Vts(RegS::from(0)); + env.execute(op_code, false); + } + + #[test] + fn test_vts_fail_signature_mismatch() { + let mut env = TestEnv::for_transition(); + let secp = Secp256k1::new(); + let (_sk1, pk1) = generate_keypair(&mut rand::thread_rng()); + let (sk2, _pk2) = generate_keypair(&mut rand::thread_rng()); + env.regs + .set_s16(u4::with(0), ByteStr::with(&pk1.serialize()[..])); + + let transition_op_val_mut = env.transition_val.as_mut().unwrap(); + let transition_id_bytes = transition_op_val_mut.id().into_inner().into_inner(); + let message = SecpMessage::from_digest_slice(&transition_id_bytes).expect("32 bytes"); + let sig = secp.sign_ecdsa(&message, &sk2); + transition_op_val_mut.signature = Some(Bytes64::from_array(sig.serialize_compact()).into()); + + let op_code = ContractOp::Vts(RegS::from(0)); + env.execute(op_code, false); + } + + #[test] + fn test_vts_fail_on_genesis() { + let mut env = TestEnv::for_genesis(); + let (_secret_key, public_key) = generate_keypair(&mut rand::thread_rng()); + let public_key_bytes_compact = public_key.serialize(); + env.regs + .set_s16(u4::with(0), ByteStr::with(&public_key_bytes_compact[..])); + + let op_code = ContractOp::Vts(RegS::from(0)); + env.execute(op_code, false); + } +} + +mod fail_op_test { + + use super::*; + + #[test] + fn test_fail_op_unknown_opcode() { + let mut env = TestEnv::for_genesis(); + let unknown_opcode = 0xFF; + let op_code = ContractOp::Fail(unknown_opcode, core::marker::PhantomData); + env.execute(op_code, false); + } +} +mod instruction_set_impl { + use std::collections::BTreeSet; + + use aluvm::isa::InstructionSet; + use aluvm::reg::{Reg, Reg32, RegA, RegS}; + + use super::*; + + #[test] + fn test_isa_ids() { + assert_eq!(ContractOp::::isa_ids(), IsaSeg::with("RGB")); + } + + #[test] + fn test_src_regs() { + let op_ldp = ContractOp::::LdP( + DUMMY_ASSIGN_TYPE_DATA, + Reg16::Reg0, + RegS::from(0), + ); + let expected_ldp = bset![Reg::A(RegA::A16, Reg32::Reg0)]; + assert_eq!(op_ldp.src_regs(), expected_ldp); + + let op_ldf = ContractOp::::LdF( + DUMMY_ASSIGN_TYPE_FUNGIBLE, + Reg16::Reg1, + Reg16::Reg2, + ); + let expected_ldf = bset![Reg::A(RegA::A16, Reg32::Reg1)]; + assert_eq!(op_ldf.src_regs(), expected_ldf); + + let op_ldg = + ContractOp::::LdG(DUMMY_GLOBAL_TYPE_A, Reg16::Reg3, RegS::from(1)); + let expected_ldg = bset![Reg::A(RegA::A8, Reg32::Reg3)]; + assert_eq!(op_ldg.src_regs(), expected_ldg); + + let op_ldc = + ContractOp::::LdC(DUMMY_GLOBAL_TYPE_A, Reg16::Reg4, RegS::from(2)); + let expected_ldc = bset![Reg::A(RegA::A32, Reg32::Reg4)]; + assert_eq!(op_ldc.src_regs(), expected_ldc); + + let ops_empty_src = vec![ + ContractOp::::CnP(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg32::Reg0), + ContractOp::::CnS(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg32::Reg0), + ContractOp::::CnG(DUMMY_GLOBAL_TYPE_A, Reg32::Reg0), + ContractOp::::CnC(DUMMY_GLOBAL_TYPE_A, Reg32::Reg0), + ContractOp::::LdM(DUMMY_META_TYPE_A, RegS::from(0)), + ContractOp::::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE), + ContractOp::::Vts(RegS::from(0)), + ContractOp::::Fail( + 0, + core::marker::PhantomData::, + ), + ]; + for op in ops_empty_src { + assert_eq!(op.src_regs(), BTreeSet::new(), "Failed for {:?}", op); + } + + let ops_a64_src = vec![ + ContractOp::::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE), + ContractOp::::Sps(DUMMY_ASSIGN_TYPE_FUNGIBLE), + ]; + let expected_a64_src = bset![Reg::A(RegA::A64, Reg32::Reg0)]; + for op in ops_a64_src { + assert_eq!(op.src_regs(), expected_a64_src, "Failed for {:?}", op); + } + } + + #[test] + fn test_dst_regs() { + // CnG + let op_cng = ContractOp::::CnG(DUMMY_GLOBAL_TYPE_A, Reg32::Reg5); + let expected_cng = bset![Reg::A(RegA::A8, Reg32::Reg5)]; + assert_eq!(op_cng.dst_regs(), expected_cng); + + // CnP, CnS, CnC + let ops_a16_dst = vec![ + ( + ContractOp::::CnP(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg32::Reg6), + Reg32::Reg6, + ), + ( + ContractOp::::CnS(DUMMY_ASSIGN_TYPE_DATA, Reg32::Reg7), + Reg32::Reg7, + ), + (ContractOp::::CnC(DUMMY_GLOBAL_TYPE_B, Reg32::Reg8), Reg32::Reg8), + ]; + for (op, reg_idx) in ops_a16_dst { + let expected = bset![Reg::A(RegA::A16, reg_idx)]; + assert_eq!(op.dst_regs(), expected, "Failed for {:?}", op); + } + + // LdF + let op_ldf = ContractOp::::LdF( + DUMMY_ASSIGN_TYPE_FUNGIBLE, + Reg16::Reg0, + Reg16::Reg9, + ); + // Reg16::Reg9 -> Reg32::Reg9 + let expected_ldf = bset![Reg::A(RegA::A64, Reg32::Reg9)]; + assert_eq!(op_ldf.dst_regs(), expected_ldf); + + // LdG, LdS, LdP, LdC, LdM + let ops_s_dst = vec![ + ( + ContractOp::::LdG( + DUMMY_GLOBAL_TYPE_A, + Reg16::Reg0, + RegS::from(10), + ), + RegS::from(10), + ), + ( + ContractOp::::LdS( + DUMMY_ASSIGN_TYPE_DATA, + Reg16::Reg0, + RegS::from(11), + ), + RegS::from(11), + ), + ( + ContractOp::::LdP( + DUMMY_ASSIGN_TYPE_RIGHTS, + Reg16::Reg0, + RegS::from(12), + ), + RegS::from(12), + ), + ( + ContractOp::::LdC( + DUMMY_GLOBAL_TYPE_B, + Reg16::Reg0, + RegS::from(13), + ), + RegS::from(13), + ), + ( + ContractOp::::LdM(DUMMY_META_TYPE_A, RegS::from(14)), + RegS::from(14), + ), + ]; + for (op, reg_s_idx) in ops_s_dst { + let expected = bset![Reg::S(reg_s_idx)]; + assert_eq!(op.dst_regs(), expected, "Failed for {:?}", op); + } + + let ops_empty_dst = vec![ + ContractOp::::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE), + ContractOp::::Sas(DUMMY_ASSIGN_TYPE_FUNGIBLE), + ContractOp::::Sps(DUMMY_ASSIGN_TYPE_FUNGIBLE), + ContractOp::::Fail( + 0, + core::marker::PhantomData::, + ), + ]; + for op in ops_empty_dst { + assert_eq!(op.dst_regs(), BTreeSet::new(), "Failed for {:?}", op); + } + + // Vts + let op_vts = ContractOp::::Vts(RegS::from(15)); + let expected_vts = bset![Reg::S(RegS::from(15))]; + assert_eq!(op_vts.dst_regs(), expected_vts); + } + + #[test] + fn test_complexity() { + // Cn* + assert_eq!( + ContractOp::CnP::(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg32::Reg0) + .complexity(), + 2 + ); + // Ld* (except LdM) + assert_eq!( + ContractOp::LdP::( + DUMMY_ASSIGN_TYPE_DATA, + Reg16::Reg0, + RegS::from(0) + ) + .complexity(), + 8 + ); + // LdM + assert_eq!( + ContractOp::LdM::(DUMMY_META_TYPE_A, RegS::from(0)).complexity(), + 6 + ); + // S*s + assert_eq!( + ContractOp::Svs::(DUMMY_ASSIGN_TYPE_FUNGIBLE).complexity(), + 20 + ); + // Vts + assert_eq!(ContractOp::Vts::(RegS::from(0)).complexity(), 512); + // Fail + assert_eq!( + ContractOp::Fail(0, core::marker::PhantomData::).complexity(), + u64::MAX + ); + + assert_eq!( + ContractOp::LdF::( + DUMMY_ASSIGN_TYPE_FUNGIBLE, + Reg16::Reg0, + Reg16::Reg0 + ) + .complexity(), + 8 + ); + assert_eq!( + ContractOp::LdS::( + DUMMY_ASSIGN_TYPE_DATA, + Reg16::Reg0, + RegS::from(0) + ) + .complexity(), + 8 + ); + assert_eq!( + ContractOp::LdG::(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(0)) + .complexity(), + 8 + ); + assert_eq!( + ContractOp::LdC::(DUMMY_GLOBAL_TYPE_A, Reg16::Reg0, RegS::from(0)) + .complexity(), + 8 + ); + + assert_eq!( + ContractOp::Sas::(DUMMY_ASSIGN_TYPE_FUNGIBLE).complexity(), + 20 + ); + assert_eq!( + ContractOp::Sps::(DUMMY_ASSIGN_TYPE_FUNGIBLE).complexity(), + 20 + ); + } +} + +mod bytecode_impl { + use std::fmt::Debug; + + use aluvm::isa::Bytecode; + use aluvm::library::{Cursor, LibSeg, Read}; + + use super::*; + use crate::vm::{ContractOp, ContractStateAccess}; + + const TEST_BUFFER_SIZE: usize = 1024; + + fn roundtrip_test_raw(op: ContractOp) + where ContractOp: Bytecode + PartialEq + Debug { + let libs = LibSeg::default(); + + let mut encoded_len_bytecode: u16; + let final_data_segment_content: ByteStr; + let mut actual_encoded_bytecode = [0u8; TEST_BUFFER_SIZE]; + + let instr_byte_expected = op.instr_byte(); + println!("Encoding op: {:?}, expected instr_byte: {:#04x}", op, instr_byte_expected); + { + let data_segment_for_encoding = ByteStr::default(); + let mut writer_cursor = + Cursor::with(&mut actual_encoded_bytecode[..], data_segment_for_encoding, &libs); + op.encode(&mut writer_cursor).expect("Failed to encode op"); + let final_byte_pos = writer_cursor.pos(); + let final_bit_pos = writer_cursor.offset().0; + + encoded_len_bytecode = + if final_bit_pos as u8 > 0 { final_byte_pos + 1 } else { final_byte_pos }; + + encoded_len_bytecode = encoded_len_bytecode.min(TEST_BUFFER_SIZE as u16); + + final_data_segment_content = writer_cursor.into_data_segment(); + + println!( + "Encoded bytecode ({} bytes): {:02x?}", + encoded_len_bytecode, + &actual_encoded_bytecode[..encoded_len_bytecode as usize] + ); + if encoded_len_bytecode > 0 { + println!("First encoded byte: {:#04x}", actual_encoded_bytecode[0]); + assert_eq!( + actual_encoded_bytecode[0], instr_byte_expected, + "Mismatch of first byte after encoding" + ); + } + println!("Encoded data segment len: {}", final_data_segment_content.len()); + } + + { + let mut reader_cursor = Cursor::with( + &actual_encoded_bytecode[..encoded_len_bytecode as usize], + final_data_segment_content.as_ref(), + &libs, + ); + + if encoded_len_bytecode > 0 { + let peeked_byte = reader_cursor + .peek_u8() + .expect("Failed to peek byte for decode"); + println!("Byte to be decoded by ContractOp::decode: {:#04x}", peeked_byte); + } + + let decoded_op = match ContractOp::::decode(&mut reader_cursor) { + Ok(d_op) => { + println!("Successfully decoded to: {:?}", d_op); + d_op + } + Err(e) => { + panic!("Failed to decode op: {:?}, error: {:?}", op, e); + } + }; + + assert_eq!(op, decoded_op, "Raw roundtrip failed for op: {:?}", op); + } + } + #[test] + fn test_instr_range() { + let range = ContractOp::::instr_range(); + assert_eq!(range, INSTR_CONTRACT_FROM..=INSTR_CONTRACT_TO); + } + + #[test] + fn test_instr_byte_and_roundtrip() { + // CnP + let op_cnp = ContractOp::::CnP(DUMMY_ASSIGN_TYPE_FUNGIBLE, Reg32::Reg0); + assert_eq!(op_cnp.instr_byte(), INSTR_CNP); + roundtrip_test_raw(op_cnp); + + // CnS + let op_cns = ContractOp::::CnS(DUMMY_ASSIGN_TYPE_DATA, Reg32::Reg1); + assert_eq!(op_cns.instr_byte(), INSTR_CNS); + roundtrip_test_raw(op_cns); + + // CnG + let op_cng = ContractOp::::CnG(DUMMY_GLOBAL_TYPE_A, Reg32::Reg2); + assert_eq!(op_cng.instr_byte(), INSTR_CNG); + roundtrip_test_raw(op_cng); + + // CnC + let op_cnc = ContractOp::::CnC(DUMMY_GLOBAL_TYPE_B, Reg32::Reg3); + assert_eq!(op_cnc.instr_byte(), INSTR_CNC); + roundtrip_test_raw(op_cnc); + + // LdP + let op_ldp = ContractOp::::LdP( + DUMMY_ASSIGN_TYPE_DATA, + Reg16::Reg0, + RegS::from(1), + ); + assert_eq!(op_ldp.instr_byte(), INSTR_LDP); + roundtrip_test_raw(op_ldp); + + // LdS + let op_lds = ContractOp::::LdS( + DUMMY_ASSIGN_TYPE_FUNGIBLE, + Reg16::Reg2, + RegS::from(3), + ); + assert_eq!(op_lds.instr_byte(), INSTR_LDS); + roundtrip_test_raw(op_lds); + + // LdF + let op_ldf = ContractOp::::LdF( + DUMMY_ASSIGN_TYPE_UNUSED, + Reg16::Reg4, + Reg16::Reg5, + ); + assert_eq!(op_ldf.instr_byte(), INSTR_LDF); + roundtrip_test_raw(op_ldf); + + // LdG + let op_ldg = + ContractOp::::LdG(DUMMY_GLOBAL_TYPE_A, Reg16::Reg6, RegS::from(7)); + assert_eq!(op_ldg.instr_byte(), INSTR_LDG); + roundtrip_test_raw(op_ldg); + + // LdC + let op_ldc = ContractOp::::LdC( + DUMMY_GLOBAL_TYPE_UNUSED, + Reg16::Reg8, + RegS::from(9), + ); + assert_eq!(op_ldc.instr_byte(), INSTR_LDC); + roundtrip_test_raw(op_ldc); + + // LdM + let op_ldm = ContractOp::::LdM(DUMMY_META_TYPE_A, RegS::from(10)); + assert_eq!(op_ldm.instr_byte(), INSTR_LDM); + roundtrip_test_raw(op_ldm); + + // Svs + let op_svs = ContractOp::::Svs(DUMMY_ASSIGN_TYPE_FUNGIBLE); + assert_eq!(op_svs.instr_byte(), INSTR_SVS); + roundtrip_test_raw(op_svs); + + // Sas + let op_sas = ContractOp::::Sas(DUMMY_ASSIGN_TYPE_DATA); + assert_eq!(op_sas.instr_byte(), INSTR_SAS); + roundtrip_test_raw(op_sas); + + // Sps + let op_sps = ContractOp::::Sps(DUMMY_ASSIGN_TYPE_UNUSED); + assert_eq!(op_sps.instr_byte(), INSTR_SPS); + roundtrip_test_raw(op_sps); + + // Vts + let op_vts = ContractOp::::Vts(RegS::from(11)); + assert_eq!(op_vts.instr_byte(), INSTR_VTS); + roundtrip_test_raw(op_vts); + + // Fail + let op_fail = ContractOp::Fail(0xF0, core::marker::PhantomData::); + assert_eq!(op_fail.instr_byte(), 0xF0); + roundtrip_test_raw(op_fail); + } +}