diff --git a/etherlink/bin_node/lib_dev/encodings/l2_types.ml b/etherlink/bin_node/lib_dev/encodings/l2_types.ml index 7b8f3acf365a3ebf391abae8d373606701e6e7da..6d4253e8d462a1cafa06d384fa4131547ab1abbc 100644 --- a/etherlink/bin_node/lib_dev/encodings/l2_types.ml +++ b/etherlink/bin_node/lib_dev/encodings/l2_types.ml @@ -48,17 +48,12 @@ module Chain_family = struct end module Tezos_block = struct - type block_without_hash = { - level : int32; - timestamp : Time.Protocol.t; - parent_hash : Ethereum_types.block_hash; - } - type t = { - level : int32; hash : Ethereum_types.block_hash; + level : int32; timestamp : Time.Protocol.t; parent_hash : Ethereum_types.block_hash; + operations : bytes; (* TODO: #7928 decode operations with receipts *) } let decode_block_hash = Ethereum_types.decode_block_hash @@ -69,7 +64,7 @@ module Tezos_block = struct Ethereum_types.Block_hash (Hex "8fcf233671b6a04fcf679d2a381c2544ea6c1ea29ba6157776ed8423e7c02934") - let block_without_hash_encoding : block_without_hash Data_encoding.t = + let block_encoding : t Data_encoding.t = let open Data_encoding in let timestamp_encoding = Time.Protocol.encoding in let block_hash_encoding = @@ -83,36 +78,26 @@ module Tezos_block = struct in def "tezlink_block" @@ conv - (fun ({level; parent_hash; timestamp} : block_without_hash) -> - (level, parent_hash, timestamp)) - (fun (level, parent_hash, timestamp) -> - {level; parent_hash; timestamp}) - (obj3 + (fun {hash; level; parent_hash; timestamp; operations} -> + (hash, level, parent_hash, timestamp, operations)) + (fun (hash, level, parent_hash, timestamp, operations) -> + {hash; level; parent_hash; timestamp; operations}) + (obj5 + (req "hash" block_hash_encoding) (req "level" int32) (req "parent_hash" block_hash_encoding) - (req "timestamp" timestamp_encoding)) + (req "timestamp" timestamp_encoding) + (req "operations" bytes)) - let () = Data_encoding.Registration.register block_without_hash_encoding + let () = Data_encoding.Registration.register block_encoding (* This function may be replaced in the future by an already existing function *) (* When Tezos block will be complete *) - let block_from_binary bytes : t = - let ({level; parent_hash; timestamp} : block_without_hash) = - Data_encoding.Binary.of_bytes_exn block_without_hash_encoding bytes - in - let block_hash = Block_hash.hash_bytes [bytes] in - let hash = - Ethereum_types.decode_block_hash (Block_hash.to_bytes block_hash) - in - {level; parent_hash; timestamp; hash} - - let encode_block ({level; parent_hash; timestamp; hash = _} : t) : - (string, string) result = - Ok - (Bytes.to_string - @@ Data_encoding.Binary.to_bytes_exn - block_without_hash_encoding - {level; parent_hash; timestamp}) + let block_from_binary bytes = + Data_encoding.Binary.of_bytes_exn block_encoding bytes + + let encode_block (block : t) : (string, string) result = + Ok (Data_encoding.Binary.to_string_exn block_encoding block) let decode_block (b : string) : (t, string) result = let b = Bytes.of_string b in diff --git a/etherlink/bin_node/lib_dev/encodings/l2_types.mli b/etherlink/bin_node/lib_dev/encodings/l2_types.mli index 3173eb34fac0bc396df898efd2d5dfdc731298cc..b13fa1225e043379ed7f3b9db73cff9927822490 100644 --- a/etherlink/bin_node/lib_dev/encodings/l2_types.mli +++ b/etherlink/bin_node/lib_dev/encodings/l2_types.mli @@ -46,10 +46,11 @@ end module Tezos_block : sig type t = { - level : int32; hash : Ethereum_types.block_hash; + level : int32; timestamp : Time.Protocol.t; parent_hash : Ethereum_types.block_hash; + operations : bytes; } val decode_block_hash : bytes -> Ethereum_types.block_hash diff --git a/etherlink/bin_node/test/test_blueprint_roundtrip.ml b/etherlink/bin_node/test/test_blueprint_roundtrip.ml index 5385a6b3202d7b04e14cf702058a5150a58e3d38..ea7ab5fcf7b613f41f805e2340bcf0e682e1db46 100644 --- a/etherlink/bin_node/test/test_blueprint_roundtrip.ml +++ b/etherlink/bin_node/test/test_blueprint_roundtrip.ml @@ -78,7 +78,14 @@ let make_blueprint ~delayed_transactions ~transactions = let make_tez_block ~level ~timestamp ~parent_hash () = let block_without_hash = - L2_types.Tezos_block.{level; hash = zero_hash; timestamp; parent_hash} + L2_types.Tezos_block. + { + level; + hash = zero_hash; + timestamp; + parent_hash; + operations = Bytes.empty; + } in let block_bytes = Bytes.of_string @@ -89,7 +96,9 @@ let make_tez_block ~level ~timestamp ~parent_hash () = let hash = Ethereum_types.decode_block_hash (Block_hash.to_bytes block_hash) in - return L2_types.Tezos_block.{level; hash; timestamp; parent_hash} + return + L2_types.Tezos_block. + {level; hash; timestamp; parent_hash; operations = Bytes.empty} let test_blueprint_roundtrip ~title ~delayed_transactions ~transactions () = register ~title:(sf "Blueprint producer decoder roundtrip (%s)" title) diff --git a/etherlink/kernel_latest/Cargo.lock b/etherlink/kernel_latest/Cargo.lock index f39004ae114dcdb1aa307b4251e18fc182830cd8..f6b4442b05f03208d9e8b5fd9a744348bca93b6d 100644 --- a/etherlink/kernel_latest/Cargo.lock +++ b/etherlink/kernel_latest/Cargo.lock @@ -780,6 +780,7 @@ dependencies = [ "getrandom 0.2.15", "hex", "libsecp256k1", + "num-bigint", "num-derive", "num-traits", "pretty_assertions", @@ -2331,6 +2332,7 @@ dependencies = [ "nom", "num-bigint", "primitive-types", + "rlp", "tezos-smart-rollup", "tezos_crypto_rs", "tezos_data_encoding", diff --git a/etherlink/kernel_latest/kernel/Cargo.toml b/etherlink/kernel_latest/kernel/Cargo.toml index ce106f29f148cf11f381a97eb0c85bd3e8ee354e..efa52a466231c98ae699615d988ab74180fa3e32 100644 --- a/etherlink/kernel_latest/kernel/Cargo.toml +++ b/etherlink/kernel_latest/kernel/Cargo.toml @@ -55,6 +55,7 @@ tezos-smart-rollup-debug.workspace = true tezos-smart-rollup-encoding.workspace = true tezos-smart-rollup-installer-config.workspace = true tezos-smart-rollup-storage.workspace = true +num-bigint.workspace = true tezos_data_encoding.workspace = true diff --git a/etherlink/kernel_latest/kernel/src/block.rs b/etherlink/kernel_latest/kernel/src/block.rs index 078f480a3ef9860cb0892fd3cbd88bb2fd9c77a4..b018027f5561220f6afae73c6c1b08df0b992eb0 100644 --- a/etherlink/kernel_latest/kernel/src/block.rs +++ b/etherlink/kernel_latest/kernel/src/block.rs @@ -601,7 +601,10 @@ mod tests { use crate::blueprint::Blueprint; use crate::blueprint_storage::store_inbox_blueprint; use crate::blueprint_storage::store_inbox_blueprint_by_number; - use crate::chains::{EvmChainConfig, MichelsonChainConfig, TezTransactions}; + use crate::chains::{ + EvmChainConfig, MichelsonChainConfig, TezTransactions, + TEZLINK_SAFE_STORAGE_ROOT_PATH, + }; use crate::fees::DA_FEE_PER_BYTE; use crate::fees::MINIMUM_BASE_FEE_PER_GAS; use crate::storage::read_block_in_progress; @@ -620,6 +623,7 @@ mod tests { use evm_execution::precompiles::precompile_set; use primitive_types::{H160, U256}; use std::str::FromStr; + use tezos_crypto_rs::hash::UnknownSignature; use tezos_ethereum::block::BlockFees; use tezos_ethereum::transaction::{ TransactionHash, TransactionStatus, TransactionType, TRANSACTION_HASH_SIZE, @@ -628,6 +632,12 @@ mod tests { use tezos_evm_runtime::extensions::WithGas; use tezos_evm_runtime::runtime::MockKernelHost; use tezos_evm_runtime::runtime::Runtime; + use tezos_execution::account_storage::Manager; + use tezos_execution::account_storage::TezlinkImplicitAccount; + use tezos_execution::context; + use tezos_smart_rollup::types::Contract; + use tezos_smart_rollup::types::PublicKey; + use tezos_smart_rollup::types::PublicKeyHash; use tezos_smart_rollup_encoding::timestamp::Timestamp; use tezos_smart_rollup_host::path::concat; use tezos_smart_rollup_host::path::RefPath; @@ -637,6 +647,34 @@ mod tests { } use tezos_smart_rollup_host::runtime::Runtime as SdkRuntime; + use tezos_tezlink::operation::ManagerOperation; + use tezos_tezlink::operation::Operation; + use tezos_tezlink::operation::OperationContent; + + pub fn make_reveal_operation( + fee: u64, + counter: u64, + gas_limit: u64, + storage_limit: u64, + source: PublicKeyHash, + pk: PublicKey, + ) -> Operation { + let branch = tezos_tezlink::block::TezBlock::genesis_block_hash(); + // No need a real signature for now + let signature = UnknownSignature::from_base58_check("sigSPESPpW4p44JK181SmFCFgZLVvau7wsJVN85bv5ciigMu7WSRnxs9H2NydN5ecxKHJBQTudFPrUccktoi29zHYsuzpzBX").unwrap(); + Operation { + branch, + content: ManagerOperation { + source, + fee: fee.into(), + counter: counter.into(), + operation: OperationContent::Reveal { pk }, + gas_limit: gas_limit.into(), + storage_limit: storage_limit.into(), + }, + signature, + } + } fn blueprint(transactions: Vec) -> Blueprint { Blueprint { @@ -645,9 +683,9 @@ mod tests { } } - fn tezlink_blueprint() -> Blueprint { + fn tezlink_blueprint(operations: Vec) -> Blueprint { Blueprint { - transactions: TezTransactions {}, + transactions: TezTransactions(operations), timestamp: Timestamp::from(0i64), } } @@ -927,9 +965,9 @@ mod tests { store_blueprints::<_, MichelsonChainConfig>( &mut host, vec![ - tezlink_blueprint(), - tezlink_blueprint(), - tezlink_blueprint(), + tezlink_blueprint(vec![]), + tezlink_blueprint(vec![]), + tezlink_blueprint(vec![]), ], ); @@ -945,6 +983,61 @@ mod tests { assert_eq!(U256::from(2), read_current_number(&host).unwrap()); } + #[test] + // Test if tezlink block production works with a reveal operation + fn test_produce_tezlink_block_with_reveal_operation() { + let mut host = MockKernelHost::default(); + + 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 account = TezlinkImplicitAccount::from_contract(&context, &contract) + .expect("Account interface should be correct"); + + 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 pk = PublicKey::from_b58check( + "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", + ) + .expect("Public key creation should have succeeded"); + + let manager = account + .manager(&host) + .expect("Retrieve manager should have succeeded"); + + assert_eq!(Manager::NotRevealed(src.clone()), manager); + + let operation = make_reveal_operation(0, 1, 0, 0, src, pk.clone()); + + store_blueprints::<_, MichelsonChainConfig>( + &mut host, + vec![tezlink_blueprint(vec![operation])], + ); + + 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()); + + let manager = account + .manager(&host) + .expect("Retrieve manager should have succeeded"); + + assert_eq!(Manager::Revealed(pk), manager); + } + #[test] // Test if the invalid transactions are producing receipts fn test_invalid_transactions_receipt_status() { diff --git a/etherlink/kernel_latest/kernel/src/block_storage.rs b/etherlink/kernel_latest/kernel/src/block_storage.rs index 1544192cb693641d141c97f804e84b73ce798988..e6b4171c79252b69ddaf6d239e7b14bbbfeae3ef 100644 --- a/etherlink/kernel_latest/kernel/src/block_storage.rs +++ b/etherlink/kernel_latest/kernel/src/block_storage.rs @@ -92,7 +92,7 @@ fn store_block( index.push_value(host, block.hash().as_bytes())?; } let path = path::path(root, block.hash())?; - let bytes = block.to_bytes(); + let bytes = block.to_bytes()?; Ok(host.store_write_all(&path, &bytes)?) } diff --git a/etherlink/kernel_latest/kernel/src/chains.rs b/etherlink/kernel_latest/kernel/src/chains.rs index b143e55d0011d0f01c0cb02a4b5db8d789906025..60b26ee09bb421cc15e5c21a969b2567cadc8418 100644 --- a/etherlink/kernel_latest/kernel/src/chains.rs +++ b/etherlink/kernel_latest/kernel/src/chains.rs @@ -10,6 +10,7 @@ use crate::{ DelayedTransactionFetchingResult, EVMBlockHeader, TezBlockHeader, }, delayed_inbox::DelayedInbox, + error, fees::MINIMUM_BASE_FEE_PER_GAS, l2block::L2Block, simulation::start_simulation_mode, @@ -25,12 +26,19 @@ use evm_execution::{ }; use primitive_types::{H160, H256, U256}; use rlp::{Decodable, Encodable}; -use std::fmt::{Debug, Display}; +use std::{ + collections::VecDeque, + fmt::{Debug, Display}, +}; use tezos_evm_logging::{log, Level::*}; use tezos_evm_runtime::runtime::Runtime; +use tezos_execution::context; use tezos_smart_rollup::{outbox::OutboxQueue, types::Timestamp}; use tezos_smart_rollup_host::path::{Path, RefPath}; -use tezos_tezlink::block::TezBlock; +use tezos_tezlink::{ + block::{AppliedOperation, TezBlock}, + operation::Operation, +}; pub const ETHERLINK_SAFE_STORAGE_ROOT_PATH: RefPath = RefPath::assert_from(b"/evm/world_state"); @@ -85,6 +93,8 @@ pub struct TezBlockInProgress { number: U256, timestamp: Timestamp, previous_hash: H256, + applied: Vec, + operations: VecDeque, } impl BlockInProgressTrait for TezBlockInProgress { @@ -116,26 +126,44 @@ impl TransactionsTrait for crate::transaction::Transactions { } #[derive(Debug)] -pub struct TezTransactions {} +pub struct TezTransactions(pub Vec); impl TransactionsTrait for TezTransactions { - fn extend(&mut self, _: Self) {} + fn extend(&mut self, other: Self) { + let TezTransactions(ref mut ops) = self; + let TezTransactions(other) = other; + ops.extend(other) + } fn number_of_txs(&self) -> usize { - 0 + let TezTransactions(operations) = self; + operations.len() } } impl Encodable for TezTransactions { fn rlp_append(&self, stream: &mut rlp::RlpStream) { - let Self {} = self; - stream.begin_list(0); + let Self(operations) = self; + stream.begin_list(operations.len()); + for op in operations { + // We don't want the kernel to panic if there's an error + // and we can't print a log as we don't have access to + // the host. So we just ignore the result. + let _ = op.rlp_append(stream); + } } } impl Decodable for TezTransactions { - fn decode(_decoder: &rlp::Rlp) -> Result { - Ok(Self {}) + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(rlp::DecoderError::RlpExpectedToBeList); + } + let operations = decoder + .iter() + .map(|rlp| Operation::decode(&rlp)) + .collect::, rlp::DecoderError>>()?; + Ok(TezTransactions(operations)) } } @@ -398,10 +426,13 @@ impl ChainConfigTrait for MichelsonChainConfig { header: Self::ChainHeader, blueprint: Blueprint, ) -> Self::BlockInProgress { + let TezTransactions(operations) = blueprint.transactions; TezBlockInProgress { number: current_block_number, timestamp: blueprint.timestamp, previous_hash: header.hash, + applied: vec![], + operations: VecDeque::from(operations), } } @@ -413,15 +444,23 @@ impl ChainConfigTrait for MichelsonChainConfig { ) -> anyhow::Result<(DelayedTransactionFetchingResult, usize)> { Ok(( - DelayedTransactionFetchingResult::Ok(TezTransactions {}), + DelayedTransactionFetchingResult::Ok(TezTransactions(vec![])), current_blueprint_size, )) } fn transactions_from_bytes( - _bytes: Vec>, + bytes: Vec>, ) -> anyhow::Result { - Ok(TezTransactions {}) + let operations = bytes + .iter() + .map(|bytes| { + Operation::try_from_bytes(bytes).map_err(|decode_error| { + error::Error::NomReadError(format!("{:?}", decode_error)) + }) + }) + .collect::, error::Error>>()?; + Ok(TezTransactions(operations)) } fn read_block_in_progress( @@ -447,6 +486,8 @@ impl ChainConfigTrait for MichelsonChainConfig { number, timestamp, previous_hash, + mut applied, + mut operations, } = block_in_progress; log!( host, @@ -455,7 +496,31 @@ impl ChainConfigTrait for MichelsonChainConfig { number ); - let tezblock = TezBlock::new(number, timestamp, previous_hash); + let context = context::Context::from(&self.storage_root_path())?; + + // Compute operations that are in the block in progress + while !operations.is_empty() { + // Retrieve the next operation in the VecDequeue + let operation = operations.pop_front().ok_or(error::Error::Reboot)?; + + // Try to apply the operation with the tezos_execution crate, return a receipt + // on whether it failed or not + let receipt = tezos_execution::apply_operation(host, &context, &operation)?; + + // Compute the hash of the operation + let hash = operation.hash()?; + + // Add the applied operation in the block in progress + let applied_operation = AppliedOperation { + hash, + data: operation, + receipt, + }; + applied.push(applied_operation); + } + + // Create a Tezos block from the block in progess + let tezblock = TezBlock::new(number, timestamp, previous_hash, applied)?; let new_block = L2Block::Tezlink(tezblock); let root = self.storage_root_path(); crate::block_storage::store_current(host, &root, &new_block) diff --git a/etherlink/kernel_latest/kernel/src/error.rs b/etherlink/kernel_latest/kernel/src/error.rs index 3684320a3ced52e58ed2547984d1032df0e390fe..84bcb3b1bcfd1bfcca77f67e3cccdbb01c6c69fe 100644 --- a/etherlink/kernel_latest/kernel/src/error.rs +++ b/etherlink/kernel_latest/kernel/src/error.rs @@ -11,6 +11,7 @@ use primitive_types::U256; use rlp::DecoderError; use tezos_data_encoding::enc::BinError; use tezos_ethereum::tx_common::SigError; +use tezos_execution::ApplyKernelError; use tezos_indexable_storage::IndexableStorageError; use tezos_smart_rollup_encoding::entrypoint::EntrypointError; use tezos_smart_rollup_encoding::michelson::ticket::TicketError; @@ -90,6 +91,8 @@ pub enum Error { #[error(transparent)] Transfer(TransferError), #[error(transparent)] + Operation(ApplyKernelError), + #[error(transparent)] Storage(StorageError), #[error("Invalid conversion")] InvalidConversion, diff --git a/etherlink/kernel_latest/kernel/src/l2block.rs b/etherlink/kernel_latest/kernel/src/l2block.rs index c5dd2011726a581eb0c6563af60a4628c934ca91..c53f0b706f1152242806d9f4907490c7e5222733 100644 --- a/etherlink/kernel_latest/kernel/src/l2block.rs +++ b/etherlink/kernel_latest/kernel/src/l2block.rs @@ -37,7 +37,7 @@ impl L2Block { pub fn number_of_transactions(&self) -> usize { match &self { Self::Etherlink(block) => block.transactions.len(), - Self::Tezlink(_) => 0, + Self::Tezlink(block) => block.operations.len(), } } @@ -62,10 +62,10 @@ impl L2Block { } } - pub fn to_bytes(&self) -> Vec { + pub fn to_bytes(&self) -> anyhow::Result> { match self { - Self::Etherlink(block) => block.to_bytes(), - Self::Tezlink(block) => block.to_bytes(), + Self::Etherlink(block) => Ok(block.to_bytes()), + Self::Tezlink(block) => Ok(block.to_bytes()?), } } diff --git a/etherlink/kernel_latest/tezos/Cargo.toml b/etherlink/kernel_latest/tezos/Cargo.toml index e3c64fdb22972ee1a5b76a728b28150bbfaff2d4..6c75b9dc52a4bb8348f74c4b26e6e379ec9a0be2 100644 --- a/etherlink/kernel_latest/tezos/Cargo.toml +++ b/etherlink/kernel_latest/tezos/Cargo.toml @@ -18,3 +18,5 @@ tezos-smart-rollup.workspace = true num-bigint.workspace = true tezos_data_encoding.workspace = true nom.workspace = true + +rlp.workspace = true diff --git a/etherlink/kernel_latest/tezos/src/block.rs b/etherlink/kernel_latest/tezos/src/block.rs index 2390ae6bbc1c0938750a1eb4924b60b6789da8e8..5f4b3b3c5dc39925ae15740d15d1564f3725010f 100644 --- a/etherlink/kernel_latest/tezos/src/block.rs +++ b/etherlink/kernel_latest/tezos/src/block.rs @@ -2,107 +2,234 @@ // // SPDX-License-Identifier: MIT +use crate::{operation::Operation, operation_result::OperationResultSum}; +use nom::bytes::complete::take; +use nom::combinator::map; +use nom::error::ParseError; +use nom::Finish; use primitive_types::{H256, U256}; -use std::{array::TryFromSliceError, str::FromStr}; use tezos_crypto_rs::blake2b::digest_256; +use tezos_crypto_rs::hash::HashType; +use tezos_data_encoding::enc as tezos_enc; +use tezos_data_encoding::nom as tezos_nom; +use tezos_data_encoding::nom::error::DecodeError; +use tezos_data_encoding::nom::NomError; +use tezos_enc::{BinError, BinWriter}; +use tezos_nom::{NomReader, NomResult}; use tezos_smart_rollup::types::Timestamp; +#[derive(PartialEq, Debug)] +pub struct AppliedOperation { + // OperationHash are 32 bytes long + pub hash: H256, + pub data: Operation, + pub receipt: OperationResultSum, +} + +impl NomReader<'_> for AppliedOperation { + fn nom_read(input: &'_ [u8]) -> NomResult<'_, Self> { + // OperationHash are 32 bytes long + let size = HashType::OperationHash.size(); + let (input, hash) = + map(take::(size), H256::from_slice)(input)?; + let (input, operation) = Operation::nom_read(input)?; + let (input, receipt) = OperationResultSum::nom_read(input)?; + let applied_op = Self { + hash, + data: operation, + receipt, + }; + Ok((input, applied_op)) + } +} + +impl BinWriter for AppliedOperation { + fn bin_write(&self, output: &mut Vec) -> tezos_enc::BinResult { + let Self { + hash, + data, + receipt, + } = self; + tezos_enc::put_bytes(hash.as_bytes(), output); + data.bin_write(output)?; + receipt.bin_write(output)?; + Ok(()) + } +} + // WIP: This structure will evolve to look like Tezos block #[derive(PartialEq, Debug)] pub struct TezBlock { - pub number: U256, pub hash: H256, + pub number: U256, pub timestamp: Timestamp, pub previous_hash: H256, + pub operations: Vec, +} + +impl NomReader<'_> for TezBlock { + fn nom_read(input: &'_ [u8]) -> NomResult<'_, Self> { + let (remaining, hash) = + map(take::(32_usize), H256::from_slice)(input)?; + + let (remaining, number) = nom::number::complete::be_u32(remaining)?; + let number = U256::from(number); + + let (remaining, previous_hash) = + map(take::(32_usize), H256::from_slice)(remaining)?; + + // Decode the timestamp + let (remaining, timestamp) = nom::number::complete::be_i64(remaining)?; + let timestamp = Timestamp::from(timestamp); + + let (remaining, operations) = + tezos_nom::dynamic(tezos_nom::list(AppliedOperation::nom_read))(remaining)?; + + Ok(( + remaining, + Self { + hash, + number, + timestamp, + previous_hash, + operations, + }, + )) + } +} + +impl BinWriter for TezBlock { + // Encoded size for parameter were taken from this command: + // `octez-codec describe block_header binary schema` + fn bin_write(&self, output: &mut Vec) -> Result<(), BinError> { + let Self { + hash, + number, + timestamp, + previous_hash, + operations, + } = self; + // Encode all block fields + tezos_enc::put_bytes(&hash.to_fixed_bytes(), output); + tezos_enc::u32(&number.as_u32(), output)?; + tezos_enc::put_bytes(&previous_hash.to_fixed_bytes(), output); + tezos_enc::i64(×tamp.i64(), output)?; + tezos_enc::dynamic(tezos_enc::list(AppliedOperation::bin_write))( + operations, output, + )?; + Ok(()) + } } impl TezBlock { pub fn genesis_block_hash() -> H256 { // This H256 comes from this b58 hash 'BLockGenesisGenesisGenesisGenesisGenesis1db77eJNeJ9' // That is the ghostnet genesis hash according to 'devtools/get_contracts/config.ml' - H256::from_str("8fcf233671b6a04fcf679d2a381c2544ea6c1ea29ba6157776ed8423e7c02934") - .unwrap() + H256::from_slice( + &hex::decode( + "8fcf233671b6a04fcf679d2a381c2544ea6c1ea29ba6157776ed8423e7c02934", + ) + .unwrap(), + ) } - fn hash(&self) -> H256 { - let encoded_data = self.to_bytes(); + // This function must be used on a TezBlock whose hash field is H256::zero() + fn hash(&self) -> Result { + let mut encoded_data = vec![]; + self.bin_write(&mut encoded_data)?; let hashed_data = digest_256(&encoded_data); - H256::from_slice(&hashed_data) + Ok(H256::from_slice(&hashed_data)) } - pub fn new(number: U256, timestamp: Timestamp, previous_hash: H256) -> Self { + pub fn new( + number: U256, + timestamp: Timestamp, + previous_hash: H256, + operations: Vec, + ) -> Result { let block = Self { hash: H256::zero(), number, timestamp, previous_hash, + operations, }; - Self { - hash: block.hash(), + Ok(Self { + hash: block.hash()?, ..block - } + }) } - // Encoded size for parameter were taken from this command: - // `octez-codec describe block_header binary schema` - pub fn to_bytes(&self) -> Vec { - let Self { - hash: _, - number, - timestamp, - previous_hash, - } = self; - let mut data = vec![]; - - // Encode all block fields - let num_enc: [u8; 4] = number.as_u32().to_be_bytes(); - let predecessor: [u8; 32] = previous_hash.to_fixed_bytes(); - let time_enc: [u8; 8] = timestamp.i64().to_be_bytes(); - - // Append encoded fields to data - data.extend_from_slice(&num_enc); - data.extend_from_slice(&predecessor); - data.extend_from_slice(&time_enc); - - data + pub fn to_bytes(&self) -> Result, BinError> { + let mut output = vec![]; + self.bin_write(&mut output)?; + Ok(output) } - pub fn try_from_bytes(bytes: &[u8]) -> Result { - let number = U256::from_big_endian(&bytes[0..4]); - - let previous_hash = H256::from_slice(&bytes[4..36]); - - // Decode the timestamp - let timestamp_array: [u8; 8] = bytes[36..44].try_into()?; - let timestamp = Timestamp::from(i64::from_be_bytes(timestamp_array)); - - Ok(TezBlock::new(number, timestamp, previous_hash)) + pub fn try_from_bytes(bytes: &[u8]) -> Result> { + let (remaining, block) = Self::nom_read(bytes).finish()?; + if !remaining.is_empty() { + return Err(DecodeError::from_error_kind( + remaining, + nom::error::ErrorKind::NonEmpty, + )); + } + Ok(block) } } #[cfg(test)] mod tests { - use primitive_types::U256; + use primitive_types::{H256, U256}; use tezos_smart_rollup::types::Timestamp; - use super::TezBlock; + use crate::operation_result::{OperationResult, OperationResultSum, RevealSuccess}; + + use super::{AppliedOperation, TezBlock}; pub fn block_roundtrip(block: TezBlock) { - let bytes = block.to_bytes(); + let bytes = block + .to_bytes() + .expect("Block encoding should have succeeded"); let decoded_block = TezBlock::try_from_bytes(&bytes).expect("Block should be decodable"); assert_eq!(block, decoded_block, "Roundtrip failed on {:?}", block) } - fn dummy_tezblock() -> TezBlock { + fn dummy_applied_operation() -> AppliedOperation { + let hash = H256::random(); + let data = crate::operation::make_dummy_reveal_operation(); + let receipt = OperationResultSum::Reveal(OperationResult { + balance_updates: vec![], + result: crate::operation_result::ContentResult::Applied(RevealSuccess { + consumed_gas: 0u64.into(), + }), + }); + AppliedOperation { + hash, + data, + receipt, + } + } + + fn dummy_tezblock(operations: Vec) -> TezBlock { let number = U256::one(); let timestamp = Timestamp::from(0); let previous_hash = TezBlock::genesis_block_hash(); - TezBlock::new(number, timestamp, previous_hash) + TezBlock::new(number, timestamp, previous_hash, operations) + .expect("Block creation should have succeeded") + } + + #[test] + fn test_empty_block_rlp_roundtrip() { + block_roundtrip(dummy_tezblock(vec![])); } #[test] fn test_block_rlp_roundtrip() { - block_roundtrip(dummy_tezblock()); + block_roundtrip(dummy_tezblock(vec![ + dummy_applied_operation(), + dummy_applied_operation(), + ])); } } diff --git a/etherlink/kernel_latest/tezos/src/operation.rs b/etherlink/kernel_latest/tezos/src/operation.rs index 8da026ad6fbdd5c01d9d802b27395f9e34316b84..7e56cdcd5a0aff1c42da5e3fe4fd4f43c01e616c 100644 --- a/etherlink/kernel_latest/tezos/src/operation.rs +++ b/etherlink/kernel_latest/tezos/src/operation.rs @@ -9,6 +9,8 @@ use nom::combinator::map; use nom::error::{ErrorKind, ParseError}; use nom::{bytes::complete::take, Finish}; use primitive_types::H256; +use rlp::Decodable; +use tezos_crypto_rs::blake2b::digest_256; use tezos_crypto_rs::hash::{HashType, UnknownSignature}; use tezos_data_encoding::types::Narith; use tezos_data_encoding::{ @@ -205,50 +207,94 @@ impl Operation { } } +impl Operation { + // The `rlp_append` function from the Encodable trait can't fail but `to_bytes` + // return a result. To avoid unwraping and risk a panic we're not implementing + // the trait exactly, but we expose a serialization function. + pub fn rlp_append(&self, s: &mut rlp::RlpStream) -> Result<(), BinError> { + let bytes = self.to_bytes()?; + s.append(&bytes); + Ok(()) + } + + pub fn hash(&self) -> Result { + let serialized_op = self.to_bytes()?; + let op_hash = digest_256(&serialized_op); + Ok(H256::from_slice(&op_hash)) + } +} + +impl Decodable for Operation { + fn decode(rlp: &rlp::Rlp) -> Result { + let raw: Vec = rlp.as_val()?; + Operation::try_from_bytes(&raw) + .map_err(|_| rlp::DecoderError::Custom("Operation::try_from_bytes failed")) + } +} + +#[cfg(test)] +fn make_dummy_operation( + operation: OperationContent, + signature: UnknownSignature, +) -> Operation { + use crate::block::TezBlock; + + let branch = TezBlock::genesis_block_hash(); + + // Public key hash in b58 for 0002298c03ed7d454a101eb7022bc95f7e5f41ac78 + let source = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + .expect("Public key hash conversion should succeeded"); + + Operation { + branch, + content: ManagerOperation { + source, + fee: 1_u64.into(), + counter: 10_u64.into(), + gas_limit: 68_u64.into(), + storage_limit: 45_u64.into(), + operation, + }, + signature, + } +} + +#[cfg(test)] +pub fn make_dummy_reveal_operation() -> Operation { + let pk = PublicKey::from_b58check( + "edpkuT1qccDweCHnvgjLuNUHERpZmEaFZfbWvTzj2BxmTgQBZjaDFD", + ) + .expect("Public key creation should have succeeded"); + + let signature = UnknownSignature::from_base58_check("sigSPESPpW4p44JK181SmFCFgZLVvau7wsJVN85bv5ciigMu7WSRnxs9H2NydN5ecxKHJBQTudFPrUccktoi29zHYsuzpzBX").unwrap(); + + make_dummy_operation(OperationContent::Reveal { pk }, signature) +} + #[cfg(test)] mod tests { use super::{ManagerOperation, Operation, OperationContent}; - use crate::block::TezBlock; + use crate::operation::make_dummy_reveal_operation; use primitive_types::H256; + use rlp::{Decodable, Rlp, RlpStream}; use tezos_crypto_rs::{ hash::{HashType, UnknownSignature}, public_key::PublicKey, }; use tezos_smart_rollup::types::PublicKeyHash; - fn make_dummy_operation( - operation: OperationContent, - signature: UnknownSignature, - ) -> Operation { - let branch = TezBlock::genesis_block_hash(); - - // Public key hash in b58 for 0002298c03ed7d454a101eb7022bc95f7e5f41ac78 - let source = PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") - .expect("Public key hash conversion should succeed"); - - Operation { - branch, - content: ManagerOperation { - source, - fee: 1_u64.into(), - counter: 10_u64.into(), - gas_limit: 68_u64.into(), - storage_limit: 45_u64.into(), - operation, - }, - signature, - } - } - - fn make_dummy_reveal_operation() -> Operation { - let pk = PublicKey::from_b58check( - "edpkuT1qccDweCHnvgjLuNUHERpZmEaFZfbWvTzj2BxmTgQBZjaDFD", - ) - .expect("Public key creation should have succeed"); - - let signature = UnknownSignature::from_base58_check("sigSPESPpW4p44JK181SmFCFgZLVvau7wsJVN85bv5ciigMu7WSRnxs9H2NydN5ecxKHJBQTudFPrUccktoi29zHYsuzpzBX").unwrap(); - - make_dummy_operation(OperationContent::Reveal { pk }, signature) + #[test] + fn operation_rlp_roundtrip() { + let operation = make_dummy_reveal_operation(); + let mut stream = RlpStream::new(); + operation + .rlp_append(&mut stream) + .expect("rlp_append should have succeeded"); + let bytes = stream.as_raw(); + let rlp = Rlp::new(bytes); + let decoded_operation = + Operation::decode(&rlp).expect("Decoding operation should have succeeded"); + assert_eq!(operation, decoded_operation); } #[test] @@ -257,9 +303,9 @@ mod tests { let bytes = operation .to_bytes() - .expect("Encoding reveal operation should have succeed"); + .expect("Encoding reveal operation should have succeeded"); let operation_from_bytes = Operation::try_from_bytes(&bytes) - .expect("Decoding reveal operation should have succeed"); + .expect("Decoding reveal operation should have succeeded"); assert_eq!(operation, operation_from_bytes); }