From 293990fb7c0877d319ab39212e1837b12e6f9dc0 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 2 Dec 2025 10:31:16 +0100 Subject: [PATCH 1/6] Tezlink/Kernel: No need to keep btree_map diff as a mutable reference --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 6 +++--- etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 48d1a74c7e64..529a7779a479 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -406,7 +406,7 @@ fn transfer<'a, Host: Runtime>( // operations. let consumed_milligas = ctx.tc_ctx.gas.milligas_consumed_by_operation(); let lazy_storage_diff = - convert_big_map_diff(std::mem::take(ctx.tc_ctx.big_map_diff)); + convert_big_map_diff(std::mem::take(&mut ctx.tc_ctx.big_map_diff)); execute_internal_operations( ctx.tc_ctx, ctx.operation_ctx, @@ -536,7 +536,7 @@ fn handle_storage_with_big_maps<'a, Host: Runtime>( let storage = storage .into_micheline_optimized_legacy(&parser.arena) .encode(); - let lazy_storage_diff = convert_big_map_diff(std::mem::take(ctx.big_map_diff)); + let lazy_storage_diff = convert_big_map_diff(std::mem::take(&mut ctx.big_map_diff)); Ok((storage, lazy_storage_diff)) } @@ -897,7 +897,7 @@ fn apply_operation( host, context, gas: &mut gas, - big_map_diff: &mut BTreeMap::new(), + big_map_diff: BTreeMap::new(), next_temporary_id: BigMapId { value: (-1).into() }, }; let parser = Parser::new(); diff --git a/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs b/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs index 48df86b0d219..0b632208d90d 100644 --- a/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs +++ b/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs @@ -42,7 +42,7 @@ pub struct TcCtx<'operation, Host: Runtime> { pub host: &'operation mut Host, pub context: &'operation Context, pub gas: &'operation mut crate::gas::TezlinkOperationGas, - pub big_map_diff: &'operation mut BTreeMap, + pub big_map_diff: BTreeMap, pub next_temporary_id: BigMapId, } @@ -643,7 +643,7 @@ pub mod tests { host: $host, context: $context, gas: &mut gas, - big_map_diff: &mut BTreeMap::new(), + big_map_diff: BTreeMap::new(), next_temporary_id: BigMapId { value: (-1).into() }, }; }; -- GitLab From eedbc2bded1f02ad09f2e0cdd1ebf8f2c63dc873 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 1 Dec 2025 17:32:33 +0100 Subject: [PATCH 2/6] MIR: Function to work with mutable BigMapId --- contrib/mir/src/ast/big_map.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/contrib/mir/src/ast/big_map.rs b/contrib/mir/src/ast/big_map.rs index 2c5928c3d7fd..6526c07b1670 100644 --- a/contrib/mir/src/ast/big_map.rs +++ b/contrib/mir/src/ast/big_map.rs @@ -5,7 +5,7 @@ //! `big_map` typed representation and utilities for working with `big_map`s. use num_bigint::{BigInt, Sign}; -use num_traits::One; +use num_traits::{One, Zero}; use std::{ collections::{btree_map::Entry, BTreeMap}, fmt::Display, @@ -63,6 +63,34 @@ impl BigMapId { } } + /// Increment the mutable id + pub fn incr(&mut self) { + let Zarith(ref int_value) = self.value; + let result = if self.is_temporary() { + int_value - BigInt::one() + } else { + int_value + BigInt::one() + }; + self.value = Zarith(result); + } + + /// Decrement the id + /// + /// If there's no predecessor return false, otherwise true + pub fn dec(&mut self) -> bool { + let Zarith(ref int_value) = self.value; + let result = if self.is_temporary() { + int_value + BigInt::one() + } else { + int_value - BigInt::one() + }; + let has_pred = !result.eq(&BigInt::zero()); + if has_pred { + self.value = Zarith(result); + } + has_pred + } + /// Tells if a big_map id is temporary pub fn is_temporary(&self) -> bool { self.value.0.sign() == Sign::Minus -- GitLab From f622b430e4dc6fb85f3d5e0b0dad0e1ab2be13c2 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 2 Dec 2025 10:47:15 +0100 Subject: [PATCH 3/6] Tezlink/Kernel: The temporary id is common to all operations in a batch --- etherlink/kernel_latest/tezos_execution/src/lib.rs | 6 ++++-- etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 529a7779a479..8112e694c7ba 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -838,7 +838,7 @@ fn apply_batch( } = validation_info; let mut first_failure: Option = None; let mut receipts = Vec::with_capacity(validated_operations.len()); - + let mut next_temporary_id = BigMapId { value: (-1).into() }; for (index, validated_operation) in validated_operations.into_iter().enumerate() { log!( host, @@ -863,6 +863,7 @@ fn apply_batch( origination_nonce, &source_account, validated_operation, + &mut next_temporary_id, block_ctx, ) }; @@ -889,6 +890,7 @@ fn apply_operation( origination_nonce: &mut OriginationNonce, source_account: &TezlinkImplicitAccount, validated_operation: validate::ValidatedOperation, + next_temporary_id: &mut BigMapId, block_ctx: &BlockCtx, ) -> OperationWithMetadata { let mut internal_operations_receipts = Vec::new(); @@ -898,7 +900,7 @@ fn apply_operation( context, gas: &mut gas, big_map_diff: BTreeMap::new(), - next_temporary_id: BigMapId { value: (-1).into() }, + next_temporary_id, }; let parser = Parser::new(); match &validated_operation.content.operation { diff --git a/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs b/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs index 0b632208d90d..83a85b82a66b 100644 --- a/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs +++ b/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs @@ -43,7 +43,7 @@ pub struct TcCtx<'operation, Host: Runtime> { pub context: &'operation Context, pub gas: &'operation mut crate::gas::TezlinkOperationGas, pub big_map_diff: BTreeMap, - pub next_temporary_id: BigMapId, + pub next_temporary_id: &'operation mut BigMapId, } pub struct OperationCtx<'operation> { @@ -334,7 +334,7 @@ impl TcCtx<'_, Host> { fn generate_id(&mut self, temporary: bool) -> Result { if temporary { let new_id = self.next_temporary_id.clone(); - self.next_temporary_id = new_id.succ(); + self.next_temporary_id.incr(); Ok(new_id) } else { let next_id_path = next_id_path(self.context)?; @@ -644,7 +644,7 @@ pub mod tests { context: $context, gas: &mut gas, big_map_diff: BTreeMap::new(), - next_temporary_id: BigMapId { value: (-1).into() }, + next_temporary_id: &mut BigMapId { value: (-1).into() }, }; }; } -- GitLab From 7cefc34758cff6830934672cfb2abc47ffd24cd2 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 2 Dec 2025 10:56:52 +0100 Subject: [PATCH 4/6] Tezlink/Kernel: Remove temporary big_map --- .../kernel_latest/tezos_execution/src/lib.rs | 17 ++++++- .../tezos_execution/src/mir_ctx.rs | 45 ++++++++++++++----- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 8112e694c7ba..528ad64b6c30 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -42,7 +42,10 @@ use tezos_tezlink::{ use crate::address::OriginationNonce; use crate::gas::Cost; -use crate::mir_ctx::{convert_big_map_diff, BlockCtx, Ctx, ExecCtx, OperationCtx, TcCtx}; +use crate::mir_ctx::{ + clear_temporary_big_maps, convert_big_map_diff, BlockCtx, Ctx, ExecCtx, OperationCtx, + TcCtx, +}; extern crate alloc; pub mod account_storage; @@ -875,12 +878,24 @@ fn apply_batch( receipts.push(receipt); } + // Clear all the temporaries big_map after the application of the batch + let cleared = clear_temporary_big_maps(host, context, &mut next_temporary_id); + + if let Err(lazy_storage_err) = cleared { + log!( + host, + Error, + "Cleaning the temporary big_map in the storage failed: {lazy_storage_err}" + ) + } + if let Some(failure_idx) = first_failure { receipts[..failure_idx].iter_mut().for_each(|receipt| { OperationResultSum::transform_result_backtrack(&mut receipt.receipt) }); return (receipts, false); } + (receipts, true) } diff --git a/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs b/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs index 83a85b82a66b..1a3a81cbc1d2 100644 --- a/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs +++ b/etherlink/kernel_latest/tezos_execution/src/mir_ctx.rs @@ -347,6 +347,39 @@ impl TcCtx<'_, Host> { } } +fn remove_big_map( + host: &mut Host, + context: &Context, + id: &BigMapId, +) -> Result<(), LazyStorageError> { + // Remove the key type of the big_map + let key_type_path = key_type_path(context, id)?; + host.store_delete(&key_type_path)?; + + // Remove the value type of the big_map + let value_type_path = value_type_path(context, id)?; + host.store_delete(&value_type_path)?; + + // Removing the content of the big_map + BigMapKeys::remove_keys_in_storage(host, context, id)?; + + Ok(()) +} + +/// Function to clear temporary big_maps create for an operation +/// +/// This function also reset the next temporary id to minus one +pub fn clear_temporary_big_maps( + host: &mut Host, + context: &Context, + next_temp_id: &mut BigMapId, +) -> Result<(), LazyStorageError> { + while next_temp_id.dec() { + remove_big_map(host, context, next_temp_id)?; + } + Ok(()) +} + /// Function to retrieve the hash of a TypedValue. /// Used to retrieve the path where a value is stored in the /// lazy storage. @@ -608,19 +641,11 @@ impl<'a, Host: Runtime> LazyStorage<'a> for TcCtx<'a, Host> { } fn big_map_remove(&mut self, id: &BigMapId) -> Result<(), LazyStorageError> { - // Remove the key type of the big_map - let key_type_path = key_type_path(self.context, id)?; - self.host.store_delete(&key_type_path)?; - - // Remove the value type of the big_map - let value_type_path = value_type_path(self.context, id)?; - self.host.store_delete(&value_type_path)?; - - // Removing the content of the big_map - BigMapKeys::remove_keys_in_storage(self.host, self.context, id)?; + remove_big_map(self.host, self.context, id)?; // Write in the diff that there was a remove self.big_map_diff_remove(id.value.clone()); + Ok(()) } } -- GitLab From 7400bc18fc4b13d6dbaf8e2da02abce0983d5f4d Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 4 Dec 2025 12:02:03 +0100 Subject: [PATCH 5/6] Tezlink/Kernel/Test: Refactor test function for big_map transfer and introduce a check that big_map temporary are cleaned --- .../kernel_latest/tezos_execution/src/lib.rs | 97 ++++++++++++++----- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 528ad64b6c30..ac2b4e69b539 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -1027,6 +1027,7 @@ mod tests { }; use mir::ast::big_map::BigMapId; use mir::ast::{Address, Entrypoint, IntoMicheline, Micheline, Type, TypedValue}; + use mir::context::TypecheckingCtx; use mir::parser::Parser; use mir::typechecker::typecheck_value; use num_traits::ops::checked::CheckedSub; @@ -4580,25 +4581,33 @@ mod tests { .unwrap_or_else(|_| panic!("Contract source code not found for {file}")) } - fn transfer_big_map<'a>( - script_receiver: &str, - init_receiver: &str, + fn big_map_was_removed(ctx: &mut TcCtx<'_, Host>, id: BigMapId) { + let types = ctx + .big_map_get_type(&id) + .expect("Get big_map type should not panic"); + assert_eq!(types, None, "Temporary big_map was not correctly removed"); + } + + struct BigMapTransfer { + sender: TezlinkOriginatedAccount, + receiver: TezlinkOriginatedAccount, + receipts: Vec, + } + + fn transfer_big_map( + ctx: &mut TcCtx<'_, impl Runtime>, + tz1: &Bootstrap, script_sender: &str, init_sender: &str, - expected_sender_big_map: Option, TypedValue<'a>>>, - expected_receiver_big_map: Option, TypedValue<'a>>>, - ) { - let mut host = MockKernelHost::default(); - let context = context::Context::init_context(); - make_default_ctx!(ctx, &mut host, &context); - let tz1 = bootstrap1(); - + script_receiver: &str, + init_receiver: &str, + ) -> BigMapTransfer { let sender_addr = ContractKt1Hash::from_base58_check(CONTRACT_1) .expect("ContractKt1Hash b58 conversion should have succeeded"); let receiver_addr = ContractKt1Hash::from_base58_check(CONTRACT_2) .expect("ContractKt1Hash b58 conversion should have succeeded"); init_account(ctx.host, &tz1.pkh, 1000); - reveal_account(ctx.host, &tz1); + reveal_account(ctx.host, tz1); let parser = Parser::new(); @@ -4638,7 +4647,7 @@ mod tests { let receipts = validate_and_apply_operation( ctx.host, - &context, + ctx.context, OperationHash(H256::zero()), operation, &block_ctx!(), @@ -4648,10 +4657,45 @@ mod tests { "validate_and_apply_operation should not have failed with a kernel error", ); - for r in receipts { + BigMapTransfer { + sender: sender_contract, + receiver: receiver_contract, + receipts, + } + } + + fn test_transfer_big_map<'a>( + script_receiver: &str, + init_receiver: &str, + script_sender: &str, + init_sender: &str, + expected_sender_big_map: Option, TypedValue<'a>>>, + expected_receiver_big_map: Option, TypedValue<'a>>>, + ) { + let mut host = MockKernelHost::default(); + let context = context::Context::init_context(); + make_default_ctx!(ctx, &mut host, &context); + let tz1 = bootstrap1(); + + let result = transfer_big_map( + &mut ctx, + &tz1, + script_sender, + init_sender, + script_receiver, + init_receiver, + ); + + big_map_was_removed(&mut ctx, (-1).into()); + + for r in result.receipts { assert!(r.receipt.is_applied()) } + let parser = Parser::new(); + let sender_contract = result.sender; + let receiver_contract = result.receiver; + if let Some(expected_sender_big_map) = expected_sender_big_map { let storage = sender_contract.storage(ctx.host).unwrap(); let mich_storage = Micheline::decode_raw(&parser.arena, &storage) @@ -4694,14 +4738,21 @@ mod tests { fn big_map_transfer_receiver_drop_sender_fresh() { let script_receiver = read_script("receiver_drop.tz"); let script_sender = read_script("sender_fresh.tz"); - transfer_big_map(&script_receiver, "Unit", &script_sender, "Unit", None, None); + test_transfer_big_map( + &script_receiver, + "Unit", + &script_sender, + "Unit", + None, + None, + ); } #[test] fn big_map_transfer_receiver_drop_sender_stored() { let script_receiver = read_script("receiver_drop.tz"); let script_sender = read_script("sender_stored.tz"); - transfer_big_map( + test_transfer_big_map( &script_receiver, "Unit", &script_sender, @@ -4718,7 +4769,7 @@ mod tests { fn big_map_transfer_receiver_drop_sender_stored_updated() { let script_receiver = read_script("receiver_drop.tz"); let script_sender = read_script("sender_stored_updated.tz"); - transfer_big_map( + test_transfer_big_map( &script_receiver, "Unit", &script_sender, @@ -4735,7 +4786,7 @@ mod tests { fn big_map_transfer_receiver_store_sender_fresh() { let script_receiver = read_script("receiver_store.tz"); let script_sender = read_script("sender_fresh.tz"); - transfer_big_map( + test_transfer_big_map( &script_receiver, "{}", &script_sender, @@ -4752,7 +4803,7 @@ mod tests { fn big_map_transfer_receiver_store_sender_stored() { let script_receiver = read_script("receiver_store.tz"); let script_sender = read_script("sender_stored.tz"); - transfer_big_map( + test_transfer_big_map( &script_receiver, "{}", &script_sender, @@ -4772,7 +4823,7 @@ mod tests { fn big_map_transfer_receiver_store_sender_stored_updated() { let script_receiver = read_script("receiver_store.tz"); let script_sender = read_script("sender_stored_updated.tz"); - transfer_big_map( + test_transfer_big_map( &script_receiver, "{}", &script_sender, @@ -4795,7 +4846,7 @@ mod tests { fn big_map_transfer_receiver_store_updated_sender_fresh() { let script_receiver = read_script("receiver_store_updated.tz"); let script_sender = read_script("sender_fresh.tz"); - transfer_big_map( + test_transfer_big_map( &script_receiver, "{}", &script_sender, @@ -4812,7 +4863,7 @@ mod tests { fn big_map_transfer_receiver_store_updated_sender_stored() { let script_receiver = read_script("receiver_store_updated.tz"); let script_sender = read_script("sender_stored.tz"); - transfer_big_map( + test_transfer_big_map( &script_receiver, "{}", &script_sender, @@ -4832,7 +4883,7 @@ mod tests { fn big_map_transfer_receiver_store_updated_sender_stored_updated() { let script_receiver = read_script("receiver_store_updated.tz"); let script_sender = read_script("sender_stored_updated.tz"); - transfer_big_map( + test_transfer_big_map( &script_receiver, "{}", &script_sender, -- GitLab From ecf68bfb316625f1e5187bfdf98f5456ef9098ec Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 2 Dec 2025 14:18:28 +0100 Subject: [PATCH 6/6] Tezlink/Tests: Verify that the content of a big_map temporary is cleaned between call --- .../kernel_latest/tezos_execution/src/lib.rs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index ac2b4e69b539..a54c6ceddd98 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -5106,4 +5106,108 @@ mod tests { println!("Created_2 OK"); } + + #[test] + fn verify_temp_big_map_content_is_cleaned() { + let mut host = MockKernelHost::default(); + let context = context::Context::init_context(); + make_default_ctx!(ctx, &mut host, &context); + let tz1 = bootstrap1(); + let parser = Parser::new(); + + // A contract that receives a big_map and checks if + // there's a "d" key in the big_map received + let script_receiver = r#" + parameter (big_map string bytes) ; + storage bool ; + code { CAR; PUSH string "d"; MEM; NIL operation; PAIR } + "#; + let script_sender = read_script("sender_stored.tz"); + + // Transfer the big_map in the storage of the sender ({ Elt "d" 0x }) + // And the receiver checks if the big_map received contains the key "d" + // The storage of the receiver contract should change to 'True' after this + // transfer. + let result = transfer_big_map( + &mut ctx, + &tz1, + &script_sender, + "{Elt \"d\" 0x; }", + script_receiver, + "False", + ); + + let receiver = result.receiver; + + for r in result.receipts { + assert!(r.receipt.is_applied()) + } + + // Checks that the storage of the receiver is true + let storage = receiver + .storage(ctx.host) + .expect("Get storage should succeed"); + let storage = Micheline::decode_raw(&parser.arena, &storage) + .expect("Micheline should be decodable"); + let typed_storage = typecheck_value(&storage, &mut ctx, &Type::Bool) + .expect("Typecheck value should succeed"); + assert_eq!(typed_storage, TypedValue::Bool(true)); + + // We redeploy a second contract sender, that will send a different big_map + // to the receiver contract ({Elt "a" 0x}) + let second_sender_contract = + ContractKt1Hash::from_base58_check(CONTRACT_3).unwrap(); + + let _ = init_contract( + ctx.host, + &second_sender_contract, + &script_sender, + &parser + .parse("{Elt \"a\" 0x; }") + .expect("Failed to parse receiver storage"), + &0.into(), + ); + + let operation = make_transfer_operation( + 0, + 2, + 21040, + 5, + tz1.clone(), + 0.into(), + Contract::Originated(second_sender_contract), + Some(Parameter { + entrypoint: Entrypoint::default(), + value: Micheline::from(receiver.kt1().to_base58_check()).encode(), + }), + ); + + // After that call, the storage of the receiver should be 'False' + // as the big_map passed in argument doesn't have the key "d" + let receipts = validate_and_apply_operation( + ctx.host, + ctx.context, + OperationHash(H256::zero()), + operation, + &block_ctx!(), + false, + ) + .expect( + "validate_and_apply_operation should not have failed with a kernel error", + ); + + for r in receipts { + assert!(r.receipt.is_applied()) + } + + // Checks that the storage of the receiver contract is 'False' + let storage = receiver + .storage(ctx.host) + .expect("Get storage should succeed"); + let storage = Micheline::decode_raw(&parser.arena, &storage) + .expect("Micheline should be decodable"); + let typed_storage = typecheck_value(&storage, &mut ctx, &Type::Bool) + .expect("Typecheck value should succeed"); + assert_eq!(typed_storage, TypedValue::Bool(false)); + } } -- GitLab