diff --git a/etherlink/kernel_latest/Cargo.lock b/etherlink/kernel_latest/Cargo.lock index 5a0caf59edf212d33a88c93428d1b342c75d92db..7072378f9a5831e68e5ec1aefe9b53357a2247b5 100644 --- a/etherlink/kernel_latest/Cargo.lock +++ b/etherlink/kernel_latest/Cargo.lock @@ -3061,6 +3061,7 @@ dependencies = [ "nom", "num-bigint", "num-traits", + "pretty_assertions", "primitive-types", "tezos-evm-logging-latest", "tezos-evm-runtime-latest", diff --git a/etherlink/kernel_latest/tezos/src/operation_result.rs b/etherlink/kernel_latest/tezos/src/operation_result.rs index d193215781d67185ec9f196173f5a05fd4c8c843..45f1303a79fce79d9799c46e82f08288f71c743f 100644 --- a/etherlink/kernel_latest/tezos/src/operation_result.rs +++ b/etherlink/kernel_latest/tezos/src/operation_result.rs @@ -96,6 +96,7 @@ impl From for TransferError { pub enum ApplyOperationError { Reveal(RevealError), Transfer(TransferError), + UnSupportedOperation(String), } impl From for ApplyOperationError { diff --git a/etherlink/kernel_latest/tezos_execution/Cargo.toml b/etherlink/kernel_latest/tezos_execution/Cargo.toml index 92e4d1b8fe47bb2224522bf6508358bcd4ff6c56..4664fd40cdd6d21f406e2e42a54602a50e271b61 100644 --- a/etherlink/kernel_latest/tezos_execution/Cargo.toml +++ b/etherlink/kernel_latest/tezos_execution/Cargo.toml @@ -30,3 +30,6 @@ tezos-smart-rollup.workspace = true tezos_tezlink.workspace = true tezos-evm-logging.workspace = true mir.workspace = true + +[dev-dependencies] +pretty_assertions = "1.4.0" diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index fbdbd6907c92d7a54b61b431e54cd6c6e335d8d0..73bdf839d1da8c8cf6207314d58ce0f4aab607d2 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -4,12 +4,13 @@ use account_storage::TezlinkAccount; use account_storage::{Manager, TezlinkImplicitAccount, TezlinkOriginatedAccount}; +use mir::ast::{AddressHash, Entrypoint, OperationInfo, TransferTokens}; use mir::{ ast::{IntoMicheline, Micheline}, context::Ctx, parser::Parser, }; -use num_bigint::BigInt; +use num_bigint::{BigInt, BigUint}; use num_traits::ops::checked::CheckedSub; use tezos_crypto_rs::{base58::FromBase58CheckError, PublicKeyWithHash}; use tezos_data_encoding::enc::BinError; @@ -18,6 +19,7 @@ use tezos_evm_logging::{log, Level::*, Verbosity}; use tezos_evm_runtime::{runtime::Runtime, safe_storage::SafeStorage}; use tezos_smart_rollup::types::{Contract, PublicKey, PublicKeyHash}; use tezos_tezlink::operation::Operation; +use tezos_tezlink::operation_result::{ApplyOperationError, TransferTarget}; use tezos_tezlink::{ operation::{ ManagerOperation, OperationContent, Parameter, RevealContent, TransferContent, @@ -25,7 +27,7 @@ use tezos_tezlink::{ operation_result::{ is_applied, produce_operation_result, Balance, BalanceTooLow, BalanceUpdate, OperationError, OperationResultSum, Reveal, RevealError, RevealSuccess, - TransferError, TransferSuccess, TransferTarget, UpdateOrigin, + TransferError, TransferSuccess, UpdateOrigin, }, }; use thiserror::Error; @@ -49,6 +51,8 @@ pub enum ApplyKernelError { BigIntError(num_bigint::TryFromBigIntError), #[error("Serialization failed because of {0}")] BinaryError(String), + #[error("Apply operation failed because of an unsupported address error")] + MirAddressUnsupportedError, } // 'FromBase58CheckError' doesn't implement PartialEq and Eq @@ -110,32 +114,35 @@ fn reveal( })) } -/// Handles manager transfer operations for both implicit and originated contracts. -pub fn transfer( +fn contract_from_address(address: AddressHash) -> Result { + match address { + AddressHash::Kt1(kt1) => Ok(Contract::Originated(kt1)), + AddressHash::Implicit(pkh) => Ok(Contract::Implicit(pkh)), + AddressHash::Sr1(_) => Err(ApplyKernelError::MirAddressUnsupportedError), + } +} + +fn address_from_contract(contract: Contract) -> AddressHash { + match contract { + Contract::Originated(kt1) => AddressHash::Kt1(kt1), + Contract::Implicit(hash) => AddressHash::Implicit(hash), + } +} + +pub fn transfer_tez( host: &mut Host, - context: &context::Context, - src: &PublicKeyHash, + src_contract: &Contract, + src_account: &mut impl TezlinkAccount, amount: &Narith, - dest: &Contract, - parameter: Option, -) -> ExecutionResult { - log!( - host, - Debug, - "Applying a transfer operation from {} to {:?} of {:?} mutez", - src, - dest, - amount - ); - - let src_contract = Contract::Implicit(src.clone()); - let (src_update, dest_update) = compute_balance_updates(&src_contract, dest, amount) - .map_err(ApplyKernelError::BigIntError)?; + dest_contract: &Contract, + dest_account: &mut impl TezlinkAccount, +) -> ExecutionResult { + let (src_update, dest_update) = + compute_balance_updates(src_contract, dest_contract, amount) + .map_err(ApplyKernelError::BigIntError)?; // Check source balance - let mut src_account = TezlinkImplicitAccount::from_public_key_hash(context, src)?; let current_src_balance = src_account.balance(host)?.0; - let new_source_balance = match current_src_balance.checked_sub(&amount.0) { None => { log!(host, Debug, "Balance is too low"); @@ -148,73 +155,213 @@ pub fn transfer( } Some(new_source_balance) => new_source_balance, }; + apply_balance_changes( + host, + src_account, + new_source_balance, + dest_account, + &amount.0, + )?; + Ok(Ok(TransferSuccess { + storage: None, + lazy_storage_diff: None, + balance_updates: vec![src_update, dest_update], + ticket_receipt: vec![], + originated_contracts: vec![], + consumed_gas: 0_u64.into(), + storage_size: 0_u64.into(), + paid_storage_size_diff: 0_u64.into(), + allocated_destination_contract: false, + })) +} + +pub fn execute_internal_operations<'a, Host: Runtime>( + host: &mut Host, + context: &context::Context, + internal_operations: impl Iterator>, + sender_contract: &Contract, + sender_account: &mut TezlinkOriginatedAccount, + parser: &'a Parser<'a>, + ctx: &mut Ctx<'a>, +) -> ExecutionResult<()> { + for internal_op in internal_operations { + log!( + host, + Debug, + "Executing internal operation: {:?}", + internal_op + ); + let internal_receipt = match internal_op.operation { + mir::ast::Operation::TransferTokens(TransferTokens { + param, + destination_address, + amount, + }) => { + let amount = Narith(amount.try_into().unwrap_or(BigUint::ZERO)); + let dest_contract = contract_from_address(destination_address.hash)?; + transfer( + host, + context, + sender_contract, + sender_account, + &amount, + &dest_contract, + destination_address.entrypoint, + param.into_micheline_optimized_legacy(&parser.arena), + parser, + ctx, + ) + } + _ => { + return Ok(Err(ApplyOperationError::UnSupportedOperation( + "Unsupported internal operation".to_string(), + ) + .into())); + } + }?; + match internal_receipt { + Ok(receipt) => { + log!( + host, + Debug, + "Internal operation executed successfully: {:?}", + receipt + ); + } + Err(error) => { + return Ok(Err(error)); + } + } + } + Ok(Ok(())) +} - // Delegate to appropriate handler - let success = match dest { - Contract::Implicit(dest_key_hash) => { - if parameter.is_some() { +/// Handles manager transfer operations for both implicit and originated contracts but with a MIR context. +#[allow(clippy::too_many_arguments)] +pub fn transfer<'a, Host: Runtime>( + host: &mut Host, + context: &context::Context, + src_contract: &Contract, + src_account: &mut impl TezlinkAccount, + amount: &Narith, + dest_contract: &Contract, + entrypoint: Entrypoint, + param: Micheline<'a>, + parser: &'a Parser<'a>, + ctx: &mut Ctx<'a>, +) -> ExecutionResult { + match dest_contract { + Contract::Implicit(pkh) => { + if param != Micheline::from(()) || !entrypoint.is_default() { return Ok(Err(TransferError::NonSmartContractExecutionCall.into())); } - let allocated = TezlinkImplicitAccount::allocate(host, context, dest)?; - let mut dest_account = - TezlinkImplicitAccount::from_public_key_hash(context, dest_key_hash)?; - apply_balance_changes( + // Allocate the implicit account if it doesn't exist + let allocated = + TezlinkImplicitAccount::allocate(host, context, dest_contract)?; + match transfer_tez( host, - &mut src_account, - new_source_balance.clone(), - &mut dest_account, - &amount.0, - )?; - - Ok(Ok(TransferTarget::ToContrat(TransferSuccess { - storage: None, - lazy_storage_diff: None, - balance_updates: vec![src_update, dest_update], - ticket_receipt: vec![], - originated_contracts: vec![], - consumed_gas: 0_u64.into(), - storage_size: 0_u64.into(), - paid_storage_size_diff: 0_u64.into(), - allocated_destination_contract: allocated, - }))) + src_contract, + src_account, + amount, + dest_contract, + &mut TezlinkImplicitAccount::from_public_key_hash(context, pkh)?, + )? { + Ok(success) => Ok(Ok(TransferSuccess { + allocated_destination_contract: allocated, + ..success + })), + Err(error) => Ok(Err(error)), + } } - Contract::Originated(_) => { - let mut dest_contract = - TezlinkOriginatedAccount::from_contract(context, dest)?; - apply_balance_changes( + let mut dest_account = + TezlinkOriginatedAccount::from_contract(context, dest_contract)?; + let receipt = match transfer_tez( host, - &mut src_account, - new_source_balance.clone(), - &mut dest_contract, - &amount.0, - )?; - - let code = dest_contract.code(host)?; - let storage = dest_contract.storage(host)?; - - let new_storage = execute_smart_contract(code, storage, ¶meter); - - match new_storage { - Ok(new_storage) => { - let _ = dest_contract.set_storage(host, &new_storage); - Ok(Ok(TransferTarget::ToContrat(TransferSuccess { + src_contract, + src_account, + amount, + dest_contract, + &mut dest_account, + )? { + Ok(success) => success, + Err(error) => return Ok(Err(error)), + }; + ctx.sender = address_from_contract(src_contract.clone()); + let code = dest_account.code(host)?; + let storage = dest_account.storage(host)?; + let result = + execute_smart_contract(code, storage, entrypoint, param, parser, ctx); + match result { + Ok((internal_operations, new_storage)) => { + dest_account.set_storage(host, &new_storage)?; + let _internal_receipt = execute_internal_operations( + host, + context, + internal_operations, + dest_contract, + &mut dest_account, + parser, + ctx, + )?; + Ok(Ok(TransferSuccess { storage: Some(new_storage), - lazy_storage_diff: None, - balance_updates: vec![src_update, dest_update], - ticket_receipt: vec![], - originated_contracts: vec![], - consumed_gas: 0_u64.into(), - storage_size: 0_u64.into(), - paid_storage_size_diff: 0_u64.into(), - allocated_destination_contract: false, - }))) + ..receipt + })) } - Err(err) => Ok(Err(err.into())), } } + } +} + +// Handles manager transfer operations. +pub fn transfer_external( + host: &mut Host, + context: &context::Context, + src: &PublicKeyHash, + amount: &Narith, + dest: &Contract, + parameter: Option, +) -> ExecutionResult { + log!( + host, + Debug, + "Applying an external transfer operation from {} to {:?} of {:?} mutez with parameters {:?}", + src, + dest, + amount, + parameter + ); + + let src_contract = Contract::Implicit(src.clone()); + let mut src_account = TezlinkImplicitAccount::from_public_key_hash(context, src)?; + let parser = Parser::new(); + let (entrypoint, value) = match parameter { + Some(param) => ( + param.entrypoint, + match Micheline::decode_raw(&parser.arena, ¶m.value) { + Ok(value) => value, + Err(err) => return Ok(Err(TransferError::from(err).into())), + }, + ), + None => (Entrypoint::default(), Micheline::from(())), }; + let mut ctx = Ctx::default(); + ctx.source = address_from_contract(src_contract.clone()); + + let success = transfer( + host, + context, + &src_contract, + &mut src_account, + amount, + dest, + entrypoint, + value, + &parser, + &mut ctx, + ); // TODO : Counter Increment should be done after successful validation (see issue #8031) src_account.increment_counter(host)?; success @@ -288,40 +435,35 @@ fn apply_balance_changes( Ok(()) } -/// Executes the entrypoint logic of an originated smart contract and returns the new storage and consumed gas. -fn execute_smart_contract( +/// Executes the entrypoint logic of an originated smart contract and returns the new storage. +fn execute_smart_contract<'a>( code: Vec, storage: Vec, - parameter: &Option, -) -> Result, TransferError> { - let parser = Parser::new(); + entrypoint: Entrypoint, + value: Micheline<'a>, + parser: &'a Parser<'a>, + ctx: &mut Ctx<'a>, +) -> Result<(impl Iterator>, Vec), TransferError> { + // Parse and typecheck the contract let contract_micheline = Micheline::decode_raw(&parser.arena, &code)?; + let contract_typechecked = contract_micheline.typecheck_script(ctx)?; + let storage_micheline = Micheline::decode_raw(&parser.arena, &storage)?; - let (entrypoint, value) = match parameter { - Some(param) => ( - Some(param.entrypoint.clone()), - Micheline::decode_raw(&parser.arena, ¶m.value)?, - ), - None => (None, Micheline::from(())), - }; - - let mut ctx = Ctx::default(); - let contract_typechecked = contract_micheline.typecheck_script(&mut ctx)?; - - let storage = Micheline::decode_raw(&parser.arena, &storage)?; - - let (_, new_storage) = contract_typechecked.interpret( - &mut ctx, + // Execute the contract + let (internal_operations, new_storage) = contract_typechecked.interpret( + ctx, &parser.arena, value, - entrypoint, - storage, + Some(entrypoint), + storage_micheline, )?; + // Encode the new storage let new_storage = new_storage .into_micheline_optimized_legacy(&parser.arena) .encode(); - Ok(new_storage) + + Ok((internal_operations, new_storage)) } pub fn apply_operation( @@ -403,7 +545,7 @@ pub fn apply_operation( destination, parameters, }) => { - let transfer_result = transfer( + let transfer_result = transfer_external( &mut safe_host, context, source, @@ -411,8 +553,10 @@ pub fn apply_operation( &destination, parameters, )?; - let manager_result = - produce_operation_result(vec![src_delta, block_fees], transfer_result); + let manager_result = produce_operation_result( + vec![src_delta, block_fees], + transfer_result.map(TransferTarget::ToContrat), + ); OperationResultSum::Transfer(manager_result) } }; @@ -429,7 +573,9 @@ pub fn apply_operation( #[cfg(test)] mod tests { - use crate::{TezlinkImplicitAccount, TezlinkOriginatedAccount}; + use crate::{account_storage::TezlinkOriginatedAccount, TezlinkImplicitAccount}; + use mir::ast::{Entrypoint, Micheline}; + use pretty_assertions::assert_eq; use tezos_crypto_rs::hash::{ContractKt1Hash, SecretKeyEd25519}; use tezos_data_encoding::types::Narith; use tezos_evm_runtime::runtime::{MockKernelHost, Runtime}; @@ -1159,6 +1305,68 @@ mod tests { assert_eq!(receipt, expected_receipt); } + #[test] + fn apply_transfer_to_originated_faucet() { + let mut host = MockKernelHost::default(); + let context = context::Context::init_context(); + let (requester_balance, faucet_balance, fees) = (50, 1000, 15); + let src = bootstrap1(); + let desthash = + ContractKt1Hash::from_base58_check("KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton") + .expect("ContractKt1Hash b58 conversion should have succeeded"); + // Setup accounts with 50 mutez in their balance + let requester = init_account(&mut host, &src.pkh); + reveal_account(&mut host, &src); + let (code, storage) = ( + r#" + parameter (mutez %fund); + storage unit; + code + { + UNPAIR; + SENDER; + CONTRACT unit; + IF_NONE { FAILWITH } {}; + SWAP; + UNIT; + TRANSFER_TOKENS; + NIL operation; + SWAP; + CONS; + PAIR + } + "#, + "Unit", + ); + let faucet = init_contract(&mut host, &desthash, code, storage, &1000.into()); + let requested_amount = 100; + let operation = make_transfer_operation( + fees, + 1, + 4, + 5, + src.clone(), + 0.into(), + Contract::Originated(desthash).clone(), + Some(Parameter { + entrypoint: Entrypoint::try_from("fund") + .expect("Entrypoint should be valid"), + value: Micheline::from(requested_amount as i128).encode(), + }), + ); + let res = apply_operation(&mut host, &context, operation) + .expect("apply_operation should not have failed with a kernel error"); + println!("Result: {:?}", res); + assert_eq!( + faucet.balance(&host).unwrap(), + (faucet_balance - requested_amount).into() + ); + assert_eq!( + requester.balance(&host).unwrap(), + (requester_balance + requested_amount - fees).into() + ); // The faucet should have transferred 100 mutez to the source + } + #[test] fn apply_transfer_with_execution() { let mut host = MockKernelHost::default(); @@ -1320,4 +1528,115 @@ mod tests { "Counter should not have been incremented" ); } + + #[test] + fn apply_transfer_with_argument_to_implicit_fails() { + let mut host = MockKernelHost::default(); + let parser = mir::parser::Parser::new(); + + let src = bootstrap1(); + + let dest = bootstrap2(); + + init_account(&mut host, &src.pkh); + reveal_account(&mut host, &src); + + let operation = make_transfer_operation( + 15, + 1, + 4, + 5, + src.clone(), + 30_u64.into(), + Contract::Implicit(dest.pkh), + Some(Parameter { + entrypoint: mir::ast::entrypoint::Entrypoint::default(), + value: parser.parse("0").unwrap().encode(), + }), + ); + + let receipt = + apply_operation(&mut host, &context::Context::init_context(), operation) + .expect("apply_operation should not have failed with a kernel error"); + + let expected_receipt = OperationResultSum::Transfer(OperationResult { + balance_updates: vec![ + BalanceUpdate { + balance: Balance::Account(Contract::Implicit(src.pkh)), + changes: -15, + update_origin: UpdateOrigin::BlockApplication, + }, + BalanceUpdate { + balance: Balance::BlockFees, + changes: 15, + update_origin: UpdateOrigin::BlockApplication, + }, + ], + result: ContentResult::Failed( + vec![OperationError::Apply(ApplyOperationError::Transfer( + TransferError::NonSmartContractExecutionCall, + ))] + .into(), + ), + internal_operation_results: vec![], + }); + + assert_eq!(receipt, expected_receipt); + } + + #[test] + fn apply_transfer_with_non_default_entrypoint_to_implicit_fails() { + let mut host = MockKernelHost::default(); + let parser = mir::parser::Parser::new(); + + let src = bootstrap1(); + + let dest = bootstrap2(); + + init_account(&mut host, &src.pkh); + reveal_account(&mut host, &src); + + let operation = make_transfer_operation( + 15, + 1, + 4, + 5, + src.clone(), + 30_u64.into(), + Contract::Implicit(dest.pkh), + Some(Parameter { + entrypoint: mir::ast::entrypoint::Entrypoint::try_from("non_default") + .expect("Entrypoint should be valid"), + value: parser.parse("0").unwrap().encode(), + }), + ); + + let receipt = + apply_operation(&mut host, &context::Context::init_context(), operation) + .expect("apply_operation should not have failed with a kernel error"); + + let expected_receipt = OperationResultSum::Transfer(OperationResult { + balance_updates: vec![ + BalanceUpdate { + balance: Balance::Account(Contract::Implicit(src.pkh)), + changes: -15, + update_origin: UpdateOrigin::BlockApplication, + }, + BalanceUpdate { + balance: Balance::BlockFees, + changes: 15, + update_origin: UpdateOrigin::BlockApplication, + }, + ], + result: ContentResult::Failed( + vec![OperationError::Apply(ApplyOperationError::Transfer( + TransferError::NonSmartContractExecutionCall, + ))] + .into(), + ), + internal_operation_results: vec![], + }); + + assert_eq!(receipt, expected_receipt); + } } diff --git a/etherlink/tezt/lib/michelson_contracts.ml b/etherlink/tezt/lib/michelson_contracts.ml index a4e9116252fc7597caa9bff8a2dbb5d27df711f4..5563c4cda807a62759162faf533cc060b2a20fe3 100644 --- a/etherlink/tezt/lib/michelson_contracts.ml +++ b/etherlink/tezt/lib/michelson_contracts.ml @@ -16,3 +16,13 @@ let concat_hello () = find ["opcodes"; "concat_hello"] tezlink_protocol |> path); initial_storage = "{ \"initial\" }"; } + +let faucet_contract () = + Evm_node. + { + address = "KT1QuofAgnsWffHzLA7D78rxytJruGHDe7XG"; + path = + Michelson_script.( + find ["mini_scenarios"; "faucet"] tezlink_protocol |> path); + initial_storage = "Unit"; + } diff --git a/etherlink/tezt/tests/tezlink.ml b/etherlink/tezt/tests/tezlink.ml index cb56bcb9150841d05138acbadfd977750fb3855c..8e6142f96e4e833de188f2d294cfe4ebc886555f 100644 --- a/etherlink/tezt/tests/tezlink.ml +++ b/etherlink/tezt/tests/tezlink.ml @@ -1074,6 +1074,43 @@ let test_tezlink_sandbox () = ~error_msg:"Wrong balance for bootstrap2: expected %R, actual %L" ; unit +let test_tezlink_internal_operation = + let bootstrap_balance = Tez.of_mutez_int 3_800_000_000_000 in + let faucet = Tezt_etherlink.Michelson_contracts.faucet_contract () in + register_tezlink_test + ~title:"Internal operation" + ~tags:["internal"; "operation"] + ~bootstrap_accounts:[Constant.bootstrap1] + ~bootstrap_contracts:[faucet] + @@ fun {sequencer; client; _} _protocol -> + let endpoint = + Client.( + Foreign_endpoint + Endpoint. + {(Evm_node.rpc_endpoint_record sequencer) with path = "/tezlink"}) + in + let* () = + Client.transfer + ~endpoint + ~fee:Tez.zero + ~amount:Tez.zero + ~giver:Constant.bootstrap1.alias + ~receiver:faucet.address + ~burn_cap:Tez.one + ~entrypoint:"fund" + ~arg:"1000000" + client + in + let*@ _ = produce_block sequencer in + let* balance = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap1.alias client + in + Check.( + (Tez.to_mutez balance = Tez.to_mutez bootstrap_balance + Tez.(to_mutez one)) + int) + ~error_msg:"Wrong balance for bootstrap1: exptected %R, actual %L" ; + unit + let () = test_observer_starts [Alpha] ; test_describe_endpoint [Alpha] ; @@ -1102,4 +1139,5 @@ let () = test_tezlink_storage [Alpha] ; test_tezlink_execution [Alpha] ; test_tezlink_bootstrap_block_info [Alpha] ; - test_tezlink_sandbox () + test_tezlink_sandbox () ; + test_tezlink_internal_operation [Alpha] diff --git a/michelson_test_scripts/mini_scenarios/faucet.tz b/michelson_test_scripts/mini_scenarios/faucet.tz new file mode 100644 index 0000000000000000000000000000000000000000..c8cdb75f3f06914e55cd7c4c45941488968ac860 --- /dev/null +++ b/michelson_test_scripts/mini_scenarios/faucet.tz @@ -0,0 +1,16 @@ +parameter (mutez %fund); +storage unit; +code + { + UNPAIR; + SENDER; + CONTRACT unit; + ASSERT_SOME; + SWAP; + UNIT; + TRANSFER_TOKENS; + NIL operation; + SWAP; + CONS; + PAIR + }