diff --git a/contrib/mir/src/ast.rs b/contrib/mir/src/ast.rs index 353bc5f7bb56c6c3743250063105048fa3e2da5f..4708b30ea4470ca24571ee0f4a1d5bbdbfdf4b74 100644 --- a/contrib/mir/src/ast.rs +++ b/contrib/mir/src/ast.rs @@ -477,9 +477,11 @@ pub enum Instruction<'a> { Amount, Nil, EmptySet, + EmptyBigMap(Type, Type), Mem(overloads::Mem), Get(overloads::Get), Update(overloads::Update), + GetAndUpdate(overloads::GetAndUpdate), Concat(overloads::Concat), Size(overloads::Size), Seq(Vec), diff --git a/contrib/mir/src/ast/big_map.rs b/contrib/mir/src/ast/big_map.rs index 59fe3c208cd5dc96fa6a74043dca224b664707d7..0d36be45d9c49174da71c99106250cbcaee8e8f5 100644 --- a/contrib/mir/src/ast/big_map.rs +++ b/contrib/mir/src/ast/big_map.rs @@ -56,7 +56,7 @@ impl<'a> BigMap<'a> { &self, arena: &'a Arena>, key: &TypedValue, - storage: &impl LazyStorage<'a>, + storage: &(impl LazyStorage<'a> + ?Sized), ) -> Result>, LazyStorageError> { Ok(match self.overlay.get(key) { // If the key is mentioned in the overlay, the associated value is @@ -74,7 +74,7 @@ impl<'a> BigMap<'a> { pub fn mem( &self, key: &TypedValue, - storage: &impl LazyStorage<'a>, + storage: &(impl LazyStorage<'a> + ?Sized), ) -> Result { Ok(match self.overlay.get(key) { // If the key is mentioned in the overlay, the associated value is @@ -196,7 +196,7 @@ pub trait LazyStorageBulkUpdate<'a>: LazyStorage<'a> { } } -impl<'a, T: LazyStorage<'a>> LazyStorageBulkUpdate<'a> for T {} +impl<'a, T: LazyStorage<'a> + ?Sized> LazyStorageBulkUpdate<'a> for T {} #[derive(Clone, PartialEq, Eq, Debug)] pub struct MapInfo<'a> { diff --git a/contrib/mir/src/ast/micheline.rs b/contrib/mir/src/ast/micheline.rs index f178c2a64ed8b1c29e98265fd70b3dd061e4a9dc..9bacd634ecaeb425eb3e4214402f5aa83a9bdafa 100644 --- a/contrib/mir/src/ast/micheline.rs +++ b/contrib/mir/src/ast/micheline.rs @@ -209,8 +209,6 @@ macro_rules! micheline_unsupported_instructions { | Prim::CREATE_CONTRACT | Prim::EMIT | Prim::EMPTY_MAP - | Prim::EMPTY_BIG_MAP - | Prim::GET_AND_UPDATE | Prim::MAP | Prim::SAPLING_EMPTY_STATE | Prim::SAPLING_VERIFY_UPDATE diff --git a/contrib/mir/src/ast/overloads.rs b/contrib/mir/src/ast/overloads.rs index 18f0098bfe95b43fccf48a993826b849f9d24553..20b09d6e6fdf2e73323f0785e1f220d24d9cc367 100644 --- a/contrib/mir/src/ast/overloads.rs +++ b/contrib/mir/src/ast/overloads.rs @@ -51,6 +51,7 @@ pub enum Not { pub enum Mem { Set, Map, + BigMap, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -82,12 +83,20 @@ pub enum Neg { #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Get { Map, + BigMap, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Update { Set, Map, + BigMap, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum GetAndUpdate { + Map, + BigMap, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] diff --git a/contrib/mir/src/gas.rs b/contrib/mir/src/gas.rs index c0ae7814bce8b5429ec89adc341770d7cb31c288..146451d55989a1a68f40eff7cb55764f8722d86e 100644 --- a/contrib/mir/src/gas.rs +++ b/contrib/mir/src/gas.rs @@ -296,6 +296,7 @@ pub mod interpret_cost { pub const SIZE_LIST: u32 = 10; pub const SIZE_SET: u32 = 10; pub const SIZE_MAP: u32 = 10; + pub const EMPTY_BIG_MAP: u32 = 300; pub const CHAIN_ID: u32 = 15; pub const PACK: u32 = 0; pub const SELF: u32 = 10; @@ -633,6 +634,22 @@ pub mod interpret_cost { (130 + 2 * lookup_cost).as_gas_cost() } + pub fn map_get_and_update(k: &TypedValue, map_size: usize) -> Result { + // NB: same considerations as for map_get + let compare_cost = compare(k, k)?; + let size_log = (Checked::from(map_size) + 1).ok_or(OutOfGas)?.log2i(); + let lookup_cost = Checked::from(compare_cost) * size_log; + // NB: 3 factor copied from Tezos protocol, in principle it should + // reflect update vs get overhead, but it seems like an overestimation, + // get_and_update should cost almost exactly the same as update, any + // observable difference would be in the constant term. + // + // However, note that this function is also reused for big_map version + // of GET_AND_UPDATE, wherein it's more justified. That is to say, take + // care when updating this. + (80 + 3 * lookup_cost).as_gas_cost() + } + /// Measures size of Michelson using several metrics. pub struct MichelineSize { /// Total number of nodes (including leaves). diff --git a/contrib/mir/src/interpreter.rs b/contrib/mir/src/interpreter.rs index 0c5f50454a29738b39f2bf26b3c3b33b50046dd6..ac8f214a5957e454e8b90fc68f0556de3b356617 100644 --- a/contrib/mir/src/interpreter.rs +++ b/contrib/mir/src/interpreter.rs @@ -12,6 +12,7 @@ use num_traits::{Signed, Zero}; use std::rc::Rc; use typed_arena::Arena; +use crate::ast::big_map::{BigMap, LazyStorageError}; use crate::ast::*; use crate::bls; use crate::context::Ctx; @@ -28,6 +29,8 @@ pub enum InterpretError<'a> { MutezOverflow, #[error("failed with: {1:?} of type {0:?}")] FailedWith(Type, TypedValue<'a>), + #[error("lazy storage error: {0}")] + LazyStorageError(#[from] LazyStorageError), } #[derive(Debug, PartialEq, Eq, thiserror::Error)] @@ -50,7 +53,7 @@ impl<'a> ContractScript<'a> { /// allows ensuring they satisfy the types expected by the script. pub fn interpret( &self, - ctx: &mut crate::context::Ctx, + ctx: &mut Ctx<'a>, arena: &'a Arena>, parameter: Micheline<'a>, storage: Micheline<'a>, @@ -86,7 +89,7 @@ impl<'a> Instruction<'a> { /// When the instruction can't be executed on the provided stack. pub fn interpret( &self, - ctx: &mut Ctx, + ctx: &mut Ctx<'a>, arena: &'a Arena>, stack: &mut IStack<'a>, ) -> Result<(), InterpretError<'a>> { @@ -96,7 +99,7 @@ impl<'a> Instruction<'a> { fn interpret<'a>( ast: &[Instruction<'a>], - ctx: &mut Ctx, + ctx: &mut Ctx<'a>, arena: &'a Arena>, stack: &mut IStack<'a>, ) -> Result<(), InterpretError<'a>> { @@ -116,7 +119,7 @@ fn unreachable_state() -> ! { fn interpret_one<'a>( i: &Instruction<'a>, - ctx: &mut Ctx, + ctx: &mut Ctx<'a>, arena: &'a Arena>, stack: &mut IStack<'a>, ) -> Result<(), InterpretError<'a>> { @@ -800,6 +803,16 @@ fn interpret_one<'a>( ctx.gas.consume(interpret_cost::EMPTY_SET)?; stack.push(V::Set(BTreeSet::new())) } + I::EmptyBigMap(kty, vty) => { + use std::collections::BTreeMap; + ctx.gas.consume(interpret_cost::EMPTY_BIG_MAP)?; + stack.push(V::BigMap(BigMap { + id: None, + overlay: BTreeMap::new(), + key_type: kty.clone(), + value_type: vty.clone(), + })) + } I::Mem(overload) => match overload { overloads::Mem::Set => { let key = pop!(); @@ -815,6 +828,15 @@ fn interpret_one<'a>( let result = map.contains_key(&key); stack.push(V::Bool(result)); } + overloads::Mem::BigMap => { + let key = pop!(); + let map = pop!(V::BigMap); + // the protocol deliberately uses map costs for the overlay + ctx.gas + .consume(interpret_cost::map_mem(&key, map.overlay.len())?)?; + let result = map.mem(&key, ctx.big_map_storage.as_ref())?; + stack.push(V::Bool(result)); + } }, I::Get(overload) => match overload { overloads::Get::Map => { @@ -824,6 +846,15 @@ fn interpret_one<'a>( let result = map.get(&key); stack.push(V::new_option(result.cloned())); } + overloads::Get::BigMap => { + let key = pop!(); + let map = pop!(V::BigMap); + // the protocol intentionally uses map costs for the overlay + ctx.gas + .consume(interpret_cost::map_get(&key, map.overlay.len())?)?; + let result = map.get(arena, &key, ctx.big_map_storage.as_ref())?; + stack.push(V::new_option(result)); + } }, I::Update(overload) => match overload { overloads::Update::Set => { @@ -849,6 +880,40 @@ fn interpret_one<'a>( Some(val) => map.insert(key, *val), }; } + overloads::Update::BigMap => { + let key = pop!(); + let opt_new_val = pop!(V::Option); + let map = irrefutable_match!(&mut stack[0]; V::BigMap); + // the protocol intentionally uses map costs for the overlay + ctx.gas + .consume(interpret_cost::map_update(&key, map.overlay.len())?)?; + map.update(key, opt_new_val.map(|x| *x)); + } + }, + I::GetAndUpdate(overload) => match overload { + overloads::GetAndUpdate::Map => { + let key = pop!(); + let opt_new_val = pop!(V::Option); + let map = irrefutable_match!(&mut stack[0]; V::Map); + ctx.gas + .consume(interpret_cost::map_get_and_update(&key, map.len())?)?; + let opt_old_val = match opt_new_val { + None => map.remove(&key), + Some(val) => map.insert(key, *val), + }; + stack.push(V::new_option(opt_old_val)); + } + overloads::GetAndUpdate::BigMap => { + let key = pop!(); + let opt_new_val = pop!(V::Option); + let map = irrefutable_match!(&mut stack[0]; V::BigMap); + // the protocol intentionally uses map costs for the overlay + ctx.gas + .consume(interpret_cost::map_get_and_update(&key, map.overlay.len())?)?; + let opt_old_val = map.get(arena, &key, ctx.big_map_storage.as_ref())?; + map.update(key, opt_new_val.map(|x| *x)); + stack.push(V::new_option(opt_old_val)); + } }, I::Size(overload) => { macro_rules! run_size { @@ -1224,6 +1289,7 @@ mod interpreter_tests { use super::*; use super::{Lambda, Or}; + use crate::ast::big_map::{InMemoryLazyStorage, LazyStorageBulkUpdate}; use crate::ast::michelson_address as addr; use crate::bls; use crate::gas::Gas; @@ -1239,7 +1305,7 @@ mod interpreter_tests { fn interpret<'a>( ast: &[Instruction<'a>], - ctx: &mut Ctx, + ctx: &mut Ctx<'a>, stack: &mut IStack<'a>, ) -> Result<(), InterpretError<'a>> { let temp = Box::leak(Box::default()); @@ -1248,7 +1314,7 @@ mod interpreter_tests { fn interpret_one<'a>( i: &Instruction<'a>, - ctx: &mut Ctx, + ctx: &mut Ctx<'a>, stack: &mut IStack<'a>, ) -> Result<(), InterpretError<'a>> { let temp = Box::leak(Box::default()); @@ -2501,6 +2567,49 @@ mod interpreter_tests { ); } + #[test] + fn get_big_map() { + // overlay semantics are tested in big_map module, here we only check + // the interpreter works + let mut ctx = Ctx::default(); + ctx.big_map_storage = Box::new(InMemoryLazyStorage::new()); + let big_map_id = ctx + .big_map_storage + .big_map_new(&Type::Int, &Type::String) + .unwrap(); + ctx.big_map_storage + .big_map_update( + &big_map_id, + TypedValue::int(1), + Some(TypedValue::String("foo".to_owned())), + ) + .unwrap(); + let big_map = BigMap { + id: Some(big_map_id), + overlay: BTreeMap::from([( + TypedValue::int(2), + Some(TypedValue::String("bar".to_owned())), + )]), + key_type: Type::Int, + value_type: Type::String, + }; + let mut stack = stk![TypedValue::BigMap(big_map), TypedValue::int(1)]; + assert_eq!( + interpret_one(&Get(overloads::Get::BigMap), &mut ctx, &mut stack), + Ok(()) + ); + assert_eq!( + stack, + stk![TypedValue::new_option(Some(TypedValue::String( + "foo".to_owned() + )))] + ); + assert_eq!( + ctx.gas.milligas(), + Gas::default().milligas() - interpret_cost::map_get(&TypedValue::int(1), 1).unwrap() + ); + } + #[test] fn get_map_none() { let mut ctx = Ctx::default(); @@ -2543,6 +2652,46 @@ mod interpreter_tests { ); } + #[test] + fn mem_big_map() { + let mut ctx = Ctx::default(); + let big_map_id = ctx + .big_map_storage + .big_map_new(&Type::Int, &Type::String) + .unwrap(); + ctx.big_map_storage + .big_map_bulk_update( + &big_map_id, + [ + ( + TypedValue::int(1), + Some(TypedValue::String("foo".to_owned())), + ), + ( + TypedValue::int(2), + Some(TypedValue::String("bar".to_owned())), + ), + ], + ) + .unwrap(); + let big_map = BigMap { + id: Some(big_map_id), + overlay: BTreeMap::new(), + key_type: Type::Int, + value_type: Type::String, + }; + let mut stack = stk![TypedValue::BigMap(big_map), TypedValue::int(1)]; + assert_eq!( + interpret_one(&Mem(overloads::Mem::BigMap), &mut ctx, &mut stack), + Ok(()) + ); + assert_eq!(stack, stk![TypedValue::Bool(true)]); + assert_eq!( + ctx.gas.milligas(), + Gas::default().milligas() - interpret_cost::map_mem(&TypedValue::int(1), 0).unwrap() + ); + } + #[test] fn mem_map_absent() { let mut ctx = Ctx::default(); @@ -2600,6 +2749,29 @@ mod interpreter_tests { ); } + #[test] + fn empty_big_map() { + let mut ctx = Ctx::default(); + let mut stack = stk![]; + assert_eq!( + interpret_one(&EmptyBigMap(Type::Int, Type::Unit), &mut ctx, &mut stack), + Ok(()) + ); + assert_eq!( + stack, + stk![TypedValue::BigMap(BigMap { + id: None, + overlay: BTreeMap::new(), + key_type: Type::Int, + value_type: Type::Unit, + })] + ); + assert_eq!( + ctx.gas.milligas(), + Gas::default().milligas() - interpret_cost::EMPTY_BIG_MAP + ); + } + #[test] fn update_set_insert() { let mut ctx = Ctx::default(); @@ -2833,6 +3005,179 @@ mod interpreter_tests { assert_eq!(stack, stk![TypedValue::nat(2)]); } + #[test] + fn update_big_map() { + fn check<'a>( + content: impl IntoIterator, Option>)>, + overlay: impl IntoIterator, Option>)>, + new_value: Option>, + result: impl IntoIterator, Option>)>, + ) { + let mut ctx = Ctx::default(); + let id = ctx + .big_map_storage + .big_map_new(&Type::Int, &Type::String) + .unwrap(); + ctx.big_map_storage + .big_map_bulk_update(&id, content) + .unwrap(); + let big_map = BigMap { + id: Some(id.clone()), + overlay: overlay.into_iter().collect(), + key_type: Type::Int, + value_type: Type::String, + }; + let mut stack = stk![ + TypedValue::BigMap(big_map), + TypedValue::new_option(new_value), + TypedValue::int(1) + ]; + assert_eq!( + interpret_one(&Update(overloads::Update::BigMap), &mut ctx, &mut stack), + Ok(()) + ); + assert_eq!( + stack, + stk![TypedValue::BigMap(BigMap { + id: Some(id), + overlay: result.into_iter().collect(), + key_type: Type::Int, + value_type: Type::String, + })] + ); + assert!(ctx.gas.milligas() < Gas::default().milligas()); + } + + // insert + check( + [], + [], + Some(TypedValue::String("foo".into())), + [(TypedValue::int(1), Some(TypedValue::String("foo".into())))], + ); + // update in content + check( + [(TypedValue::int(1), Some(TypedValue::String("bar".into())))], + [], + Some(TypedValue::String("foo".into())), + [(TypedValue::int(1), Some(TypedValue::String("foo".into())))], + ); + // update in overlay + check( + [], + [(TypedValue::int(1), Some(TypedValue::String("bar".into())))], + Some(TypedValue::String("foo".into())), + [(TypedValue::int(1), Some(TypedValue::String("foo".into())))], + ); + // remove in content + check( + [(TypedValue::int(1), Some(TypedValue::String("bar".into())))], + [], + None, + [(TypedValue::int(1), None)], + ); + // remove in overlay + check( + [], + [(TypedValue::int(1), Some(TypedValue::String("bar".into())))], + None, + [(TypedValue::int(1), None)], + ); + } + + #[test] + fn get_and_update_big_map() { + fn check<'a>( + content: impl IntoIterator, Option>)>, + overlay: impl IntoIterator, Option>)>, + old_value: Option>, + new_value: Option>, + result: impl IntoIterator, Option>)>, + ) { + let mut ctx = Ctx::default(); + let id = ctx + .big_map_storage + .big_map_new(&Type::Int, &Type::String) + .unwrap(); + ctx.big_map_storage + .big_map_bulk_update(&id, content) + .unwrap(); + let big_map = BigMap { + id: Some(id.clone()), + overlay: overlay.into_iter().collect(), + key_type: Type::Int, + value_type: Type::String, + }; + let mut stack = stk![ + TypedValue::BigMap(big_map), + TypedValue::new_option(new_value), + TypedValue::int(1) + ]; + assert_eq!( + interpret_one( + &GetAndUpdate(overloads::GetAndUpdate::BigMap), + &mut ctx, + &mut stack + ), + Ok(()) + ); + assert_eq!( + stack, + stk![ + TypedValue::BigMap(BigMap { + id: Some(id), + overlay: result.into_iter().collect(), + key_type: Type::Int, + value_type: Type::String, + }), + TypedValue::new_option(old_value), + ] + ); + assert!(ctx.gas.milligas() < Gas::default().milligas()); + } + + // insert + check( + [], + [], + None, + Some(TypedValue::String("foo".into())), + [(TypedValue::int(1), Some(TypedValue::String("foo".into())))], + ); + // update in content + check( + [(TypedValue::int(1), Some(TypedValue::String("bar".into())))], + [], + Some(TypedValue::String("bar".into())), + Some(TypedValue::String("foo".into())), + [(TypedValue::int(1), Some(TypedValue::String("foo".into())))], + ); + // update in overlay + check( + [], + [(TypedValue::int(1), Some(TypedValue::String("bar".into())))], + Some(TypedValue::String("bar".into())), + Some(TypedValue::String("foo".into())), + [(TypedValue::int(1), Some(TypedValue::String("foo".into())))], + ); + // remove in content + check( + [(TypedValue::int(1), Some(TypedValue::String("bar".into())))], + [], + Some(TypedValue::String("bar".into())), + None, + [(TypedValue::int(1), None)], + ); + // remove in overlay + check( + [], + [(TypedValue::int(1), Some(TypedValue::String("bar".into())))], + Some(TypedValue::String("bar".into())), + None, + [(TypedValue::int(1), None)], + ); + } + #[test] fn seq() { let mut stack = stk![V::int(1), V::nat(2)]; diff --git a/contrib/mir/src/lib.rs b/contrib/mir/src/lib.rs index 4e0b88f333282db730740912ab04a968c60c72f4..ae2c47287666319a085a5054e60633dd2ca6cb01 100644 --- a/contrib/mir/src/lib.rs +++ b/contrib/mir/src/lib.rs @@ -33,7 +33,7 @@ mod tests { use crate::stack::{stk, tc_stk}; use crate::typechecker; - fn report_gas R>(ctx: &mut Ctx, f: F) -> R { + fn report_gas<'a, R, F: FnOnce(&mut Ctx<'a>) -> R>(ctx: &mut Ctx<'a>, f: F) -> R { let initial_milligas = ctx.gas.milligas(); let r = f(ctx); let gas_diff = initial_milligas - ctx.gas.milligas(); @@ -58,10 +58,10 @@ mod tests { #[test] fn interpret_mutez_push_add() { let ast = parse("{ PUSH mutez 100; PUSH mutez 500; ADD }").unwrap(); + let temp = Arena::new(); let mut ctx = Ctx::default(); let ast = ast.typecheck_instruction(&mut ctx, None, &[]).unwrap(); let mut istack = stk![]; - let temp = Arena::new(); assert!(ast.interpret(&mut ctx, &temp, &mut istack).is_ok()); assert_eq!(istack, stk![TypedValue::Mutez(600)]); } @@ -73,8 +73,8 @@ mod tests { .typecheck_instruction(&mut Ctx::default(), None, &[app!(nat)]) .unwrap(); let mut istack = stk![TypedValue::nat(5)]; - let mut ctx = Ctx::default(); let temp = Arena::new(); + let mut ctx = Ctx::default(); report_gas(&mut ctx, |ctx| { assert!(ast.interpret(ctx, &temp, &mut istack).is_ok()); }); @@ -88,9 +88,9 @@ mod tests { .typecheck_instruction(&mut Ctx::default(), None, &[app!(nat)]) .unwrap(); let mut istack = stk![TypedValue::nat(5)]; + let temp = Arena::new(); let ctx = &mut Ctx::default(); ctx.gas = Gas::new(1); - let temp = Arena::new(); assert_eq!( ast.interpret(ctx, &temp, &mut istack), Err(interpreter::InterpretError::OutOfGas(crate::gas::OutOfGas)), @@ -227,19 +227,18 @@ mod tests { #[test] fn vote_contract() { + let arena = typed_arena::Arena::new(); let ctx = &mut Ctx::default(); ctx.amount = 5_000_000; - let arena = typed_arena::Arena::new(); use crate::lexer::Prim; use Micheline as M; - let temp = Arena::new(); let interp_res = parse_contract_script(VOTE_SRC) .unwrap() .typecheck_script(ctx) .unwrap() .interpret( ctx, - &temp, + &arena, "foo".into(), M::seq( &arena, @@ -264,19 +263,16 @@ mod tests { use std::collections::HashMap; #[track_caller] - fn run_e2e_test( - instr: &str, + fn run_e2e_test<'a>( + arena: &'a Arena>, + instr: &'a str, input_type_stack: TypeStack, output_type_stack: TypeStack, - input_stack: Stack, - output_stack: Stack, - mut ctx: Ctx, + mut input_stack: Stack>, + output_stack: Stack>, + mut ctx: Ctx<'a>, ) { - let instr = instr.to_owned(); - let temp = Arena::new(); - // NB: required to appease the borrow checker - let mut input_stack = input_stack; - let ast = parse(&instr).unwrap(); + let ast = parse(instr).unwrap(); let mut input_failing_type_stack = FailingTypeStack::Ok(input_type_stack); let ast = typecheck_instruction(&ast, &mut ctx, None, &mut input_failing_type_stack).unwrap(); @@ -284,7 +280,7 @@ mod tests { input_failing_type_stack, FailingTypeStack::Ok(output_type_stack) ); - assert!(ast.interpret(&mut ctx, &temp, &mut input_stack).is_ok()); + assert!(ast.interpret(&mut ctx, arena, &mut input_stack).is_ok()); assert_eq!(input_stack, output_stack); } @@ -292,6 +288,7 @@ mod tests { fn ticket_instr() { let ctx = Ctx::default(); run_e2e_test( + &Arena::new(), "TICKET", stk![Type::Nat, Type::Int], stk![Type::new_option(Type::new_ticket(Type::Int))], @@ -321,6 +318,7 @@ mod tests { content: TypedValue::int(20), }; run_e2e_test( + &Arena::new(), "READ_TICKET", stk![Type::new_ticket(Type::Int)], stk![ @@ -348,6 +346,7 @@ mod tests { content: TypedValue::int(20), }; run_e2e_test( + &Arena::new(), "SPLIT_TICKET", stk![ Type::new_pair(Type::Nat, Type::Nat), @@ -384,6 +383,7 @@ mod tests { content: TypedValue::int(20), }; run_e2e_test( + &Arena::new(), "JOIN_TICKETS", stk![Type::new_pair( Type::new_ticket(Type::Int), @@ -408,6 +408,7 @@ mod tests { #[test] fn balance() { run_e2e_test( + &Arena::new(), "BALANCE", stk![], stk![Type::Mutez], @@ -426,6 +427,7 @@ mod tests { let addr = Address::try_from("KT1BRd2ka5q2cPRdXALtXD1QZ38CPam2j1ye").unwrap(); // When contract for the address does not exist. run_e2e_test( + &Arena::new(), "CONTRACT int", stk![Type::Address], stk![Type::new_option(Type::new_contract(Type::Int))], @@ -441,6 +443,7 @@ mod tests { let addr = Address::try_from("tz3McZuemh7PCYG2P57n5mN8ecz56jCfSBR6").unwrap(); // When contract is implicit run_e2e_test( + &Arena::new(), "CONTRACT unit", stk![Type::Address], stk![Type::new_option(Type::new_contract(Type::Unit))], @@ -456,6 +459,7 @@ mod tests { let addr = Address::try_from("tz3McZuemh7PCYG2P57n5mN8ecz56jCfSBR6").unwrap(); // When contract is implicit and contract type is Ticket run_e2e_test( + &Arena::new(), "CONTRACT (ticket unit)", stk![Type::Address], stk![Type::new_option(Type::new_contract(Type::new_ticket( @@ -473,6 +477,7 @@ mod tests { let addr = Address::try_from("tz3McZuemh7PCYG2P57n5mN8ecz56jCfSBR6").unwrap(); // When contract is implicit and contract type is some other type run_e2e_test( + &Arena::new(), "CONTRACT int", stk![Type::Address], stk![Type::new_option(Type::new_contract(Type::Int))], @@ -487,6 +492,7 @@ mod tests { // When contract for the address does exist and is of expected type. run_e2e_test( + &Arena::new(), "CONTRACT unit", stk![Type::Address], stk![Type::new_option(Type::new_contract(Type::Unit))], @@ -511,6 +517,7 @@ mod tests { // When the address has an entrypoint. let addr = Address::try_from("KT1BRd2ka5q2cPRdXALtXD1QZ38CPam2j1ye%foo").unwrap(); run_e2e_test( + &Arena::new(), "CONTRACT unit", stk![Type::Address], stk![Type::new_option(Type::new_contract(Type::Unit))], @@ -535,6 +542,7 @@ mod tests { // When the instruction has an entrypoint. let addr = Address::try_from("KT1BRd2ka5q2cPRdXALtXD1QZ38CPam2j1ye").unwrap(); run_e2e_test( + &Arena::new(), "CONTRACT %foo unit", stk![Type::Address], stk![Type::new_option(Type::new_contract(Type::Unit))], @@ -562,6 +570,7 @@ mod tests { // When the instruction has an entrypoint and address has an entrypoint. let addr = Address::try_from("KT1BRd2ka5q2cPRdXALtXD1QZ38CPam2j1ye%bar").unwrap(); run_e2e_test( + &Arena::new(), "CONTRACT %foo unit", stk![Type::Address], stk![Type::new_option(Type::new_contract(Type::Unit))], @@ -588,6 +597,7 @@ mod tests { #[test] fn level() { run_e2e_test( + &Arena::new(), "LEVEL", stk![], stk![Type::Nat], @@ -604,6 +614,7 @@ mod tests { #[test] fn min_block_time() { run_e2e_test( + &Arena::new(), "MIN_BLOCK_TIME", stk![], stk![Type::Nat], @@ -621,6 +632,7 @@ mod tests { fn self_address() { let addr = Address::try_from("KT1BRd2ka5q2cPRdXALtXD1QZ38CPam2j1ye").unwrap(); run_e2e_test( + &Arena::new(), "SELF_ADDRESS", stk![], stk![Type::Address], @@ -638,6 +650,7 @@ mod tests { fn sender() { let addr = Address::try_from("KT1BRd2ka5q2cPRdXALtXD1QZ38CPam2j1ye").unwrap(); run_e2e_test( + &Arena::new(), "SENDER", stk![], stk![Type::Address], @@ -655,6 +668,7 @@ mod tests { fn source() { let addr = Address::try_from("tz1TSbthBCECxmnABv73icw7yyyvUWFLAoSP").unwrap(); run_e2e_test( + &Arena::new(), "SOURCE", stk![], stk![Type::Address], @@ -671,6 +685,7 @@ mod tests { #[test] fn timestamp_type_and_value() { run_e2e_test( + &Arena::new(), "PUSH timestamp 1571659294", stk![], stk![Type::Timestamp], @@ -679,6 +694,7 @@ mod tests { Ctx::default(), ); run_e2e_test( + &Arena::new(), "PUSH timestamp \"2019-10-21T12:01:34Z\"", stk![], stk![Type::Timestamp], @@ -691,6 +707,7 @@ mod tests { #[test] fn now() { run_e2e_test( + &Arena::new(), "NOW", stk![], stk![Type::Timestamp], @@ -708,6 +725,7 @@ mod tests { fn implicit_account() { let key_hash = KeyHash::try_from("tz3d9na7gPpt5jxdjGBFzoGQigcStHB8w1uq").unwrap(); run_e2e_test( + &Arena::new(), "IMPLICIT_ACCOUNT", stk![Type::KeyHash], stk![Type::new_contract(Type::Unit)], @@ -725,6 +743,7 @@ mod tests { let key_hash_2 = KeyHash::try_from("tz4T8ydHwYeoLHmLNcECYVq3WkMaeVhZ81h7").unwrap(); let key_hash_3 = KeyHash::try_from("tz3hpojUX9dYL5KLusv42SCBiggB77a2QLGx").unwrap(); run_e2e_test( + &Arena::new(), "VOTING_POWER", stk![Type::KeyHash], stk![Type::Nat], @@ -741,6 +760,7 @@ mod tests { ); run_e2e_test( + &Arena::new(), "VOTING_POWER", stk![Type::KeyHash], stk![Type::Nat], @@ -759,6 +779,7 @@ mod tests { let key_hash_1 = KeyHash::try_from("tz3d9na7gPpt5jxdjGBFzoGQigcStHB8w1uq").unwrap(); let key_hash_2 = KeyHash::try_from("tz4T8ydHwYeoLHmLNcECYVq3WkMaeVhZ81h7").unwrap(); run_e2e_test( + &Arena::new(), "TOTAL_VOTING_POWER", stk![], stk![Type::Nat], @@ -775,6 +796,7 @@ mod tests { #[test] fn dig() { run_e2e_test( + &Arena::new(), "DIG 3", stk![Type::Unit, Type::Nat, Type::Int, Type::String], stk![Type::Nat, Type::Int, Type::String, Type::Unit], @@ -797,6 +819,7 @@ mod tests { #[test] fn dug() { run_e2e_test( + &Arena::new(), "DUG 2", stk![ Type::Unit, @@ -911,7 +934,7 @@ mod multisig_tests { $ CHAIN_ID='0xf3d48554' $ ANTI_REPLAY_COUNTER='111' */ - fn make_ctx() -> Ctx<'static> { + fn make_ctx<'a>() -> Ctx<'a> { let mut ctx = Ctx::default(); ctx.self_address = "KT1BFATQpdP5xJGErJyk2vfL46dvFanWz87H".try_into().unwrap(); ctx.chain_id = tezos_crypto_rs::hash::ChainId(hex::decode("f3d48554").unwrap()); @@ -953,6 +976,7 @@ mod multisig_tests { #[test] fn multisig_transfer() { + let temp = Arena::new(); let mut ctx = make_ctx(); let threshold = BigUint::from(1u32); @@ -972,7 +996,6 @@ mod multisig_tests { let transfer_amount = 123; let transfer_destination = "tz1WrbkDrzKVqcGXkjw4Qk4fXkjXpAJuNP1j"; let signature = "edsigu1GCyS754UrkFLng9P5vG5T51Hs8TcgZoV7fPfj5qeXYzC1JKuUYzyowpfGghEEqUyPxpUdU7WRFrdxad5pnspQg9hwk6v"; - let temp = Arena::new(); let interp_res = parse_contract_script(MULTISIG_SRC) .unwrap() @@ -1026,6 +1049,7 @@ mod multisig_tests { #[test] fn multisig_set_delegate() { + let temp = Arena::new(); let mut ctx = make_ctx(); let threshold = BigUint::from(1u32); @@ -1044,7 +1068,6 @@ mod multisig_tests { */ let new_delegate = "tz1V8fDHpHzN8RrZqiYCHaJM9EocsYZch5Cy"; let signature = "edsigtXyZmxgR3MDhDRdtAtopHNNE8rPsPRHgPXurkMacmRLvbLyBCTjtBFNFYHEcLTjx94jdvUf81Wd7uybJNGn5phJYaPAJST"; - let temp = Arena::new(); let interp_res = parse_contract_script(MULTISIG_SRC) .unwrap() @@ -1095,11 +1118,11 @@ mod multisig_tests { #[test] fn invalid_signature() { + let temp = Arena::new(); let mut ctx = make_ctx(); let threshold = 1; let new_delegate = "tz1V8fDHpHzN8RrZqiYCHaJM9EocsYZch5Cy"; let invalid_signature = "edsigtt6SusfFFqwKqJNDuZMbhP6Q8f6zu3c3q7W6vPbjYKpv84H3hfXhRyRvAXHzNYSwBNNqjmf5taXKd2ZW3Rbix78bhWjxg5"; - let temp = Arena::new(); let interp_res = parse_contract_script(MULTISIG_SRC) .unwrap() diff --git a/contrib/mir/src/typechecker.rs b/contrib/mir/src/typechecker.rs index 6d114a77175e37da86a8fc6fff5fe49db1a7a8d0..84027420ebd4db6b8549a5cee11b550888a9dcb1 100644 --- a/contrib/mir/src/typechecker.rs +++ b/contrib/mir/src/typechecker.rs @@ -1301,6 +1301,16 @@ pub(crate) fn typecheck_instruction<'a>( } (App(EMPTY_SET, expect_args!(1), _), _) => unexpected_micheline!(), + (App(EMPTY_BIG_MAP, [kty, vty], _), _) => { + let kty = parse_ty(ctx, kty)?; + kty.ensure_prop(&mut ctx.gas, TypeProperty::Comparable)?; + let vty = parse_ty(ctx, vty)?; + vty.ensure_prop(&mut ctx.gas, TypeProperty::BigMapValue)?; + stack.push(T::new_big_map(kty.clone(), vty.clone())); + I::EmptyBigMap(kty, vty) + } + (App(EMPTY_BIG_MAP, expect_args!(2), _), _) => unexpected_micheline!(), + (App(MEM, [], _), [.., T::Set(..), _]) => { let ty_ = pop!(); let ty = pop!(T::Set); @@ -1311,10 +1321,17 @@ pub(crate) fn typecheck_instruction<'a>( (App(MEM, [], _), [.., T::Map(..), _]) => { let kty_ = pop!(); let map_tys = pop!(T::Map); - ensure_ty_eq(&mut ctx.gas, &map_tys.as_ref().0, &kty_)?; + ensure_ty_eq(&mut ctx.gas, &map_tys.0, &kty_)?; stack.push(T::Bool); I::Mem(overloads::Mem::Map) } + (App(MEM, [], _), [.., T::BigMap(..), _]) => { + let kty_ = pop!(); + let map_tys = pop!(T::BigMap); + ensure_ty_eq(&mut ctx.gas, &map_tys.0, &kty_)?; + stack.push(T::Bool); + I::Mem(overloads::Mem::BigMap) + } (App(MEM, [], _), [.., _, _]) => no_overload!(MEM), (App(MEM, [], _), [] | [_]) => no_overload!(MEM, len 2), (App(MEM, expect_args!(0), _), _) => unexpected_micheline!(), @@ -1326,6 +1343,13 @@ pub(crate) fn typecheck_instruction<'a>( stack.push(T::new_option(map_tys.1.clone())); I::Get(overloads::Get::Map) } + (App(GET, [], _), [.., T::BigMap(..), _]) => { + let kty_ = pop!(); + let map_tys = pop!(T::BigMap); + ensure_ty_eq(&mut ctx.gas, &map_tys.0, &kty_)?; + stack.push(T::new_option(map_tys.1.clone())); + I::Get(overloads::Get::BigMap) + } (App(GET, [], _), [.., _, _]) => no_overload!(GET), (App(GET, [], _), [] | [_]) => no_overload!(GET, len 2), (App(GET, expect_args!(0), _), _) => unexpected_micheline!(), @@ -1342,10 +1366,35 @@ pub(crate) fn typecheck_instruction<'a>( stack.drop_top(2); I::Update(overloads::Update::Map) } + (App(UPDATE, [], _), [.., T::BigMap(m), T::Option(vty_new), kty_]) => { + let (kty, vty) = m.as_ref(); + ensure_ty_eq(&mut ctx.gas, kty, kty_)?; + ensure_ty_eq(&mut ctx.gas, vty, vty_new)?; + stack.drop_top(2); + I::Update(overloads::Update::BigMap) + } (App(UPDATE, [], _), [.., _, _, _]) => no_overload!(UPDATE), (App(UPDATE, [], _), [] | [_] | [_, _]) => no_overload!(UPDATE, len 3), (App(UPDATE, expect_args!(0), _), _) => unexpected_micheline!(), + (App(GET_AND_UPDATE, [], _), [.., T::Map(m), T::Option(vty_new), kty_]) => { + let (kty, vty) = m.as_ref(); + ensure_ty_eq(&mut ctx.gas, kty, kty_)?; + ensure_ty_eq(&mut ctx.gas, vty, vty_new)?; + pop!(); + I::GetAndUpdate(overloads::GetAndUpdate::Map) + } + (App(GET_AND_UPDATE, [], _), [.., T::BigMap(m), T::Option(vty_new), kty_]) => { + let (kty, vty) = m.as_ref(); + ensure_ty_eq(&mut ctx.gas, kty, kty_)?; + ensure_ty_eq(&mut ctx.gas, vty, vty_new)?; + pop!(); + I::GetAndUpdate(overloads::GetAndUpdate::BigMap) + } + (App(GET_AND_UPDATE, [], _), [.., _, _, _]) => no_overload!(GET_AND_UPDATE), + (App(GET_AND_UPDATE, [], _), [] | [_] | [_, _]) => no_overload!(GET_AND_UPDATE, len 3), + (App(GET_AND_UPDATE, expect_args!(0), _), _) => unexpected_micheline!(), + (App(SIZE, [], _), [.., T::String]) => { stack[0] = T::Nat; I::Size(overloads::Size::String) @@ -3971,6 +4020,20 @@ mod typecheck_tests { assert_eq!(stack, tc_stk![Type::new_set(Type::Int)]); } + #[test] + fn empty_big_map() { + let mut stack = tc_stk![]; + assert_eq!( + typecheck_instruction( + &parse("EMPTY_BIG_MAP int unit").unwrap(), + &mut Ctx::default(), + &mut stack + ), + Ok(EmptyBigMap(Type::Int, Type::Unit)) + ); + assert_eq!(stack, tc_stk![Type::new_big_map(Type::Int, Type::Unit)]); + } + #[test] fn empty_set_incomparable() { let mut stack = tc_stk![]; @@ -3997,6 +4060,16 @@ mod typecheck_tests { assert_eq!(stack, tc_stk![Type::new_option(Type::String)]); } + #[test] + fn get_big_map() { + let mut stack = tc_stk![Type::new_big_map(Type::Int, Type::String), Type::Int]; + assert_eq!( + typecheck_instruction(&parse("GET").unwrap(), &mut Ctx::default(), &mut stack), + Ok(Get(overloads::Get::BigMap)) + ); + assert_eq!(stack, tc_stk![Type::new_option(Type::String)]); + } + #[test] fn get_map_incomparable() { assert_eq!( @@ -4034,6 +4107,16 @@ mod typecheck_tests { assert_eq!(stack, tc_stk![Type::Bool]); } + #[test] + fn mem_big_map() { + let mut stack = tc_stk![Type::new_big_map(Type::Int, Type::String), Type::Int]; + assert_eq!( + typecheck_instruction(&parse("MEM").unwrap(), &mut Ctx::default(), &mut stack), + Ok(Mem(overloads::Mem::BigMap)) + ); + assert_eq!(stack, tc_stk![Type::Bool]); + } + #[test] fn mem_map_incomparable() { assert_eq!( @@ -4132,6 +4215,20 @@ mod typecheck_tests { assert_eq!(stack, tc_stk![Type::new_map(Type::Int, Type::String)]); } + #[test] + fn update_big_map() { + let mut stack = tc_stk![ + Type::new_big_map(Type::Int, Type::String), + Type::new_option(Type::String), + Type::Int + ]; + assert_eq!( + typecheck_instruction(&parse("UPDATE").unwrap(), &mut Ctx::default(), &mut stack), + Ok(Update(overloads::Update::BigMap)) + ); + assert_eq!(stack, tc_stk![Type::new_big_map(Type::Int, Type::String)]); + } + #[test] fn update_map_wrong_ty() { let mut stack = tc_stk![ @@ -4164,6 +4261,95 @@ mod typecheck_tests { ); } + #[test] + fn get_and_update_map() { + let mut stack = tc_stk![ + Type::new_map(Type::Int, Type::String), + Type::new_option(Type::String), + Type::Int + ]; + assert_eq!( + typecheck_instruction( + &parse("GET_AND_UPDATE").unwrap(), + &mut Ctx::default(), + &mut stack + ), + Ok(GetAndUpdate(overloads::GetAndUpdate::Map)) + ); + assert_eq!( + stack, + tc_stk![ + Type::new_map(Type::Int, Type::String), + Type::new_option(Type::String) + ] + ); + } + + #[test] + fn get_and_update_big_map() { + let mut stack = tc_stk![ + Type::new_big_map(Type::Int, Type::String), + Type::new_option(Type::String), + Type::Int + ]; + assert_eq!( + typecheck_instruction( + &parse("GET_AND_UPDATE").unwrap(), + &mut Ctx::default(), + &mut stack + ), + Ok(GetAndUpdate(overloads::GetAndUpdate::BigMap)) + ); + assert_eq!( + stack, + tc_stk![ + Type::new_big_map(Type::Int, Type::String), + Type::new_option(Type::String) + ] + ); + } + + #[test] + fn get_and_update_map_wrong_ty() { + let mut stack = tc_stk![ + Type::new_map(Type::Int, Type::String), + Type::new_option(Type::Nat), + Type::Int + ]; + assert_eq!( + typecheck_instruction( + &parse("GET_AND_UPDATE").unwrap(), + &mut Ctx::default(), + &mut stack + ), + Err(TypesNotEqual(Type::String, Type::Nat).into()) + ); + } + + #[test] + fn get_and_update_map_incomparable() { + assert_eq!( + parse("GET_AND_UPDATE").unwrap().typecheck_instruction( + &mut Ctx::default(), + None, + &[ + app!(map[app!(list[app!(int)]), app!(string)]), + app!(option[app!(string)]), + app!(list[app!(int)]), + ] + ), + Err(TcError::InvalidTypeProperty( + TypeProperty::Comparable, + Type::new_list(Type::Int) + )) + ); + } + + #[test] + fn get_and_update_map_too_short() { + too_short_test(&app!(GET_AND_UPDATE), Prim::GET_AND_UPDATE, 3) + } + #[test] fn size() { fn check(inp_ty: Type, expected_overload: overloads::Size) { diff --git a/contrib/mir/src/tzt.rs b/contrib/mir/src/tzt.rs index d71b203285bdd1ab2a726fa6baa5b2c62287c917..4937450204ddabb23b9f04061ba5178845a79851 100644 --- a/contrib/mir/src/tzt.rs +++ b/contrib/mir/src/tzt.rs @@ -339,7 +339,7 @@ pub enum TztOutput<'a> { fn execute_tzt_test_code<'a>( code: Micheline<'a>, - ctx: &mut Ctx, + ctx: &mut Ctx<'a>, arena: &'a Arena>, m_parameter: Option, input: Vec<(Type, TypedValue<'a>)>,