From 9b1c80183e6811d99b03aa6e588e4ef485a16daf Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Fri, 15 Dec 2023 19:30:57 +0300 Subject: [PATCH 1/6] MIR: EMPTY_BIG_MAP instruction --- contrib/mir/src/ast.rs | 1 + contrib/mir/src/ast/micheline.rs | 1 - contrib/mir/src/gas.rs | 1 + contrib/mir/src/interpreter.rs | 34 ++++++++++++++++++++++++++++++++ contrib/mir/src/typechecker.rs | 24 ++++++++++++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/contrib/mir/src/ast.rs b/contrib/mir/src/ast.rs index 353bc5f7bb56..a338a31e5643 100644 --- a/contrib/mir/src/ast.rs +++ b/contrib/mir/src/ast.rs @@ -477,6 +477,7 @@ pub enum Instruction<'a> { Amount, Nil, EmptySet, + EmptyBigMap(Type, Type), Mem(overloads::Mem), Get(overloads::Get), Update(overloads::Update), diff --git a/contrib/mir/src/ast/micheline.rs b/contrib/mir/src/ast/micheline.rs index f178c2a64ed8..c87067ccd580 100644 --- a/contrib/mir/src/ast/micheline.rs +++ b/contrib/mir/src/ast/micheline.rs @@ -209,7 +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 diff --git a/contrib/mir/src/gas.rs b/contrib/mir/src/gas.rs index c0ae7814bce8..7ceb1e36154e 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; diff --git a/contrib/mir/src/interpreter.rs b/contrib/mir/src/interpreter.rs index 0c5f50454a29..0d13581adbfb 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; use crate::ast::*; use crate::bls; use crate::context::Ctx; @@ -800,6 +801,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!(); @@ -2600,6 +2611,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(); diff --git a/contrib/mir/src/typechecker.rs b/contrib/mir/src/typechecker.rs index 6d114a77175e..d81480d5aa69 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); @@ -3971,6 +3981,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![]; -- GitLab From 8ff9673216e5f89023d0b09d680bcc7cca693f7e Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Fri, 15 Dec 2023 19:59:18 +0300 Subject: [PATCH 2/6] MIR: GET instruction, big_map version --- contrib/mir/src/ast/big_map.rs | 6 +-- contrib/mir/src/ast/overloads.rs | 1 + contrib/mir/src/interpreter.rs | 69 ++++++++++++++++++++++++++++---- contrib/mir/src/lib.rs | 67 +++++++++++++++++++++---------- contrib/mir/src/typechecker.rs | 17 ++++++++ contrib/mir/src/tzt.rs | 2 +- 6 files changed, 129 insertions(+), 33 deletions(-) diff --git a/contrib/mir/src/ast/big_map.rs b/contrib/mir/src/ast/big_map.rs index 59fe3c208cd5..0d36be45d9c4 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/overloads.rs b/contrib/mir/src/ast/overloads.rs index 18f0098bfe95..39f8c601bc7b 100644 --- a/contrib/mir/src/ast/overloads.rs +++ b/contrib/mir/src/ast/overloads.rs @@ -82,6 +82,7 @@ pub enum Neg { #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Get { Map, + BigMap, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] diff --git a/contrib/mir/src/interpreter.rs b/contrib/mir/src/interpreter.rs index 0d13581adbfb..f2903f528e12 100644 --- a/contrib/mir/src/interpreter.rs +++ b/contrib/mir/src/interpreter.rs @@ -12,7 +12,7 @@ use num_traits::{Signed, Zero}; use std::rc::Rc; use typed_arena::Arena; -use crate::ast::big_map::BigMap; +use crate::ast::big_map::{BigMap, LazyStorageError}; use crate::ast::*; use crate::bls; use crate::context::Ctx; @@ -29,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)] @@ -51,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>, @@ -87,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>> { @@ -97,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>> { @@ -117,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>> { @@ -835,6 +837,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 => { @@ -1235,6 +1246,7 @@ mod interpreter_tests { use super::*; use super::{Lambda, Or}; + use crate::ast::big_map::InMemoryLazyStorage; use crate::ast::michelson_address as addr; use crate::bls; use crate::gas::Gas; @@ -1250,7 +1262,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()); @@ -1259,7 +1271,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()); @@ -2512,6 +2524,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(); diff --git a/contrib/mir/src/lib.rs b/contrib/mir/src/lib.rs index 4e0b88f33328..ae2c47287666 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 d81480d5aa69..2fb3fb7dd97b 100644 --- a/contrib/mir/src/typechecker.rs +++ b/contrib/mir/src/typechecker.rs @@ -1336,6 +1336,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!(), @@ -4021,6 +4028,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!( diff --git a/contrib/mir/src/tzt.rs b/contrib/mir/src/tzt.rs index d71b203285bd..4937450204dd 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>)>, -- GitLab From 593057915243cef65d572da00de54029b4916d14 Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Fri, 15 Dec 2023 20:10:18 +0300 Subject: [PATCH 3/6] MIR: MEM instruction, big_map version --- contrib/mir/src/ast/overloads.rs | 1 + contrib/mir/src/interpreter.rs | 51 +++++++++++++++++++++++++++++++- contrib/mir/src/typechecker.rs | 19 +++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/contrib/mir/src/ast/overloads.rs b/contrib/mir/src/ast/overloads.rs index 39f8c601bc7b..1d82c333a483 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)] diff --git a/contrib/mir/src/interpreter.rs b/contrib/mir/src/interpreter.rs index f2903f528e12..6930d87880df 100644 --- a/contrib/mir/src/interpreter.rs +++ b/contrib/mir/src/interpreter.rs @@ -828,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 => { @@ -1246,7 +1255,7 @@ mod interpreter_tests { use super::*; use super::{Lambda, Or}; - use crate::ast::big_map::InMemoryLazyStorage; + use crate::ast::big_map::{InMemoryLazyStorage, LazyStorageBulkUpdate}; use crate::ast::michelson_address as addr; use crate::bls; use crate::gas::Gas; @@ -2609,6 +2618,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(); diff --git a/contrib/mir/src/typechecker.rs b/contrib/mir/src/typechecker.rs index 2fb3fb7dd97b..76e459511d04 100644 --- a/contrib/mir/src/typechecker.rs +++ b/contrib/mir/src/typechecker.rs @@ -1321,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!(), @@ -4075,6 +4082,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!( -- GitLab From 83a09c7caf74848f96dffb5855c1461fab08ca2d Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Fri, 15 Dec 2023 20:25:19 +0300 Subject: [PATCH 4/6] MIR: UPDATE instruction, big_map version --- contrib/mir/src/ast/overloads.rs | 1 + contrib/mir/src/interpreter.rs | 89 ++++++++++++++++++++++++++++++++ contrib/mir/src/typechecker.rs | 21 ++++++++ 3 files changed, 111 insertions(+) diff --git a/contrib/mir/src/ast/overloads.rs b/contrib/mir/src/ast/overloads.rs index 1d82c333a483..2c13b196255b 100644 --- a/contrib/mir/src/ast/overloads.rs +++ b/contrib/mir/src/ast/overloads.rs @@ -90,6 +90,7 @@ pub enum Get { pub enum Update { Set, Map, + BigMap, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] diff --git a/contrib/mir/src/interpreter.rs b/contrib/mir/src/interpreter.rs index 6930d87880df..f82e45e19377 100644 --- a/contrib/mir/src/interpreter.rs +++ b/contrib/mir/src/interpreter.rs @@ -880,6 +880,15 @@ 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::Size(overload) => { macro_rules! run_size { @@ -2971,6 +2980,86 @@ 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 seq() { let mut stack = stk![V::int(1), V::nat(2)]; diff --git a/contrib/mir/src/typechecker.rs b/contrib/mir/src/typechecker.rs index 76e459511d04..068439236dca 100644 --- a/contrib/mir/src/typechecker.rs +++ b/contrib/mir/src/typechecker.rs @@ -1366,6 +1366,13 @@ 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!(), @@ -4190,6 +4197,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![ -- GitLab From 73a880a6770ea0ccfcb4f39ced7a2c523d26f57b Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Wed, 13 Dec 2023 17:07:52 +0300 Subject: [PATCH 5/6] MIR: GET_AND_UPDATE instruction, map overload --- contrib/mir/src/ast.rs | 1 + contrib/mir/src/ast/micheline.rs | 1 - contrib/mir/src/ast/overloads.rs | 5 +++ contrib/mir/src/gas.rs | 12 +++++ contrib/mir/src/interpreter.rs | 14 ++++++ contrib/mir/src/typechecker.rs | 76 ++++++++++++++++++++++++++++++++ 6 files changed, 108 insertions(+), 1 deletion(-) diff --git a/contrib/mir/src/ast.rs b/contrib/mir/src/ast.rs index a338a31e5643..4708b30ea447 100644 --- a/contrib/mir/src/ast.rs +++ b/contrib/mir/src/ast.rs @@ -481,6 +481,7 @@ pub enum Instruction<'a> { 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/micheline.rs b/contrib/mir/src/ast/micheline.rs index c87067ccd580..9bacd634ecae 100644 --- a/contrib/mir/src/ast/micheline.rs +++ b/contrib/mir/src/ast/micheline.rs @@ -209,7 +209,6 @@ macro_rules! micheline_unsupported_instructions { | Prim::CREATE_CONTRACT | Prim::EMIT | Prim::EMPTY_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 2c13b196255b..d6937788aed0 100644 --- a/contrib/mir/src/ast/overloads.rs +++ b/contrib/mir/src/ast/overloads.rs @@ -93,6 +93,11 @@ pub enum Update { BigMap, } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum GetAndUpdate { + Map, +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Size { String, diff --git a/contrib/mir/src/gas.rs b/contrib/mir/src/gas.rs index 7ceb1e36154e..d716ed4688ba 100644 --- a/contrib/mir/src/gas.rs +++ b/contrib/mir/src/gas.rs @@ -634,6 +634,18 @@ 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. + (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 f82e45e19377..1305461933ed 100644 --- a/contrib/mir/src/interpreter.rs +++ b/contrib/mir/src/interpreter.rs @@ -890,6 +890,20 @@ fn interpret_one<'a>( 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)); + } + }, I::Size(overload) => { macro_rules! run_size { ($ctor:tt, $gas:ident) => {{ diff --git a/contrib/mir/src/typechecker.rs b/contrib/mir/src/typechecker.rs index 068439236dca..767d11f371cb 100644 --- a/contrib/mir/src/typechecker.rs +++ b/contrib/mir/src/typechecker.rs @@ -1377,6 +1377,17 @@ pub(crate) fn typecheck_instruction<'a>( (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, [], _), [.., _, _, _]) => 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) @@ -4243,6 +4254,71 @@ 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_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) { -- GitLab From b3204223f29ccbf7e1f56afbe280b0fac8466923 Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov Date: Fri, 15 Dec 2023 20:37:50 +0300 Subject: [PATCH 6/6] MIR: GET_AND_UPDATE instruction, big_map version --- contrib/mir/src/ast/overloads.rs | 1 + contrib/mir/src/gas.rs | 4 ++ contrib/mir/src/interpreter.rs | 104 +++++++++++++++++++++++++++++++ contrib/mir/src/typechecker.rs | 31 +++++++++ 4 files changed, 140 insertions(+) diff --git a/contrib/mir/src/ast/overloads.rs b/contrib/mir/src/ast/overloads.rs index d6937788aed0..20b09d6e6fdf 100644 --- a/contrib/mir/src/ast/overloads.rs +++ b/contrib/mir/src/ast/overloads.rs @@ -96,6 +96,7 @@ pub enum Update { #[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 d716ed4688ba..146451d55989 100644 --- a/contrib/mir/src/gas.rs +++ b/contrib/mir/src/gas.rs @@ -643,6 +643,10 @@ pub mod interpret_cost { // 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() } diff --git a/contrib/mir/src/interpreter.rs b/contrib/mir/src/interpreter.rs index 1305461933ed..ac8f214a5957 100644 --- a/contrib/mir/src/interpreter.rs +++ b/contrib/mir/src/interpreter.rs @@ -903,6 +903,17 @@ fn interpret_one<'a>( }; 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 { @@ -3074,6 +3085,99 @@ mod interpreter_tests { ); } + #[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/typechecker.rs b/contrib/mir/src/typechecker.rs index 767d11f371cb..84027420ebd4 100644 --- a/contrib/mir/src/typechecker.rs +++ b/contrib/mir/src/typechecker.rs @@ -1384,6 +1384,13 @@ pub(crate) fn typecheck_instruction<'a>( 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!(), @@ -4278,6 +4285,30 @@ mod typecheck_tests { ); } + #[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![ -- GitLab