diff --git a/etherlink/kernel_latest/Cargo.lock b/etherlink/kernel_latest/Cargo.lock index fb24c1f98942ce4abc5fd3ff2ccfa15b82f65f16..e5e4336ec542757369321ef8e8ca52628126bbd3 100644 --- a/etherlink/kernel_latest/Cargo.lock +++ b/etherlink/kernel_latest/Cargo.lock @@ -2471,9 +2471,11 @@ dependencies = [ name = "tezos_tezlink_latest" version = "0.1.0" dependencies = [ + "hex", "primitive-types", "rlp", "tezos-smart-rollup", + "tezos_crypto_rs", "tezos_ethereum_latest", ] diff --git a/etherlink/kernel_latest/kernel/src/block.rs b/etherlink/kernel_latest/kernel/src/block.rs index b1a3648e02bfe7ce25f9e376f7a7c8a3c0deac2c..1baa7fd9147daafc429499c2b2bb134415b8e0d0 100644 --- a/etherlink/kernel_latest/kernel/src/block.rs +++ b/etherlink/kernel_latest/kernel/src/block.rs @@ -18,12 +18,12 @@ use crate::delayed_inbox::DelayedInbox; use crate::error::Error; use crate::event::Event; use crate::l2block::L2Block; -use crate::storage; use crate::transaction::Transaction; use crate::upgrade; use crate::upgrade::KernelUpgrade; use crate::Configuration; use crate::{block_in_progress, tick_model}; +use crate::{block_storage, storage}; use anyhow::Context; use block_in_progress::EthBlockInProgress; use evm::Config; @@ -249,14 +249,18 @@ fn compute( #[allow(clippy::large_enum_variant)] pub enum BlockInProgress { Etherlink(EthBlockInProgress), - Tezlink(U256, Timestamp), + Tezlink { + number: U256, + timestamp: Timestamp, + previous_hash: H256, + }, } impl BlockInProgress { pub fn number(&self) -> U256 { match self { Self::Etherlink(bip) => bip.number, - Self::Tezlink(n, _) => *n, + Self::Tezlink { number, .. } => *number, } } } @@ -330,11 +334,12 @@ fn next_bip_from_blueprints( BlockInProgress::Etherlink(bip), ))) } - (ChainConfig::Michelson(_), ChainHeader::Tez(_)) => { - Ok(BlueprintParsing::Next(Box::new(BlockInProgress::Tezlink( - next_bip_number, - blueprint.timestamp, - )))) + (ChainConfig::Michelson(_), ChainHeader::Tez(header)) => { + Ok(BlueprintParsing::Next(Box::new(BlockInProgress::Tezlink { + number: next_bip_number, + timestamp: blueprint.timestamp, + previous_hash: header.hash, + }))) } (_, _) => { log!( @@ -411,7 +416,7 @@ fn compute_bip( .context("Failed to finalize the block in progress")?; Ok(BlockComputationResult::Finished { included_delayed_transactions, - block: L2Block::Etherlink(Box::new(new_block)), + block: new_block, }) } } @@ -580,21 +585,28 @@ pub fn produce( &chain_config.evm_config, ) } - (ChainConfig::Michelson(_), BlockInProgress::Tezlink(number, timestamp)) => { + ( + ChainConfig::Michelson(_), + BlockInProgress::Tezlink { + number, + timestamp, + previous_hash, + }, + ) => { log!( safe_host, Debug, "Computing the BlockInProgress for Tezlink at level {}", number ); + + let tezblock = TezBlock::new(number, timestamp, previous_hash); + let new_block = L2Block::Tezlink(tezblock); + block_storage::store_current(&mut safe_host, &new_block) + .context("Failed to store the current block")?; Ok(BlockComputationResult::Finished { included_delayed_transactions: vec![], - block: L2Block::Tezlink(TezBlock { - number, - hash: TezBlock::genesis_block_hash(), - timestamp, - previous_hash: TezBlock::genesis_block_hash(), - }), + block: new_block, }) } (_, _) => { @@ -663,10 +675,12 @@ pub fn produce( mod tests { use super::*; use crate::block_storage; + use crate::block_storage::read_current_number; use crate::blueprint::Blueprint; use crate::blueprint_storage::read_next_blueprint; use crate::blueprint_storage::store_inbox_blueprint; use crate::blueprint_storage::store_inbox_blueprint_by_number; + use crate::chains::ChainFamily; use crate::chains::MichelsonChainConfig; use crate::fees::DA_FEE_PER_BYTE; use crate::fees::MINIMUM_BASE_FEE_PER_GAS; @@ -949,7 +963,7 @@ mod tests { } fn assert_current_block_reading_validity(host: &mut Host) { - match block_storage::read_current(host) { + match block_storage::read_current(host, &ChainFamily::Evm) { Ok(_) => (), Err(e) => { panic!("Block reading failed: {:?}\n", e) @@ -958,18 +972,31 @@ mod tests { } #[test] - // Test if tezlink block production doesn't panic + // Test if tezlink block production works fn test_produce_tezlink_block() { let mut host = MockKernelHost::default(); let mut config = dummy_tez_configuration(); - store_blueprints(&mut host, vec![tezlink_blueprint()]); + store_blueprints( + &mut host, + vec![ + tezlink_blueprint(), + tezlink_blueprint(), + tezlink_blueprint(), + ], + ); + produce(&mut host, &mut config, None, None) + .expect("The block production should have succeeded."); + produce(&mut host, &mut config, None, None) + .expect("The block production should have succeeded."); + produce(&mut host, &mut config, None, None) + .expect("The block production should have succeeded."); let computation = produce(&mut host, &mut config, None, None) - .expect("The block production failed."); - - assert_eq!(computation, ComputationResult::RebootNeeded) + .expect("The block production should have succeeded."); + assert_eq!(ComputationResult::Finished, computation); + assert_eq!(U256::from(2), read_current_number(&host).unwrap()); } #[test] @@ -1351,11 +1378,12 @@ mod tests { let new_number_of_blocks_indexed = blocks_index.length(&host).unwrap(); - let current_block_hash = block_storage::read_current(&mut host) - .unwrap() - .hash - .as_bytes() - .to_vec(); + let current_block_hash = + block_storage::read_current(&mut host, &ChainFamily::Evm) + .unwrap() + .hash() + .as_bytes() + .to_vec(); assert_eq!(number_of_blocks_indexed + 1, new_number_of_blocks_indexed); @@ -1954,12 +1982,10 @@ mod tests { produce(&mut host, &mut configuration, None, None).expect("Should have produced"); - let block = - block_storage::read_current(&mut host).expect("Should have found a block"); - let failed_loop_hash = block - .transactions - .first() - .expect("There should have been a transaction"); + let block = block_storage::read_current(&mut host, &ChainFamily::Evm) + .expect("Should have found a block"); + let transaction = block.first_transaction_hash(); + let failed_loop_hash = transaction.expect("There should have been a transaction"); let failed_loop_status = storage::read_transaction_receipt_status(&mut host, failed_loop_hash) .expect("There should have been a receipt"); diff --git a/etherlink/kernel_latest/kernel/src/block_in_progress.rs b/etherlink/kernel_latest/kernel/src/block_in_progress.rs index 173a9e721d01e06e1f9f22d329e1c45f4a52a2aa..7bb71fa274897736db10dc5a013d7fcc2fcfdf38 100644 --- a/etherlink/kernel_latest/kernel/src/block_in_progress.rs +++ b/etherlink/kernel_latest/kernel/src/block_in_progress.rs @@ -10,6 +10,7 @@ use crate::block_storage; use crate::error::Error; use crate::error::TransferError::CumulativeGasUsedOverflow; use crate::gas_price::base_fee_per_gas; +use crate::l2block::L2Block; use crate::storage::{self, object_path, receipt_path}; use crate::tick_model; use crate::transaction::{Transaction, Transactions::EthTxs, Transactions::TezTxs}; @@ -449,7 +450,7 @@ impl EthBlockInProgress { self, host: &mut Host, block_constants: &BlockConstants, - ) -> Result { + ) -> Result { let state_root = Self::safe_store_get_hash(host, &EVM_ACCOUNTS_PATH)?; let receipts_root = self.receipts_root(host, &self.previous_receipts_root)?; let transactions_root = @@ -472,6 +473,7 @@ impl EthBlockInProgress { block_constants, base_fee_per_gas, ); + let new_block = L2Block::Etherlink(Box::new(new_block)); block_storage::store_current(host, &new_block) .context("Failed to store the current block")?; Ok(new_block) diff --git a/etherlink/kernel_latest/kernel/src/block_storage.rs b/etherlink/kernel_latest/kernel/src/block_storage.rs index 22586cbdd07dc05ec9954498029d11d4e32b6530..88f560398a8bb938da99d5a6c5723274b2777b61 100644 --- a/etherlink/kernel_latest/kernel/src/block_storage.rs +++ b/etherlink/kernel_latest/kernel/src/block_storage.rs @@ -3,8 +3,6 @@ // SPDX-License-Identifier: MIT use primitive_types::{H256, U256}; -use tezos_ethereum::block::EthBlock; -use tezos_ethereum::rlp_helpers::VersionedEncoding; use tezos_evm_logging::{ log, Level::{Debug, Info}, @@ -16,9 +14,9 @@ use tezos_smart_rollup_host::path::OwnedPath; use tezos_smart_rollup_host::path::RefPath; use tezos_storage::{read_h256_be, read_u256_le, write_h256_be, write_u256_le}; -use crate::migration::allow_path_not_found; use crate::storage::EVM_TRANSACTIONS_OBJECTS; use crate::storage::EVM_TRANSACTIONS_RECEIPTS; +use crate::{chains::ChainFamily, l2block::L2Block, migration::allow_path_not_found}; mod path { use super::*; @@ -52,45 +50,45 @@ fn store_current_hash(host: &mut impl Runtime, hash: H256) -> anyhow::Result<()> fn store_block( host: &mut impl Runtime, - block: &EthBlock, + block: &L2Block, index_block: bool, ) -> anyhow::Result<()> { if index_block { // Index the block, /evm/world_state/indexes/blocks/ points to // the block hash. let index = IndexableStorage::new(&path::INDEXES)?; - index.push_value(host, block.hash.as_bytes())?; + index.push_value(host, block.hash().as_bytes())?; } - let path = path::path(block.hash)?; + let path = path::path(block.hash())?; let bytes = block.to_bytes(); Ok(host.store_write_all(&path, &bytes)?) } fn store_current_index_or_not( host: &mut impl Runtime, - block: &EthBlock, + block: &L2Block, index_block: bool, ) -> anyhow::Result<()> { - store_current_number(host, block.number)?; - store_current_hash(host, block.hash)?; + store_current_number(host, block.number())?; + store_current_hash(host, block.hash())?; store_block(host, block, index_block)?; log!( host, Info, "Storing block {} at {} containing {} transaction(s) for {} gas used.", - block.number, - block.timestamp, - block.transactions.len(), - U256::to_string(&block.gas_used) + block.number(), + block.timestamp(), + block.number_of_transactions(), + U256::to_string(&block.gas_used()) ); Ok(()) } -pub fn store_current(host: &mut impl Runtime, block: &EthBlock) -> anyhow::Result<()> { +pub fn store_current(host: &mut impl Runtime, block: &L2Block) -> anyhow::Result<()> { store_current_index_or_not(host, block, true) } -pub fn restore_current(host: &mut impl Runtime, block: &EthBlock) -> anyhow::Result<()> { +pub fn restore_current(host: &mut impl Runtime, block: &L2Block) -> anyhow::Result<()> { store_current_index_or_not(host, block, false) } @@ -102,17 +100,23 @@ pub fn read_current_hash(host: &impl Runtime) -> anyhow::Result { read_h256_be(host, &path::CURRENT_HASH) } -pub fn read_current(host: &mut impl Runtime) -> anyhow::Result { +pub fn read_current( + host: &mut impl Runtime, + chain_family: &ChainFamily, +) -> anyhow::Result { let hash = read_current_hash(host)?; let block_path = path::path(hash)?; let bytes = &host.store_read_all(&block_path)?; - let block_from_bytes = EthBlock::from_bytes(bytes)?; + let block_from_bytes = L2Block::try_from_bytes(chain_family, bytes)?; Ok(block_from_bytes) } -pub fn garbage_collect_blocks(host: &mut impl Runtime) -> anyhow::Result<()> { +pub fn garbage_collect_blocks( + host: &mut impl Runtime, + chain_family: &ChainFamily, +) -> anyhow::Result<()> { log!(host, Debug, "Garbage collecting blocks."); - if let Ok(block) = read_current(host) { + if let Ok(block) = read_current(host, chain_family) { // The kernel needs the current block to process the next one. Therefore // we garbage collect everything but the current block. host.store_delete(&path::PATH)?; diff --git a/etherlink/kernel_latest/kernel/src/blueprint_storage.rs b/etherlink/kernel_latest/kernel/src/blueprint_storage.rs index 2e325dde0812e5f5a8f4395386c226b5f214b093..1760b5998516e23a1f2f55f27b86ec1c6b9fc7f4 100644 --- a/etherlink/kernel_latest/kernel/src/blueprint_storage.rs +++ b/etherlink/kernel_latest/kernel/src/blueprint_storage.rs @@ -8,6 +8,7 @@ use crate::blueprint::Blueprint; use crate::chains::ChainFamily; use crate::configuration::{Configuration, ConfigurationMode}; use crate::error::{Error, StorageError}; +use crate::l2block::L2Block; use crate::sequencer_blueprint::{ BlueprintWithDelayedHashes, UnsignedSequencerBlueprint, }; @@ -190,6 +191,15 @@ impl From for BlockHeader { } } +impl From for BlockHeader { + fn from(value: L2Block) -> Self { + match value { + L2Block::Etherlink(block) => (*block).into(), + L2Block::Tezlink(block) => block.into(), + } + } +} + pub fn blueprint_path(number: U256) -> Result { let number_as_path: Vec = format!("/{}", number).into(); // The key being an integer value, it will always be valid as a path, diff --git a/etherlink/kernel_latest/kernel/src/inbox.rs b/etherlink/kernel_latest/kernel/src/inbox.rs index 71c6a3d049d2254cc4c9de2e2a70496e751ca386..ebc6e3daf197fb412ec97a122acb212e33859c70 100644 --- a/etherlink/kernel_latest/kernel/src/inbox.rs +++ b/etherlink/kernel_latest/kernel/src/inbox.rs @@ -379,6 +379,7 @@ pub fn handle_input( host: &mut impl Runtime, input: Input, inbox_content: &mut Mode::Inbox, + chain_family: &ChainFamily, garbage_collect_blocks: bool, ) -> anyhow::Result<()> { match input { @@ -392,7 +393,7 @@ pub fn handle_input( // New inbox level detected, remove all previous events. clear_events(host)?; if garbage_collect_blocks { - crate::block_storage::garbage_collect_blocks(host)?; + crate::block_storage::garbage_collect_blocks(host, chain_family)?; } store_last_info_per_level_timestamp(host, info.info.predecessor_timestamp)?; store_l1_level(host, info.level)? @@ -463,7 +464,13 @@ fn read_and_dispatch_input( Ok(ReadStatus::FinishedIgnore) } InputResult::Input(input) => { - handle_input(host, input, res, garbage_collect_blocks)?; + handle_input( + host, + input, + res, + &chain_configuration.get_chain_family(), + garbage_collect_blocks, + )?; Ok(ReadStatus::Ongoing) } } diff --git a/etherlink/kernel_latest/kernel/src/l2block.rs b/etherlink/kernel_latest/kernel/src/l2block.rs index c7c37e07fbef4748d5bd235cd4c023287e47122f..0b54131da712bf3162ec1a5dfeb3d7a76df502b5 100644 --- a/etherlink/kernel_latest/kernel/src/l2block.rs +++ b/etherlink/kernel_latest/kernel/src/l2block.rs @@ -1,7 +1,13 @@ -use tezos_ethereum::block::EthBlock; +use primitive_types::{H256, U256}; +use rlp::DecoderError; +use tezos_ethereum::{block::EthBlock, rlp_helpers::VersionedEncoding}; +use tezos_smart_rollup::types::Timestamp; use tezos_tezlink::block::TezBlock; -use crate::blueprint_storage::{BlockHeader, ChainHeader}; +use crate::{ + blueprint_storage::{BlockHeader, ChainHeader}, + chains::ChainFamily, +}; #[derive(PartialEq, Debug)] pub enum L2Block { @@ -14,10 +20,87 @@ pub enum L2Block { } impl L2Block { + pub fn number(&self) -> U256 { + match self { + Self::Etherlink(block) => block.number, + Self::Tezlink(block) => block.number, + } + } + + pub fn timestamp(&self) -> Timestamp { + match self { + Self::Etherlink(block) => block.timestamp, + Self::Tezlink(block) => block.timestamp, + } + } + + pub fn number_of_transactions(&self) -> usize { + match &self { + Self::Etherlink(block) => block.transactions.len(), + Self::Tezlink(_) => 0, + } + } + + #[cfg(test)] + pub fn first_transaction_hash( + &self, + ) -> Option<&tezos_ethereum::transaction::TransactionHash> { + match &self { + Self::Etherlink(block) => block.transactions.first(), + Self::Tezlink(_) => None, + } + } + + pub fn gas_used(&self) -> U256 { + match self { + Self::Etherlink(block) => block.gas_used, + Self::Tezlink(_) => U256::zero(), + } + } + pub fn header(self) -> BlockHeader { match self { Self::Etherlink(block) => (*block).into(), Self::Tezlink(block) => block.into(), } } + + pub fn hash(&self) -> H256 { + match self { + Self::Etherlink(block) => block.hash, + Self::Tezlink(block) => block.hash, + } + } + + pub fn to_bytes(&self) -> Vec { + match self { + Self::Etherlink(block) => block.to_bytes(), + Self::Tezlink(block) => block.to_bytes(), + } + } + + #[cfg(test)] + pub fn base_fee_per_gas(&self) -> U256 { + use crate::fees::MINIMUM_BASE_FEE_PER_GAS; + match self { + Self::Etherlink(block) => block.base_fee_per_gas, + Self::Tezlink(_) => MINIMUM_BASE_FEE_PER_GAS.into(), + } + } + + pub fn try_from_bytes( + chain_family: &ChainFamily, + bytes: &[u8], + ) -> Result { + match chain_family { + ChainFamily::Evm => { + Ok(L2Block::Etherlink(Box::new(EthBlock::from_bytes(bytes)?))) + } + ChainFamily::Michelson => { + let block = TezBlock::try_from_bytes(bytes) + .map_err(|_| DecoderError::Custom("Binary decoding error"))?; + Ok(L2Block::Tezlink(block)) + } + } + } } diff --git a/etherlink/kernel_latest/kernel/src/lib.rs b/etherlink/kernel_latest/kernel/src/lib.rs index 13c702d711a28a9bfae20d317658d7bcf913d1c3..37462d3effa502abacf4e4f230e13cbd6839689d 100644 --- a/etherlink/kernel_latest/kernel/src/lib.rs +++ b/etherlink/kernel_latest/kernel/src/lib.rs @@ -169,9 +169,11 @@ fn retrieve_base_fee_per_gas( host: &mut Host, minimum_base_fee_per_gas: U256, ) -> U256 { - match block_storage::read_current(host) { + use chains::ChainFamily; + + match block_storage::read_current(host, &ChainFamily::Evm) { Ok(current_block) => { - let current_base_fee_per_gas = current_block.base_fee_per_gas; + let current_base_fee_per_gas = current_block.base_fee_per_gas(); if current_base_fee_per_gas < minimum_base_fee_per_gas { minimum_base_fee_per_gas } else { diff --git a/etherlink/kernel_latest/kernel/src/migration.rs b/etherlink/kernel_latest/kernel/src/migration.rs index dcd6cb263da75f9aaaa7ec336e6d3e80a83de1a6..42d8b3f17080d7f4402ceff18453c0455df94b27 100644 --- a/etherlink/kernel_latest/kernel/src/migration.rs +++ b/etherlink/kernel_latest/kernel/src/migration.rs @@ -271,7 +271,7 @@ fn migrate_to( } StorageVersion::V27 => { // Initialize the next_blueprint_info field - match block_storage::read_current(host) { + match block_storage::read_current(host, &crate::chains::ChainFamily::Evm) { Ok(block) => { store_current_block_header(host, &block.into())?; Ok(MigrationStatus::Done) diff --git a/etherlink/kernel_latest/kernel/src/simulation.rs b/etherlink/kernel_latest/kernel/src/simulation.rs index 155b2d1f356fe948bb77c36c833cbd1933ded25d..a2b126ad92ffe37a9a1576468c343ca7e1c6e6a0 100644 --- a/etherlink/kernel_latest/kernel/src/simulation.rs +++ b/etherlink/kernel_latest/kernel/src/simulation.rs @@ -8,8 +8,12 @@ // Module containing most Simulation related code, in one place, to be deleted // when the proxy node simulates directly +use std::borrow::Cow; + use crate::block_storage; +use crate::chains::ChainFamily; use crate::fees::simulation_add_gas_for_fees; +use crate::l2block::L2Block; use crate::storage::{ read_last_info_per_level_timestamp, read_maximum_allowed_ticks, read_sequencer_pool_address, read_tracer_input, @@ -395,8 +399,8 @@ impl Evaluation { } } - let constants = match block_storage::read_current(host) { - Ok(block) => { + let constants = match block_storage::read_current(host, &ChainFamily::Evm) { + Ok(L2Block::Etherlink(block)) => { // Timestamp is taken from the simulation caller if provided. // If the timestamp is missing, because of an older evm-node, // default to last block timestamp. @@ -420,6 +424,16 @@ impl Evaluation { prevrandao: None, } } + Ok(L2Block::Tezlink(_block)) => { + log!( + host, + Fatal, + "Read a tezlink block when expecting an etherlink block" + ); + return Err(Error::Simulation(EthereumError::WrappedError(Cow::from( + "Should not have found a Tezlink block", + )))); + } Err(_) => { // Timestamp is taken from the simulation caller if provided. // If the timestamp is missing, because of an older evm-node, diff --git a/etherlink/kernel_latest/tezos/Cargo.toml b/etherlink/kernel_latest/tezos/Cargo.toml index 82f253f4a49e5b5fce6e128954a71746e6a8cf1d..77246aa24759bb07aac29955814d7f9e542489b9 100644 --- a/etherlink/kernel_latest/tezos/Cargo.toml +++ b/etherlink/kernel_latest/tezos/Cargo.toml @@ -10,7 +10,9 @@ license = "MIT" [dependencies] +tezos_crypto_rs.workspace = true rlp.workspace = true +hex.workspace = true tezos_ethereum.workspace = true primitive-types.workspace = true diff --git a/etherlink/kernel_latest/tezos/src/block.rs b/etherlink/kernel_latest/tezos/src/block.rs index b1aebca921b9749117a44bff0e71d4841f7556e6..64725fb67b78f87b59b393eddc6c56c59b3679d3 100644 --- a/etherlink/kernel_latest/tezos/src/block.rs +++ b/etherlink/kernel_latest/tezos/src/block.rs @@ -4,6 +4,7 @@ use primitive_types::{H256, U256}; use std::{array::TryFromSliceError, str::FromStr}; +use tezos_crypto_rs::blake2b::digest_256; use tezos_smart_rollup::types::Timestamp; // WIP: This structure will evolve to look like Tezos block @@ -23,6 +24,34 @@ impl TezBlock { .unwrap() } + fn hash(&self) -> H256 { + let Self { + hash: _, + number, + timestamp, + previous_hash, + } = self; + let mut data = number.to_string(); + data.push_str(×tamp.i64().to_string()); + data.push_str(&previous_hash.to_string()); + let encoded_data = hex::encode(&data); + let hashed_data = digest_256(encoded_data.as_bytes()); + H256::from_slice(&hashed_data) + } + + pub fn new(number: U256, timestamp: Timestamp, previous_hash: H256) -> Self { + let block = Self { + hash: H256::zero(), + number, + timestamp, + previous_hash, + }; + 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 { @@ -56,13 +85,7 @@ impl TezBlock { let timestamp_array: [u8; 8] = bytes[36..44].try_into()?; let timestamp = Timestamp::from(i64::from_le_bytes(timestamp_array)); - // In a next MR, genesis_block_hash will be replaced by a true hash function - Ok(TezBlock { - number, - hash: TezBlock::genesis_block_hash(), - timestamp, - previous_hash, - }) + Ok(TezBlock::new(number, timestamp, previous_hash)) } } @@ -84,12 +107,7 @@ mod tests { let number = U256::one(); let timestamp = Timestamp::from(0); let previous_hash = TezBlock::genesis_block_hash(); - TezBlock { - number, - hash: TezBlock::genesis_block_hash(), - timestamp, - previous_hash, - } + TezBlock::new(number, timestamp, previous_hash) } #[test]