diff --git a/etherlink/kernel_latest/Cargo.lock b/etherlink/kernel_latest/Cargo.lock index 64449cb6bdd2297a62c57bd95d5b3f63bd8d27ef..cc783e76cd5a963c7c6a087968f1a858869488cd 100644 --- a/etherlink/kernel_latest/Cargo.lock +++ b/etherlink/kernel_latest/Cargo.lock @@ -2698,6 +2698,7 @@ dependencies = [ "hex", "nom", "num-bigint", + "num-traits", "primitive-types", "tezos-evm-logging-latest", "tezos-evm-runtime-latest", diff --git a/etherlink/kernel_latest/kernel/src/block.rs b/etherlink/kernel_latest/kernel/src/block.rs index b018027f5561220f6afae73c6c1b08df0b992eb0..08d7018d2f67db602f0aa952b7ec0fc0030add37 100644 --- a/etherlink/kernel_latest/kernel/src/block.rs +++ b/etherlink/kernel_latest/kernel/src/block.rs @@ -624,6 +624,7 @@ mod tests { use primitive_types::{H160, U256}; use std::str::FromStr; use tezos_crypto_rs::hash::UnknownSignature; + use tezos_data_encoding::types::Narith; use tezos_ethereum::block::BlockFees; use tezos_ethereum::transaction::{ TransactionHash, TransactionStatus, TransactionType, TRANSACTION_HASH_SIZE, @@ -647,19 +648,20 @@ mod tests { } use tezos_smart_rollup_host::runtime::Runtime as SdkRuntime; + use tezos_tezlink::block::TezBlock; use tezos_tezlink::operation::ManagerOperation; use tezos_tezlink::operation::Operation; use tezos_tezlink::operation::OperationContent; - pub fn make_reveal_operation( + fn make_operation( fee: u64, counter: u64, gas_limit: u64, storage_limit: u64, source: PublicKeyHash, - pk: PublicKey, + content: OperationContent, ) -> Operation { - let branch = tezos_tezlink::block::TezBlock::genesis_block_hash(); + let branch = TezBlock::genesis_block_hash(); // No need a real signature for now let signature = UnknownSignature::from_base58_check("sigSPESPpW4p44JK181SmFCFgZLVvau7wsJVN85bv5ciigMu7WSRnxs9H2NydN5ecxKHJBQTudFPrUccktoi29zHYsuzpzBX").unwrap(); Operation { @@ -668,7 +670,7 @@ mod tests { source, fee: fee.into(), counter: counter.into(), - operation: OperationContent::Reveal { pk }, + operation: content, gas_limit: gas_limit.into(), storage_limit: storage_limit.into(), }, @@ -676,6 +678,46 @@ mod tests { } } + fn make_reveal_operation( + fee: u64, + counter: u64, + gas_limit: u64, + storage_limit: u64, + source: PublicKeyHash, + pk: PublicKey, + ) -> Operation { + make_operation( + fee, + counter, + gas_limit, + storage_limit, + source, + OperationContent::Reveal { pk }, + ) + } + + fn make_transaction_operation( + fee: u64, + counter: u64, + gas_limit: u64, + storage_limit: u64, + source: PublicKeyHash, + amount: Narith, + destination: Contract, + ) -> Operation { + make_operation( + fee, + counter, + gas_limit, + storage_limit, + source, + OperationContent::Transfer { + amount, + destination, + }, + ) + } + fn blueprint(transactions: Vec) -> Blueprint { Blueprint { transactions: Transactions::EthTxs(transactions), @@ -731,6 +773,9 @@ mod tests { const DUMMY_BASE_FEE_PER_GAS: u64 = MINIMUM_BASE_FEE_PER_GAS; const DUMMY_DA_FEE: u64 = DA_FEE_PER_BYTE; + const TEZLINK_BOOTSTRAP_1: &str = "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"; + const TEZLINK_BOOTSTRAP_2: &str = "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN"; + fn dummy_evm_config(evm_configuration: Config) -> EvmChainConfig { EvmChainConfig::create_config( DUMMY_CHAIN_ID, @@ -991,20 +1036,21 @@ mod tests { let chain_config = dummy_tez_config(); let mut config = dummy_configuration(); - let contract = Contract::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") - .expect("Contract creation should have succeeded"); - let context = context::Context::from(&TEZLINK_SAFE_STORAGE_ROOT_PATH) .expect("Context creation should have succeeded"); + let contract = Contract::from_b58check(TEZLINK_BOOTSTRAP_1) + .expect("Contract creation should have succeed"); + let account = TezlinkImplicitAccount::from_contract(&context, &contract) .expect("Account interface should be correct"); + // Allocate bootstrap 1 TezlinkImplicitAccount::allocate(&mut host, &context, &contract) .expect("Contract initialization should have succeeded"); - let src = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") - .expect("PublicKeyHash b58 conversion should have succeeded"); + let src = PublicKeyHash::from_b58check(TEZLINK_BOOTSTRAP_1) + .expect("PublicKeyHash b58 conversion should have succeed"); let pk = PublicKey::from_b58check( "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", @@ -1017,11 +1063,12 @@ mod tests { assert_eq!(Manager::NotRevealed(src.clone()), manager); - let operation = make_reveal_operation(0, 1, 0, 0, src, pk.clone()); + // Reveal bootstrap 1 manager + let reveal = make_reveal_operation(0, 1, 0, 0, src, pk.clone()); store_blueprints::<_, MichelsonChainConfig>( &mut host, - vec![tezlink_blueprint(vec![operation])], + vec![tezlink_blueprint(vec![reveal])], ); produce(&mut host, &chain_config, &mut config, None, None) @@ -1038,6 +1085,114 @@ mod tests { assert_eq!(Manager::Revealed(pk), manager); } + #[test] + // Test a scenario where bootstrap 1 reveal its manager and then send mutez to bootstrap 2 + fn test_produce_tezlink_block_with_reveal_and_transfer() { + let mut host = MockKernelHost::default(); + + let chain_config = dummy_tez_config(); + let mut config = dummy_configuration(); + + let context = context::Context::from(&TEZLINK_SAFE_STORAGE_ROOT_PATH) + .expect("Context creation should have succeed"); + + let bootstrap1_contract = Contract::from_b58check(TEZLINK_BOOTSTRAP_1) + .expect("Contract creation should have succeed"); + + let mut bootstrap1 = + TezlinkImplicitAccount::from_contract(&context, &bootstrap1_contract) + .expect("Account interface should be correct"); + + // Allocate bootstrap 1 and give some mutez for a transfer + TezlinkImplicitAccount::allocate(&mut host, &context, &bootstrap1_contract) + .expect("Contract initialization should have succeed"); + + bootstrap1 + .set_balance(&mut host, &50_u64.into()) + .expect("Set balance should have suceed"); + + // Drop the mutable access to bootstrap1 + let bootstrap1 = bootstrap1; + + let manager = bootstrap1 + .manager(&host) + .expect("Retrieve manager should have succeed"); + + // Verify that bootstrap 1 is not revealed + assert!(matches!(manager, Manager::NotRevealed(_))); + + let src = PublicKeyHash::from_b58check(TEZLINK_BOOTSTRAP_1) + .expect("PublicKeyHash b58 conversion should have succeed"); + + let pk = PublicKey::from_b58check( + "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", + ) + .expect("Public key creation should have succeed"); + + // Reveal bootstrap 1 manager + let reveal = make_reveal_operation(0, 1, 0, 0, src.clone(), pk.clone()); + + // Bootstrap 1 will transfer 35 mutez to bootstrap 2 + + let bootstrap2_contract = Contract::from_b58check(TEZLINK_BOOTSTRAP_2) + .expect("Contract creation should have succeed"); + + let bootstrap2 = + TezlinkImplicitAccount::from_contract(&context, &bootstrap2_contract) + .expect("Contract creation should have succeed"); + + // Verify that bootstrap 2 is not allocated + assert!(!bootstrap2 + .allocated(&host) + .expect("Checking allocation should have succeed")); + + // Bootstrap 1 transfer 35 mutez to bootstrap 2 + let transfer = make_transaction_operation( + 0, + 1, + 0, + 0, + src.clone(), + 35_u64.into(), + bootstrap2_contract, + ); + + // Bootstrap 1 reveals its manager and then + store_blueprints::<_, MichelsonChainConfig>( + &mut host, + vec![tezlink_blueprint(vec![reveal, transfer])], + ); + + produce(&mut host, &chain_config, &mut config, None, None) + .expect("The block production should have succeeded."); + let computation = produce(&mut host, &chain_config, &mut config, None, None) + .expect("The block production should have succeeded."); + assert_eq!(ComputationResult::Finished, computation); + assert_eq!(U256::from(0), read_current_number(&host).unwrap()); + + // Bootstrap 1 should be revealed + + let manager = bootstrap1 + .manager(&host) + .expect("Retrieve manager should have succeed"); + + assert_eq!(Manager::Revealed(pk), manager); + + // Bootstrap 2 should be allocated + assert!(bootstrap2 + .allocated(&host) + .expect("Checking allocation should have succeed")); + + // Bootstrap 1 should have sent 35 mutez to Bootstrap 2 + let bootstrap1_balance = bootstrap1.balance(&host).unwrap(); + + assert_eq!(bootstrap1_balance, 15_u64.into()); + + let bootstrap2_balance = bootstrap2.balance(&host).unwrap(); + + assert_eq!(bootstrap2_balance, 35_u64.into()); + } + #[test] // Test if the invalid transactions are producing receipts fn test_invalid_transactions_receipt_status() { diff --git a/etherlink/kernel_latest/tezos/src/operation.rs b/etherlink/kernel_latest/tezos/src/operation.rs index 7e56cdcd5a0aff1c42da5e3fe4fd4f43c01e616c..a10e78cfd87dd0ddac9e3013c6499e3fa9ed8506 100644 --- a/etherlink/kernel_latest/tezos/src/operation.rs +++ b/etherlink/kernel_latest/tezos/src/operation.rs @@ -14,23 +14,32 @@ use tezos_crypto_rs::blake2b::digest_256; use tezos_crypto_rs::hash::{HashType, UnknownSignature}; use tezos_data_encoding::types::Narith; use tezos_data_encoding::{ - enc::{BinError, BinResult, BinWriter}, - nom::{error::DecodeError, NomError, NomReader, NomResult}, + enc::{self as tezos_enc, BinError, BinResult, BinWriter}, + nom::{self as tezos_nom, error::DecodeError, NomError, NomReader, NomResult}, }; -use tezos_smart_rollup::types::PublicKey; use tezos_smart_rollup::types::PublicKeyHash; +use tezos_smart_rollup::types::{Contract, PublicKey}; #[derive(PartialEq, Debug)] pub enum OperationContent { - Reveal { pk: PublicKey }, + Reveal { + pk: PublicKey, + }, + Transfer { + amount: Narith, + destination: Contract, + }, } pub const REVEAL_TAG: u8 = 107_u8; +pub const TRANSFER_TAG: u8 = 108_u8; + impl OperationContent { pub fn tag(&self) -> u8 { match self { Self::Reveal { pk: _ } => REVEAL_TAG, + Self::Transfer { .. } => TRANSFER_TAG, } } @@ -40,7 +49,21 @@ impl OperationContent { let (array, pk) = PublicKey::nom_read(bytes)?; NomResult::Ok((array, Self::Reveal { pk })) } - _ => Err(nom::Err::Error(NomError::invalid_tag( + TRANSFER_TAG => { + let (input, amount) = Narith::nom_read(bytes)?; + let (input, destination) = Contract::nom_read(input)?; + // TODO: parameter should be a Michelson expr, for now just use unit + let (input, _parameter) = + tezos_nom::optional_field(|input| Ok((input, ())))(input)?; + NomResult::Ok(( + input, + Self::Transfer { + amount, + destination, + }, + )) + } + _ => Err(nom::Err::Error(tezos_nom::NomError::invalid_tag( bytes, tag.to_string(), ))), @@ -56,6 +79,18 @@ impl BinWriter for OperationContent { pk.bin_write(data)?; Ok(()) } + Self::Transfer { + amount, + destination, + } => { + amount.bin_write(data)?; + destination.bin_write(data)?; + // TODO: parameter should be a Michelson expr, for now just use unit + let closure: for<'a> fn(&(), &'a mut Vec) -> BinResult = + |_, _| Ok(()); + tezos_enc::optional_field(closure)(&None, data)?; + Ok(()) + } } } } @@ -281,7 +316,7 @@ mod tests { hash::{HashType, UnknownSignature}, public_key::PublicKey, }; - use tezos_smart_rollup::types::PublicKeyHash; + use tezos_smart_rollup::types::{Contract, PublicKeyHash}; #[test] fn operation_rlp_roundtrip() { @@ -356,4 +391,57 @@ mod tests { assert_eq!(operation, expected_operation); } + + // The operation below is the transfer using the mockup mode of octez-client as follows: + // $ alias mockup-client='octez-client --mode mockup --base-dir /tmp/mockup --protocol PsQuebec' + // $ mockup-client create mockup + // $ TRANSFER_HEX=$(mockup-client transfer 1 from bootstrap2 to bootstrap1 --burn-cap 1 --dry-run | grep Operation: | cut -d x -f 2) + // $ octez-codec decode 021-PsQuebec.operation from "$TRANSFER_HEX" + #[test] + fn tezos_compatibility_for_transfer() { + // The goal of this test is to try to decode an encoding generated by 'octez-codec encode' command + let branch_vec = HashType::b58check_to_hash( + &HashType::BlockHash, + "BLockGenesisGenesisGenesisGenesisGenesisCCCCCeZiLHU", + ) + .unwrap(); + let branch = H256::from_slice(&branch_vec); + let signature = UnknownSignature::from_base58_check("sigT4yGRRhiMZCjGigdhopaXkshKrwDbYrPw3jGFZGkjpvpT57a6KmLa4mFVKBTNHR8NrmyMEt9Pgusac5HLqUoJie2MB5Pd").unwrap(); + let expected_operation = Operation { + branch, + content: ManagerOperation { + source: PublicKeyHash::from_b58check( + "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN", + ) + .unwrap(), + fee: 267_u64.into(), + counter: 1_u64.into(), + operation: OperationContent::Transfer { + amount: 1000000_u64.into(), + destination: Contract::from_b58check( + "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx", + ) + .unwrap(), + }, + gas_limit: 169_u64.into(), + storage_limit: 0_u64.into(), + }, + signature, + }; + + // This bytes sequence comes from the command just above the test + let operation_bytes = hex::decode("8fcf233671b6a04fcf679d2a381c2544ea6c1ea29ba6157776ed842426e5cab86c00e7670f32038107a59a2b9cfefae36ea21f5aa63c8b0201a90100c0843d000002298c03ed7d454a101eb7022bc95f7e5f41ac780026d58f30f5f8caf70878ad4efc82d71cff01b76e584958411e5a89ea2a8908e37ffc28f0af92fa651c32f6cc7362d9c735344d590360864fbf0b156c3443b108").unwrap(); + + let operation = Operation::try_from_bytes(&operation_bytes) + .expect("Decoding operation should have succeded"); + + assert_eq!(operation, expected_operation); + + // Also test the encoding + let kernel_bytes = expected_operation + .to_bytes() + .expect("Operation encoding should have succeed"); + + assert_eq!(operation_bytes, kernel_bytes) + } } diff --git a/etherlink/kernel_latest/tezos/src/operation_result.rs b/etherlink/kernel_latest/tezos/src/operation_result.rs index bc2f20ade1a92c97b8365b29b05cfb52a77f8d8e..a662560b75c9c0d7f12b6a0b945d38d172d47b86 100644 --- a/etherlink/kernel_latest/tezos/src/operation_result.rs +++ b/etherlink/kernel_latest/tezos/src/operation_result.rs @@ -6,8 +6,12 @@ /// The whole module is inspired of `src/proto_alpha/lib_protocol/apply_result.ml` to represent the result of an operation /// In Tezlink, operation is equivalent to manager operation because there is no other type of operation that interests us. +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::sequence::preceded; use std::fmt::Debug; use tezos_data_encoding::enc as tezos_enc; +use tezos_data_encoding::enc::u8; use tezos_data_encoding::nom as tezos_nom; use tezos_data_encoding::types::Narith; use tezos_data_encoding::types::Zarith; @@ -24,12 +28,98 @@ pub enum ValidityError { } #[derive(Debug, PartialEq, Eq, NomReader, BinWriter)] -pub enum ApplyOperationError { +pub enum RevealError { PreviouslyRevealedKey(PublicKey), InconsistentHash(PublicKeyHash), InconsistentPublicKey(PublicKeyHash), } +#[derive(Debug, PartialEq, Eq)] +pub enum TransferError { + BalanceTooLow { + contract: Contract, + balance: Narith, + amount: Narith, + }, + UnspendableContract(Contract), +} + +impl BinWriter for TransferError { + fn bin_write(&self, output: &mut Vec) -> tezos_enc::BinResult { + match self { + Self::BalanceTooLow { + contract, + balance, + amount, + } => { + u8(&0_u8, output)?; + contract.bin_write(output)?; + balance.bin_write(output)?; + amount.bin_write(output)?; + Ok(()) + } + Self::UnspendableContract(contract) => { + u8(&1_u8, output)?; + contract.bin_write(output)?; + Ok(()) + } + } + } +} + +impl NomReader<'_> for TransferError { + fn nom_read(input: &'_ [u8]) -> tezos_nom::NomResult<'_, Self> { + let balance_too_low_parser = preceded(tag(0_u8.to_be_bytes()), |input| { + let (input, contract) = Contract::nom_read(input)?; + let (input, balance) = Narith::nom_read(input)?; + let (input, amount) = Narith::nom_read(input)?; + Ok(( + input, + Self::BalanceTooLow { + contract, + balance, + amount, + }, + )) + }); + let unspendable_contract_parser = preceded(tag(1_u8.to_be_bytes()), |input| { + let (input, contract) = Contract::nom_read(input)?; + Ok((input, Self::UnspendableContract(contract))) + }); + alt((balance_too_low_parser, unspendable_contract_parser))(input) + } +} + +#[derive(Debug, PartialEq, Eq, NomReader, BinWriter)] +pub enum ApplyOperationError { + Reveal(RevealError), + Transfer(TransferError), +} + +impl From for ApplyOperationError { + fn from(value: RevealError) -> Self { + Self::Reveal(value) + } +} + +impl From for ApplyOperationError { + fn from(value: TransferError) -> Self { + Self::Transfer(value) + } +} + +impl From for OperationError { + fn from(value: RevealError) -> Self { + Self::Apply(value.into()) + } +} + +impl From for OperationError { + fn from(value: TransferError) -> Self { + Self::Apply(value.into()) + } +} + #[derive(Debug, PartialEq, Eq, NomReader, BinWriter)] pub enum OperationError { Validation(ValidityError), @@ -68,6 +158,46 @@ pub struct RevealContent { pk: PublicKey, } +#[derive(PartialEq, Debug, Clone)] +pub struct Empty; + +impl BinWriter for Empty { + fn bin_write(&self, _: &mut Vec) -> tezos_enc::BinResult { + Ok(()) + } +} + +impl NomReader<'_> for Empty { + fn nom_read(input: &'_ [u8]) -> tezos_nom::NomResult<'_, Self> { + Ok((input, Self)) + } +} + +#[derive(PartialEq, Debug, BinWriter, NomReader)] +pub struct TransferSuccess { + pub storage: Option, + pub lazy_storage_diff: Option, + pub balance_updates: Vec, + pub ticket_receipt: Vec, + pub originated_contracts: Vec, + pub consumed_gas: Narith, + pub storage_size: Narith, + pub paid_storage_size_diff: Narith, + pub allocated_destination_contract: bool, +} + +/// Empty struct to implement [OperationKind] trait for Transfer +#[derive(PartialEq, Debug)] +pub struct Transfer; + +impl OperationKind for Transfer { + type Success = TransferSuccess; + + fn kind() -> Self { + Self + } +} + impl OperationKind for Reveal { type Success = RevealSuccess; @@ -110,6 +240,7 @@ pub struct OperationResult { #[derive(PartialEq, Debug, NomReader, BinWriter)] pub enum OperationResultSum { Reveal(OperationResult), + Transfer(OperationResult), } pub fn produce_operation_result( diff --git a/etherlink/kernel_latest/tezos_execution/Cargo.toml b/etherlink/kernel_latest/tezos_execution/Cargo.toml index 6c6340706b4e4ce6668994054cfd7b0ad50959d6..27035bbcc73c13944987f6f51803ec21c210e04b 100644 --- a/etherlink/kernel_latest/tezos_execution/Cargo.toml +++ b/etherlink/kernel_latest/tezos_execution/Cargo.toml @@ -15,6 +15,7 @@ anyhow.workspace = true tezos_crypto_rs.workspace = true hex.workspace = true num-bigint.workspace = true +num-traits.workspace = true tezos-evm-runtime.workspace = true diff --git a/etherlink/kernel_latest/tezos_execution/src/account_storage.rs b/etherlink/kernel_latest/tezos_execution/src/account_storage.rs index 09d9dfd64f72dfe7b753ff87d794f3574b953667..34d276d6c74fa2878aa1b69db9d948e714b9f714 100644 --- a/etherlink/kernel_latest/tezos_execution/src/account_storage.rs +++ b/etherlink/kernel_latest/tezos_execution/src/account_storage.rs @@ -60,7 +60,6 @@ fn account_path(contract: &Contract) -> Result Result { + let index = context::contracts::index(context)?; + // The conversion from pkh to contract should always succeed + let contract = Contract::Implicit(pkh.clone()); + let path = concat(&index, &account_path(&contract)?)?; + Ok(path.into()) + } + /// Get the **counter** for the Tezlink account. - #[allow(dead_code)] pub fn counter( &self, host: &impl Runtime, @@ -81,7 +90,6 @@ impl TezlinkImplicitAccount { } /// Set the **counter** for the Tezlink account. - #[allow(dead_code)] pub fn set_counter( &mut self, host: &mut impl Runtime, @@ -92,7 +100,6 @@ impl TezlinkImplicitAccount { } /// Get the **balance** of an account in Mutez held by the account. - #[allow(dead_code)] pub fn balance( &self, host: &impl Runtime, @@ -102,7 +109,6 @@ impl TezlinkImplicitAccount { } /// Set the **balance** of an account in Mutez held by the account. - #[allow(dead_code)] pub fn set_balance( &mut self, host: &mut impl Runtime, @@ -112,7 +118,6 @@ impl TezlinkImplicitAccount { store_bin(balance, host, &path) } - #[allow(dead_code)] pub fn manager( &self, host: &impl Runtime, @@ -173,10 +178,10 @@ impl TezlinkImplicitAccount { host: &mut impl Runtime, context: &context::Context, contract: &Contract, - ) -> Result<(), tezos_storage::error::Error> { + ) -> Result { let mut account = Self::from_contract(context, contract)?; if account.allocated(host)? { - return Ok(()); + return Ok(true); } account.set_balance(host, &0_u64.into())?; // Only implicit accounts have counter and manager keys @@ -185,7 +190,7 @@ impl TezlinkImplicitAccount { account.set_counter(host, &0u64.into())?; account.set_manager_public_key_hash(host, pkh)?; } - Ok(()) + Ok(false) } } diff --git a/etherlink/kernel_latest/tezos_execution/src/lib.rs b/etherlink/kernel_latest/tezos_execution/src/lib.rs index 7307d103120bef302222d248370f6fbc229d05f3..c738df4bc9b45e98e97083a4324c731440e99e5b 100644 --- a/etherlink/kernel_latest/tezos_execution/src/lib.rs +++ b/etherlink/kernel_latest/tezos_execution/src/lib.rs @@ -3,16 +3,19 @@ // SPDX-License-Identifier: MIT use account_storage::{Manager, TezlinkImplicitAccount}; +use num_bigint::BigInt; +use num_traits::ops::checked::CheckedSub; use tezos_crypto_rs::{base58::FromBase58CheckError, PublicKeyWithHash}; -use tezos_data_encoding::types::Narith; +use tezos_data_encoding::types::{Narith, Zarith}; use tezos_evm_logging::{log, Level::*}; use tezos_evm_runtime::runtime::Runtime; use tezos_smart_rollup::types::{Contract, PublicKey, PublicKeyHash}; use tezos_tezlink::{ operation::{ManagerOperation, Operation, OperationContent}, operation_result::{ - produce_operation_result, ApplyOperationError, OperationError, - OperationResultSum, Reveal, RevealSuccess, ValidityError, + produce_operation_result, Balance, BalanceUpdate, OperationError, + OperationResultSum, Reveal, RevealError, RevealSuccess, TransferError, + TransferSuccess, ValidityError, }, }; use thiserror::Error; @@ -21,6 +24,8 @@ extern crate alloc; pub mod account_storage; pub mod context; +type ExecutionResult = Result, ApplyKernelError>; + #[derive(Error, Debug, PartialEq, Eq)] pub enum ApplyKernelError { #[error("Apply operation failed on a storage manipulation {0}")] @@ -79,31 +84,26 @@ fn reveal( provided_hash: &PublicKeyHash, account: &mut TezlinkImplicitAccount, public_key: &PublicKey, -) -> Result, ApplyKernelError> { +) -> ExecutionResult { log!(host, Debug, "Applying a reveal operation"); let manager = account.manager(host)?; let expected_hash = match manager { Manager::Revealed(pk) => { - return Ok(Err(ApplyOperationError::PreviouslyRevealedKey(pk).into())) + return Ok(Err(RevealError::PreviouslyRevealedKey(pk).into())) } Manager::NotRevealed(pkh) => pkh, }; // Ensure that the source of the operation is equal to the retrieved hash. if &expected_hash != provided_hash { - return Ok(Err( - ApplyOperationError::InconsistentHash(expected_hash).into() - )); + return Ok(Err(RevealError::InconsistentHash(expected_hash).into())); } // Check the public key let pkh_from_pk = public_key.pk_hash(); if expected_hash != pkh_from_pk { - return Ok(Err(ApplyOperationError::InconsistentPublicKey( - expected_hash, - ) - .into())); + return Ok(Err(RevealError::InconsistentPublicKey(expected_hash).into())); } // Set the public key as the manager @@ -116,6 +116,95 @@ fn reveal( })) } +/// Function to apply a transfer by modifying balances of both account +fn apply_transfer( + host: &mut Host, + context: &context::Context, + src: &Contract, + amount: &Narith, + dest: &Contract, +) -> ExecutionResult { + let mut source = TezlinkImplicitAccount::from_contract(context, src)?; + let src_balance = source.balance(host)?; + + let new_source_balance = match src_balance.0.checked_sub(&amount.0) { + None => { + log!(host, Debug, "Balance is too low"); + let error = TransferError::BalanceTooLow { + contract: src.clone(), + balance: src_balance, + amount: amount.clone(), + }; + return Ok(Err(error.into())); + } + Some(new_source_balance) => new_source_balance, + }; + + // Allocate the destination (does nothing if it's already allocated) + let was_allocated = TezlinkImplicitAccount::allocate(host, context, dest)?; + + let mut destination = TezlinkImplicitAccount::from_contract(context, dest)?; + + let dest_balance = destination.balance(host)?; + + let new_destination_balance = &dest_balance.0 + &amount.0; + + // Set the new balance for source and destination + source.set_balance(host, &new_source_balance.into())?; + destination.set_balance(host, &new_destination_balance.into())?; + + Ok(Ok(was_allocated)) +} + +/// Function to handle the transfer case of a manager operation +fn transfer( + host: &mut Host, + context: &context::Context, + src: &PublicKeyHash, + amount: &Narith, + dest: &Contract, +) -> ExecutionResult { + log!(host, Debug, "Applying a transfer operation"); + + let src = Contract::Implicit(src.clone()); + + // Apply the transfer and return if the destination was already allocated + let allocated_destination_contract = + match apply_transfer(host, context, &src, amount, dest)? { + Err(err) => return Ok(Err(err)), + Ok(result) => result, + }; + + // Construct the transfer result + let source_update = BigInt::from_biguint(num_bigint::Sign::Minus, amount.into()); + let dest_update = BigInt::from_biguint(num_bigint::Sign::Plus, amount.into()); + + let balance_updates = vec![ + BalanceUpdate { + balance: Balance::Account(src.clone()), + changes: Zarith(source_update), + }, + BalanceUpdate { + balance: Balance::Account(dest.clone()), + changes: Zarith(dest_update), + }, + ]; + + let success = TransferSuccess { + storage: None, + lazy_storage_diff: None, + balance_updates, + 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, + }; + + Ok(Ok(success)) +} + pub fn apply_operation( host: &mut Host, context: &context::Context, @@ -137,9 +226,7 @@ pub fn apply_operation( source ); - let contract = Contract::from_b58check(&source.to_b58check())?; - let mut account = - account_storage::TezlinkImplicitAccount::from_contract(context, &contract)?; + let mut account = TezlinkImplicitAccount::from_public_key_hash(context, source)?; log!(host, Debug, "Verifying that the operation is valid"); @@ -162,6 +249,14 @@ pub fn apply_operation( let manager_result = produce_operation_result(reveal_result); OperationResultSum::Reveal(manager_result) } + OperationContent::Transfer { + amount, + destination, + } => { + let transfer_result = transfer(host, context, source, amount, destination)?; + let manager_result = produce_operation_result(transfer_result); + OperationResultSum::Transfer(manager_result) + } }; Ok(receipt) @@ -169,15 +264,17 @@ pub fn apply_operation( #[cfg(test)] mod tests { + use num_bigint::BigInt; use tezos_crypto_rs::hash::UnknownSignature; + use tezos_data_encoding::types::{Narith, Zarith}; use tezos_evm_runtime::runtime::{MockKernelHost, Runtime}; use tezos_smart_rollup::types::{Contract, PublicKey, PublicKeyHash}; use tezos_tezlink::{ block::TezBlock, operation::{ManagerOperation, Operation, OperationContent}, operation_result::{ - ApplyOperationError, ContentResult, OperationResult, OperationResultSum, - RevealSuccess, + Balance, BalanceUpdate, ContentResult, OperationResult, OperationResultSum, + RevealError, RevealSuccess, TransferError, TransferSuccess, }, }; @@ -186,13 +283,17 @@ mod tests { apply_operation, context, OperationError, ValidityError, }; - fn make_reveal_operation( + const BOOTSTRAP_1: &str = "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"; + + const BOOTSTRAP_2: &str = "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN"; + + fn make_operation( fee: u64, counter: u64, gas_limit: u64, storage_limit: u64, source: PublicKeyHash, - pk: PublicKey, + content: OperationContent, ) -> Operation { let branch = TezBlock::genesis_block_hash(); // No need a real signature for now @@ -203,7 +304,7 @@ mod tests { source, fee: fee.into(), counter: counter.into(), - operation: OperationContent::Reveal { pk }, + operation: content, gas_limit: gas_limit.into(), storage_limit: storage_limit.into(), }, @@ -211,6 +312,46 @@ mod tests { } } + fn make_reveal_operation( + fee: u64, + counter: u64, + gas_limit: u64, + storage_limit: u64, + source: PublicKeyHash, + pk: PublicKey, + ) -> Operation { + make_operation( + fee, + counter, + gas_limit, + storage_limit, + source, + OperationContent::Reveal { pk }, + ) + } + + fn make_transfer_operation( + fee: u64, + counter: u64, + gas_limit: u64, + storage_limit: u64, + source: PublicKeyHash, + amount: Narith, + destination: Contract, + ) -> Operation { + make_operation( + fee, + counter, + gas_limit, + storage_limit, + source, + OperationContent::Transfer { + amount, + destination, + }, + ) + } + // This function setups an account that will pass the validity checks fn init_account( host: &mut impl Runtime, @@ -243,7 +384,7 @@ mod tests { fn apply_operation_empty_account() { let mut host = MockKernelHost::default(); - let src = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + let src = PublicKeyHash::from_b58check(BOOTSTRAP_1) .expect("PublicKeyHash b58 conversion should have succeed"); let pk = PublicKey::from_b58check( @@ -272,7 +413,7 @@ mod tests { fn apply_operation_cant_pay_fees() { let mut host = MockKernelHost::default(); - let src = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + let src = PublicKeyHash::from_b58check(BOOTSTRAP_1) .expect("PublicKeyHash b58 conversion should have succeed"); let _ = init_account(&mut host, &src); @@ -304,7 +445,7 @@ mod tests { fn apply_operation_invalid_counter() { let mut host = MockKernelHost::default(); - let src = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + let src = PublicKeyHash::from_b58check(BOOTSTRAP_1) .expect("PublicKeyHash b58 conversion should have succeed"); let _ = init_account(&mut host, &src); @@ -336,7 +477,7 @@ mod tests { fn apply_reveal_operation_on_already_revealed_account() { let mut host = MockKernelHost::default(); - let src = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + let src = PublicKeyHash::from_b58check(BOOTSTRAP_1) .expect("PublicKeyHash b58 conversion should have succeed"); let mut account = init_account(&mut host, &src); @@ -362,7 +503,7 @@ mod tests { let expected_receipt = OperationResultSum::Reveal(OperationResult { balance_updates: vec![], result: ContentResult::Failed(vec![OperationError::Apply( - ApplyOperationError::PreviouslyRevealedKey(pk), + RevealError::PreviouslyRevealedKey(pk).into(), )]), }); assert_eq!(receipt, expected_receipt); @@ -374,7 +515,7 @@ mod tests { fn apply_reveal_operation_with_an_inconsistent_manager() { let mut host = MockKernelHost::default(); - let src = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + let src = PublicKeyHash::from_b58check(BOOTSTRAP_1) .expect("PublicKeyHash b58 conversion should have succeed"); let mut account = init_account(&mut host, &src); @@ -402,7 +543,7 @@ mod tests { let expected_receipt = OperationResultSum::Reveal(OperationResult { balance_updates: vec![], result: ContentResult::Failed(vec![OperationError::Apply( - ApplyOperationError::InconsistentHash(inconsistent_pkh), + RevealError::InconsistentHash(inconsistent_pkh).into(), )]), }); @@ -414,7 +555,7 @@ mod tests { fn apply_reveal_operation_with_an_inconsistent_public_key() { let mut host = MockKernelHost::default(); - let src = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + let src = PublicKeyHash::from_b58check(BOOTSTRAP_1) .expect("PublicKeyHash b58 conversion should have succeed"); // Even if we don't use it we need to init the account @@ -435,7 +576,7 @@ mod tests { let expected_receipt = OperationResultSum::Reveal(OperationResult { balance_updates: vec![], result: ContentResult::Failed(vec![OperationError::Apply( - ApplyOperationError::InconsistentPublicKey(src), + RevealError::InconsistentPublicKey(src).into(), )]), }); @@ -447,7 +588,7 @@ mod tests { fn apply_reveal_operation() { let mut host = MockKernelHost::default(); - let src = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + let src = PublicKeyHash::from_b58check(BOOTSTRAP_1) .expect("PublicKeyHash b58 conversion should have succeed"); let account = init_account(&mut host, &src); @@ -484,4 +625,122 @@ mod tests { assert_eq!(manager, Manager::Revealed(pk)); } + + // Test an invalid transfer operation, source has not enough balance to fullfil the Transfer + #[test] + fn apply_transfer_with_not_enough_balance() { + let mut host = MockKernelHost::default(); + + let src = PublicKeyHash::from_b58check(BOOTSTRAP_1) + .expect("PublicKeyHash b58 conversion should have succeed"); + + let dest = PublicKeyHash::from_b58check(BOOTSTRAP_2) + .expect("PublicKeyHash b58 conversion should have succeed"); + + // Setup accounts with 50 mutez in their balance + let source = init_account(&mut host, &src); + let destination = init_account(&mut host, &dest); + + let operation = make_transfer_operation( + 15, + 1, + 4, + 5, + src, + 100_u64.into(), + Contract::Implicit(dest), + ); + + 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![], + result: ContentResult::Failed(vec![OperationError::Apply( + TransferError::BalanceTooLow { + contract: Contract::from_b58check(BOOTSTRAP_1).unwrap(), + balance: 50_u64.into(), + amount: 100_u64.into(), + } + .into(), + )]), + }); + + // Verify that source and destination balances are unchanged + assert_eq!(source.balance(&host).unwrap(), 50_u64.into()); + assert_eq!(destination.balance(&host).unwrap(), 50_u64.into()); + + assert_eq!(receipt, expected_receipt); + } + + // Bootstrap 1 successfully transfer 30 mutez to Bootstrap 2 + #[test] + fn apply_successful_transfer() { + let mut host = MockKernelHost::default(); + + let src = PublicKeyHash::from_b58check(BOOTSTRAP_1) + .expect("PublicKeyHash b58 conversion should have succeed"); + + let dest = PublicKeyHash::from_b58check(BOOTSTRAP_2) + .expect("PublicKeyHash b58 conversion should have succeed"); + + // Setup accounts with 50 mutez in their balance + let source = init_account(&mut host, &src); + let destination = init_account(&mut host, &dest); + + let operation = make_transfer_operation( + 15, + 1, + 4, + 5, + src, + 30_u64.into(), + Contract::Implicit(dest), + ); + + 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![], + result: ContentResult::Applied(TransferSuccess { + storage: None, + lazy_storage_diff: None, + balance_updates: vec![ + BalanceUpdate { + balance: Balance::Account( + Contract::from_b58check(BOOTSTRAP_1).unwrap(), + ), + changes: Zarith(BigInt::from_biguint( + num_bigint::Sign::Minus, + 30_u64.into(), + )), + }, + BalanceUpdate { + balance: Balance::Account( + Contract::from_b58check(BOOTSTRAP_2).unwrap(), + ), + changes: Zarith(BigInt::from_biguint( + num_bigint::Sign::Plus, + 30_u64.into(), + )), + }, + ], + 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: true, + }), + }); + + // Verify that source and destination balances changed + assert_eq!(source.balance(&host).unwrap(), 20_u64.into()); + assert_eq!(destination.balance(&host).unwrap(), 80_u64.into()); + + assert_eq!(receipt, expected_receipt); + } }