diff --git a/src/kernel_evm/CHANGES.md b/src/kernel_evm/CHANGES.md index 57ffb0e3bd5284ff143998c4e383d1ea243782cc..e37ef177af8bcfa9bd64a873d35946cbd74774f7 100644 --- a/src/kernel_evm/CHANGES.md +++ b/src/kernel_evm/CHANGES.md @@ -13,6 +13,7 @@ ### Breaking changes ### Internal +- The kernel reboots before reaching maximum number of ticks (!9369) ## Version 4c111dcae061bea6c3616429a0ea1262ce6c174f diff --git a/src/kernel_evm/ethereum/src/rlp_helpers.rs b/src/kernel_evm/ethereum/src/rlp_helpers.rs index 1e0ce2e33467a0281c7f96321758843a924d1145..d69908892b37c8cbda954a74e5ac1fae138674a0 100644 --- a/src/kernel_evm/ethereum/src/rlp_helpers.rs +++ b/src/kernel_evm/ethereum/src/rlp_helpers.rs @@ -5,7 +5,9 @@ //! Module containing helper functions for RLP encoding/decoding. -use crate::transaction::{TransactionHash, TransactionStatus, TransactionType}; +use crate::transaction::{ + TransactionHash, TransactionStatus, TransactionType, TRANSACTION_HASH_SIZE, +}; use primitive_types::{H256, U256}; use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpIterator, RlpStream}; @@ -89,6 +91,16 @@ pub fn append_h256(s: &mut rlp::RlpStream, h256: H256) -> &mut RlpStream { } } +pub fn decode_tx_hash(item: rlp::Rlp<'_>) -> Result { + let list = item.data()?; + if list.len() != TRANSACTION_HASH_SIZE { + return Err(DecoderError::RlpIncorrectListLen); + } + let mut tx = [0u8; TRANSACTION_HASH_SIZE]; + tx.copy_from_slice(list); + Ok(tx) +} + pub fn decode_field_u256_le( decoder: &Rlp<'_>, field_name: &'static str, diff --git a/src/kernel_evm/ethereum/src/signatures.rs b/src/kernel_evm/ethereum/src/signatures.rs index 75f5e26156c526e86951cff22a3da076d3c3ff40..3d639b661c381779a0c51a6aefa882c3e64922b1 100644 --- a/src/kernel_evm/ethereum/src/signatures.rs +++ b/src/kernel_evm/ethereum/src/signatures.rs @@ -323,7 +323,6 @@ impl Encodable for EthereumTransactionCommon { stream.append(&self.v); append_h256(stream, self.r); append_h256(stream, self.s); - assert!(stream.is_finished()); } } diff --git a/src/kernel_evm/kernel/src/block.rs b/src/kernel_evm/kernel/src/block.rs index 1c8b045ae4cc6757c259b75450dfc9c0a5a68a51..019e6e2e72d5f2e1950eadec72b2787a13576bb0 100644 --- a/src/kernel_evm/kernel/src/block.rs +++ b/src/kernel_evm/kernel/src/block.rs @@ -10,17 +10,37 @@ use crate::blueprint::Queue; use crate::current_timestamp; use crate::error::Error; use crate::indexable_storage::IndexableStorage; -use crate::storage::{self, init_account_index}; +use crate::storage; +use crate::storage::init_account_index; +use crate::tick_model; use anyhow::Context; use block_in_progress::BlockInProgress; use evm_execution::account_storage::{init_account_storage, EthereumAccountStorage}; use evm_execution::precompiles; use evm_execution::precompiles::PrecompileBTreeMap; use primitive_types::U256; +use tezos_evm_logging::{log, Level::*}; use tezos_smart_rollup_host::runtime::Runtime; use tezos_ethereum::block::BlockConstants; +/// Struct used to allow the compiler to check that the tick counter value is +/// correctly moved and updated. Copy and Clone should NOT be derived. +struct TickCounter { + c: u64, +} + +impl TickCounter { + pub fn new(c: u64) -> Self { + Self { c } + } +} + +pub enum ComputationResult { + RebootNeeded, + Finished, +} + fn compute( host: &mut Host, block_in_progress: &mut BlockInProgress, @@ -28,16 +48,14 @@ fn compute( precompiles: &PrecompileBTreeMap, evm_account_storage: &mut EthereumAccountStorage, accounts_index: &mut IndexableStorage, -) -> Result<(), anyhow::Error> { +) -> Result { // iteration over all remaining transaction in the block while block_in_progress.has_tx() { // is reboot necessary ? if block_in_progress.would_overflow() { // TODO: https://gitlab.com/tezos/tezos/-/issues/6094 // there should be an upper bound on gasLimit - // TODO: https://gitlab.com/tezos/tezos/-/issues/5873 - // store the block in progress - return Ok(()); + return Ok(ComputationResult::RebootNeeded); } let transaction = block_in_progress.pop_tx().ok_or(Error::Reboot)?; @@ -65,7 +83,7 @@ fn compute( } }; } - Ok(()) + Ok(ComputationResult::Finished) } pub fn produce( @@ -85,43 +103,75 @@ pub fn produce( init_account_storage().context("Failed to initialize EVM account storage")?; let mut accounts_index = init_account_index()?; let precompiles = precompiles::precompile_set::(); - - // TODO: https://gitlab.com/tezos/tezos/-/issues/5873 - // if there is no more gas for the transaction, - // reload current container in progress and rest of queue + let mut tick_counter = TickCounter::new(tick_model::top_level_overhead_ticks()); for proposal in queue.proposals { - // proposal is turn into a ring to allow poping from the front - let ring = proposal.transactions.into(); - // TODO: https://gitlab.com/tezos/tezos/-/issues/5873 - // add proposal to the container - let mut block_in_progress = - BlockInProgress::new(current_block_number, current_constants.gas_price, ring); - compute( + let mut block_in_progress = bip_from_queue_element( + proposal, + current_block_number, + ¤t_constants, + tick_counter, + ); + + match compute( host, &mut block_in_progress, ¤t_constants, &precompiles, &mut evm_account_storage, &mut accounts_index, - )?; - - // TODO: https://gitlab.com/tezos/tezos/-/issues/5873 - // if reboot initiated, store container in progress and exit - // else store block - let new_block = block_in_progress - .finalize_and_store(host) - .context("Failed to finalize the block in progress")?; - current_block_number = new_block.number + 1; - current_constants = new_block.constants(); + )? { + ComputationResult::RebootNeeded => { + log!(host, Info, "Ask for reboot"); + storage::store_block_in_progress(host, &block_in_progress)?; + storage::add_reboot_flag(host)?; + host.mark_for_reboot()?; + // TODO: https://gitlab.com/tezos/tezos/-/issues/5873 + // store the queue + return Ok(()); + } + ComputationResult::Finished => { + tick_counter = TickCounter::new(block_in_progress.estimated_ticks); + let new_block = block_in_progress + .finalize_and_store(host) + .context("Failed to finalize the block in progress")?; + current_block_number = new_block.number + 1; + current_constants = new_block.constants(); + storage::delete_block_in_progress(host)?; + } + } } Ok(()) } +fn bip_from_queue_element( + proposal: crate::blueprint::QueueElement, + current_block_number: U256, + constants: &BlockConstants, + tick_counter: TickCounter, +) -> BlockInProgress { + match proposal { + crate::blueprint::QueueElement::Blueprint(proposal) => { + // proposal is turn into a ring to allow poping from the front + let ring = proposal.transactions.into(); + BlockInProgress::new_with_ticks( + current_block_number, + constants.gas_price, + ring, + tick_counter.c, + ) + } + crate::blueprint::QueueElement::BlockInProgress(mut bip) => { + bip.estimated_ticks = tick_counter.c; + bip + } + } +} + #[cfg(test)] mod tests { use super::*; - use crate::blueprint::Blueprint; + use crate::blueprint::{Blueprint, QueueElement}; use crate::inbox::Transaction; use crate::inbox::TransactionContent; use crate::inbox::TransactionContent::Ethereum; @@ -136,11 +186,18 @@ mod tests { }; use primitive_types::{H160, H256, U256}; use std::collections::VecDeque; + use std::ops::Rem; use std::str::FromStr; use tezos_ethereum::signatures::EthereumTransactionCommon; - use tezos_ethereum::transaction::{TransactionStatus, TRANSACTION_HASH_SIZE}; + use tezos_ethereum::transaction::{ + TransactionHash, TransactionStatus, TRANSACTION_HASH_SIZE, + }; use tezos_smart_rollup_mock::MockHost; + fn blueprint(transactions: Vec) -> QueueElement { + QueueElement::Blueprint(Blueprint { transactions }) + } + fn address_from_str(s: &str) -> Option { let data = &hex::decode(s).unwrap(); Some(H160::from_slice(data)) @@ -291,7 +348,7 @@ mod tests { ]; let queue = Queue { - proposals: vec![Blueprint { transactions }], + proposals: vec![blueprint(transactions)], kernel_upgrade: None, }; @@ -329,7 +386,7 @@ mod tests { let transactions: Vec = vec![invalid_tx]; let queue = Queue { - proposals: vec![Blueprint { transactions }], + proposals: vec![blueprint(transactions)], kernel_upgrade: None, }; @@ -354,7 +411,7 @@ mod tests { let transactions: Vec = vec![valid_tx]; let queue = Queue { - proposals: vec![Blueprint { transactions }], + proposals: vec![blueprint(transactions)], kernel_upgrade: None, }; @@ -392,7 +449,7 @@ mod tests { let transactions: Vec = vec![valid_tx]; let queue = Queue { - proposals: vec![Blueprint { transactions }], + proposals: vec![blueprint(transactions)], kernel_upgrade: None, }; @@ -455,14 +512,7 @@ mod tests { }]; let queue = Queue { - proposals: vec![ - Blueprint { - transactions: transaction_0, - }, - Blueprint { - transactions: transaction_1, - }, - ], + proposals: vec![blueprint(transaction_0), blueprint(transaction_1)], kernel_upgrade: None, }; @@ -506,7 +556,7 @@ mod tests { ]; let queue = Queue { - proposals: vec![Blueprint { transactions }], + proposals: vec![blueprint(transactions)], kernel_upgrade: None, }; @@ -557,12 +607,7 @@ mod tests { let transactions = vec![tx.clone(), tx]; let queue = Queue { - proposals: vec![ - Blueprint { - transactions: transactions.clone(), - }, - Blueprint { transactions }, - ], + proposals: vec![blueprint(transactions.clone()), blueprint(transactions)], kernel_upgrade: None, }; @@ -601,7 +646,7 @@ mod tests { let transactions = vec![tx]; let queue = Queue { - proposals: vec![Blueprint { transactions }], + proposals: vec![blueprint(transactions)], kernel_upgrade: None, }; @@ -634,9 +679,7 @@ mod tests { let transactions = vec![tx]; let queue = Queue { - proposals: vec![Blueprint { - transactions: transactions.clone(), - }], + proposals: vec![blueprint(transactions.clone())], kernel_upgrade: None, }; @@ -645,7 +688,7 @@ mod tests { let indexed_accounts = length(&host, &accounts_index).unwrap(); let next_queue = Queue { - proposals: vec![Blueprint { transactions }], + proposals: vec![blueprint(transactions)], kernel_upgrade: None, }; @@ -677,7 +720,7 @@ mod tests { let transactions = vec![tx]; let queue = Queue { - proposals: vec![Blueprint { transactions }], + proposals: vec![blueprint(transactions)], kernel_upgrade: None, }; @@ -797,7 +840,7 @@ mod tests { assert_eq!( ticks, - tick_model::ticks_of_gas(21123) + tick_model::block_overhead_ticks() + tick_model::ticks_of_gas(21123) + tick_model::top_level_overhead_ticks() ); } @@ -835,7 +878,7 @@ mod tests { assert_eq!( ticks, tick_model::ticks_of_invalid_transaction() - + tick_model::block_overhead_ticks() + + tick_model::top_level_overhead_ticks() ); } @@ -927,9 +970,7 @@ mod tests { content: Ethereum(dummy_eth_transaction_zero()), }; let queue = Queue { - proposals: vec![Blueprint { - transactions: vec![transaction], - }], + proposals: vec![blueprint(vec![transaction])], kernel_upgrade: None, }; @@ -947,4 +988,190 @@ mod tests { let nonce = caller_account.nonce(&host).unwrap(); assert_eq!(nonce, default_nonce + 1, "nonce should have been bumped"); } + + /// A queue that should produce 1 block with an invalid transaction + fn almost_empty_queue() -> Queue { + let tx_hash = [0; TRANSACTION_HASH_SIZE]; + + // transaction should be invalid + let tx = Transaction { + tx_hash, + content: Ethereum(dummy_eth_transaction_one()), + }; + + let transactions = vec![tx]; + + Queue { + proposals: vec![blueprint(transactions)], + kernel_upgrade: None, + } + } + + fn check_current_block_number(host: &mut Host, nb: usize) { + let current_nb = storage::read_current_block_number(host) + .expect("Should have manage to check block number"); + assert_eq!(current_nb, U256::from(nb), "Incorrect block number"); + } + + #[test] + fn test_first_blocks() { + let mut host = MockHost::default(); + + // first block should be 0 + produce(&mut host, almost_empty_queue()) + .expect("Empty block should have been produced"); + check_current_block_number(&mut host, 0); + + // second block + produce(&mut host, almost_empty_queue()) + .expect("Empty block should have been produced"); + check_current_block_number(&mut host, 1); + + // third block + produce(&mut host, almost_empty_queue()) + .expect("Empty block should have been produced"); + check_current_block_number(&mut host, 2); + } + + fn dummy_eth(nonce: u64) -> EthereumTransactionCommon { + let nonce = U256::from(nonce); + let gas_price = U256::from(40000000000u64); + let gas_limit = 21000; + let value = U256::from(1); + let to = address_from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea"); + let tx = EthereumTransactionCommon { + chain_id: U256::one(), + nonce, + gas_price, + gas_limit, + to, + value, + data: vec![], + v: U256::one(), + r: H256::zero(), + s: H256::zero(), + }; + + // corresponding caller's address is 0xaf1276cbb260bb13deddb4209ae99ae6e497f446 + tx.sign_transaction( + "dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701" + .to_string(), + ) + .unwrap() + } + + fn hash_from_nonce(nonce: u64) -> TransactionHash { + let nonce = u64::to_le_bytes(nonce); + let mut hash = [0; 32]; + hash[..8].copy_from_slice(&nonce); + hash + } + + fn dummy_transaction(nonce: u64) -> Transaction { + Transaction { + tx_hash: hash_from_nonce(nonce), + content: TransactionContent::Ethereum(dummy_eth(nonce)), + } + } + + const TOO_MANY_TRANSACTIONS: u64 = 500; + + #[test] + fn test_reboot_many_tx_one_proposal() { + // init host + let mut host = MockHost::default(); + + // sanity check: no current block + assert!( + storage::read_current_block_number(&mut host).is_err(), + "Should not have found current block number" + ); + + //provision sender account + let sender = H160::from_str("af1276cbb260bb13deddb4209ae99ae6e497f446").unwrap(); + let sender_initial_balance = U256::from(10000000000000000000u64); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + sender_initial_balance, + ); + + let mut transactions = vec![]; + for n in 0..TOO_MANY_TRANSACTIONS { + transactions.push(dummy_transaction(n)); + } + let queue = Queue { + proposals: vec![blueprint(transactions)], + kernel_upgrade: None, + }; + + produce(&mut host, queue).expect("Should have produced"); + + // test no new block + assert!( + storage::read_current_block_number(&mut host).is_err(), + "Should not have found current block number" + ); + + // test reboot is set + assert!( + storage::was_rebooted(&mut host).expect("Should have found flag"), + "Flag should be set" + ); + } + + #[test] + fn test_reboot_many_tx_many_proposal() { + // init host + let mut host = MockHost::default(); + + // sanity check: no current block + assert!( + storage::read_current_block_number(&mut host).is_err(), + "Should not have found current block number" + ); + + //provision sender account + let sender = H160::from_str("af1276cbb260bb13deddb4209ae99ae6e497f446").unwrap(); + let sender_initial_balance = U256::from(10000000000000000000u64); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + sender_initial_balance, + ); + + let mut transactions = vec![]; + let mut proposals = vec![]; + for n in 1..TOO_MANY_TRANSACTIONS { + transactions.push(dummy_transaction(n)); + if n.rem(100) == 0 { + proposals.push(blueprint(transactions)); + transactions = vec![]; + } + } + let queue = Queue { + proposals, + kernel_upgrade: None, + }; + + produce(&mut host, queue).expect("Should have produced"); + + // test no new block + assert!( + storage::read_current_block_number(&mut host) + .expect("should have found a block number") + > U256::zero(), + "There should have been multiple blocks registered" + ); + + // test reboot is set + assert!( + storage::was_rebooted(&mut host).expect("Should have found flag"), + "Flag should be set" + ); + } } diff --git a/src/kernel_evm/kernel/src/block_in_progress.rs b/src/kernel_evm/kernel/src/block_in_progress.rs index 167f6bf401610baf2a0463e7950541ccbed9e43b..edd1b7e03507d0a0ec04e4123ef6afbf4375cdcb 100644 --- a/src/kernel_evm/kernel/src/block_in_progress.rs +++ b/src/kernel_evm/kernel/src/block_in_progress.rs @@ -13,13 +13,17 @@ use crate::storage; use crate::tick_model; use anyhow::Context; use primitive_types::{H256, U256}; +use rlp::{Decodable, DecoderError, Encodable}; use std::collections::VecDeque; use tezos_ethereum::block::L2Block; +use tezos_ethereum::rlp_helpers::*; use tezos_ethereum::transaction::{ TransactionObject, TransactionReceipt, TransactionStatus, TransactionType, + TRANSACTION_HASH_SIZE, }; use tezos_smart_rollup_host::runtime::Runtime; +#[derive(Debug, PartialEq, Clone)] /// Container for all data needed during block computation pub struct BlockInProgress { /// block number @@ -27,7 +31,7 @@ pub struct BlockInProgress { /// queue containing the transactions to execute tx_queue: VecDeque, /// list of transactions executed without issue - valid_txs: Vec<[u8; 32]>, + valid_txs: Vec<[u8; TRANSACTION_HASH_SIZE]>, /// gas accumulator pub cumulative_gas: U256, /// index for next transaction @@ -41,13 +45,104 @@ pub struct BlockInProgress { pub estimated_ticks: u64, } +impl Encodable for BlockInProgress { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(7); + stream.append(&self.number); + append_queue(stream, &self.tx_queue); + append_txs(stream, &self.valid_txs); + stream.append(&self.cumulative_gas); + stream.append(&self.index); + stream.append(&self.gas_price); + stream.append(&self.hash); + } +} + +fn append_queue(stream: &mut rlp::RlpStream, queue: &VecDeque) { + stream.begin_list(queue.len()); + for transaction in queue { + stream.append(transaction); + } +} + +fn append_txs(stream: &mut rlp::RlpStream, valid_txs: &Vec<[u8; TRANSACTION_HASH_SIZE]>) { + stream.begin_list(valid_txs.len()); + valid_txs.iter().for_each(|tx| { + stream.append_iter(*tx); + }) +} + +impl Decodable for BlockInProgress { + fn decode(decoder: &rlp::Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 7 { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let number: U256 = decode_field(&next(&mut it)?, "number")?; + let tx_queue: VecDeque = decode_queue(&next(&mut it)?)?; + let valid_txs: Vec<[u8; TRANSACTION_HASH_SIZE]> = + decode_valid_txs(&next(&mut it)?)?; + let cumulative_gas: U256 = decode_field(&next(&mut it)?, "cumulative_gas")?; + let index: u32 = decode_field(&next(&mut it)?, "index")?; + let gas_price: U256 = decode_field(&next(&mut it)?, "gas_price")?; + let hash: H256 = decode_field(&next(&mut it)?, "hash")?; + let estimated_ticks: u64 = 0; + let bip = Self { + number, + tx_queue, + valid_txs, + cumulative_gas, + index, + gas_price, + hash, + estimated_ticks, + }; + Ok(bip) + } +} + +fn decode_valid_txs( + decoder: &rlp::Rlp<'_>, +) -> Result, DecoderError> { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + let mut valid_txs = Vec::with_capacity(decoder.item_count()?); + for item in decoder.iter() { + let tx = decode_tx_hash(item)?; + valid_txs.push(tx); + } + Ok(valid_txs) +} + +fn decode_queue(decoder: &rlp::Rlp<'_>) -> Result, DecoderError> { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + let mut queue = VecDeque::with_capacity(decoder.item_count()?); + for item in decoder.iter() { + let tx: Transaction = item.as_val()?; + queue.push_back(tx); + } + Ok(queue) +} + impl BlockInProgress { - pub fn new( + pub fn queue_length(&self) -> usize { + self.tx_queue.len() + } + + pub fn new_with_ticks( number: U256, gas_price: U256, transactions: VecDeque, - ) -> BlockInProgress { - BlockInProgress { + estimated_ticks: u64, + ) -> Self { + Self { number, tx_queue: transactions, valid_txs: Vec::new(), @@ -58,10 +153,25 @@ impl BlockInProgress { // it should be computed at the end, and not included in the receipt // the block is referenced in the storage by the block number anyway hash: H256(number.into()), - estimated_ticks: tick_model::block_overhead_ticks(), + estimated_ticks, } } + // constructor of raw structure, used in tests + #[cfg(test)] + pub fn new( + number: U256, + gas_price: U256, + transactions: VecDeque, + ) -> BlockInProgress { + Self::new_with_ticks( + number, + gas_price, + transactions, + tick_model::top_level_overhead_ticks(), + ) + } + pub fn register_valid_transaction( &mut self, transaction: &Transaction, @@ -203,3 +313,139 @@ impl BlockInProgress { } } } + +#[cfg(test)] +mod tests { + + use super::BlockInProgress; + use crate::inbox::{Deposit, Transaction, TransactionContent}; + use primitive_types::{H160, H256, U256}; + use rlp::{Decodable, Encodable, Rlp}; + use tezos_ethereum::{ + signatures::EthereumTransactionCommon, transaction::TRANSACTION_HASH_SIZE, + }; + + fn dummy_etc(i: u8) -> EthereumTransactionCommon { + EthereumTransactionCommon { + chain_id: U256::from(i), + nonce: U256::from(i), + gas_price: U256::from(i), + gas_limit: i.into(), + to: None, + value: U256::from(i), + data: Vec::new(), + r: H256::from([i; 32]), + s: H256::from([i; 32]), + v: U256::from(i), + } + } + + fn dummy_tx_eth(i: u8) -> Transaction { + Transaction { + tx_hash: [i; TRANSACTION_HASH_SIZE], + content: TransactionContent::Ethereum(dummy_etc(i)), + } + } + + fn dummy_tx_deposit(i: u8) -> Transaction { + let deposit = Deposit { + amount: U256::from(i), + gas_price: U256::from(i), + receiver: H160::from([i; 20]), + }; + Transaction { + tx_hash: [i; TRANSACTION_HASH_SIZE], + content: TransactionContent::Deposit(deposit), + } + } + + #[test] + fn test_encode_bip_ethereum() { + let bip = BlockInProgress { + number: U256::from(42), + tx_queue: vec![dummy_tx_eth(1), dummy_tx_eth(8)].into(), + valid_txs: vec![[2; TRANSACTION_HASH_SIZE], [9; TRANSACTION_HASH_SIZE]], + cumulative_gas: U256::from(3), + index: 4, + gas_price: U256::from(5), + hash: H256::from([6; 32]), + estimated_ticks: 99, + }; + + let encoded = bip.rlp_bytes(); + let expected = "f9014d2af8e2f86fa00101010101010101010101010101010101010101010101010101010101010101f84c01f84901010180018001a00101010101010101010101010101010101010101010101010101010101010101a00101010101010101010101010101010101010101010101010101010101010101f86fa00808080808080808080808080808080808080808080808080808080808080808f84c01f84908080880088008a00808080808080808080808080808080808080808080808080808080808080808a00808080808080808080808080808080808080808080808080808080808080808f842a00202020202020202020202020202020202020202020202020202020202020202a00909090909090909090909090909090909090909090909090909090909090909030405a00606060606060606060606060606060606060606060606060606060606060606"; + assert_eq!(hex::encode(encoded), expected); + + let bytes = hex::decode(expected).expect("Should be valid hex string"); + let decoder = Rlp::new(&bytes); + let decoded = + BlockInProgress::decode(&decoder).expect("Should have decoded data"); + + // the estimated ticks are not stored + let fresh_bip = BlockInProgress { + estimated_ticks: 0, + ..bip + }; + assert_eq!(decoded, fresh_bip); + } + + #[test] + fn test_encode_bip_deposit() { + let bip = BlockInProgress { + number: U256::from(42), + tx_queue: vec![dummy_tx_deposit(1), dummy_tx_deposit(8)].into(), + valid_txs: vec![[2; TRANSACTION_HASH_SIZE], [9; TRANSACTION_HASH_SIZE]], + cumulative_gas: U256::from(3), + index: 4, + gas_price: U256::from(5), + hash: H256::from([6; 32]), + estimated_ticks: 99, + }; + + let encoded = bip.rlp_bytes(); + let expected = "f8e52af87af83ba00101010101010101010101010101010101010101010101010101010101010101d902d70101940101010101010101010101010101010101010101f83ba00808080808080808080808080808080808080808080808080808080808080808d902d70808940808080808080808080808080808080808080808f842a00202020202020202020202020202020202020202020202020202020202020202a00909090909090909090909090909090909090909090909090909090909090909030405a00606060606060606060606060606060606060606060606060606060606060606"; + assert_eq!(hex::encode(encoded), expected); + + let bytes = hex::decode(expected).expect("Should be valid hex string"); + let decoder = Rlp::new(&bytes); + let decoded = + BlockInProgress::decode(&decoder).expect("Should have decoded data"); + + // the estimated ticks are not stored + let fresh_bip = BlockInProgress { + estimated_ticks: 0, + ..bip + }; + assert_eq!(decoded, fresh_bip); + } + + #[test] + fn test_encode_bip_mixed() { + let bip = BlockInProgress { + number: U256::from(42), + tx_queue: vec![dummy_tx_eth(1), dummy_tx_deposit(8)].into(), + valid_txs: vec![[2; TRANSACTION_HASH_SIZE], [9; TRANSACTION_HASH_SIZE]], + cumulative_gas: U256::from(3), + index: 4, + gas_price: U256::from(5), + hash: H256::from([6; 32]), + estimated_ticks: 99, + }; + + let encoded = bip.rlp_bytes(); + let expected = "f901192af8aef86fa00101010101010101010101010101010101010101010101010101010101010101f84c01f84901010180018001a00101010101010101010101010101010101010101010101010101010101010101a00101010101010101010101010101010101010101010101010101010101010101f83ba00808080808080808080808080808080808080808080808080808080808080808d902d70808940808080808080808080808080808080808080808f842a00202020202020202020202020202020202020202020202020202020202020202a00909090909090909090909090909090909090909090909090909090909090909030405a00606060606060606060606060606060606060606060606060606060606060606"; + assert_eq!(hex::encode(encoded), expected); + + let bytes = hex::decode(expected).expect("Should be valid hex string"); + let decoder = Rlp::new(&bytes); + let decoded = + BlockInProgress::decode(&decoder).expect("Should have decoded data"); + + // the estimated ticks are not stored + let fresh_bip = BlockInProgress { + estimated_ticks: 0, + ..bip + }; + assert_eq!(decoded, fresh_bip); + } +} diff --git a/src/kernel_evm/kernel/src/blueprint.rs b/src/kernel_evm/kernel/src/blueprint.rs index d43cd974f606d587171c6787045ad43c4463d12a..ca4a9d3d0b1f77f44973c1f31651b51c8d6b9204 100644 --- a/src/kernel_evm/kernel/src/blueprint.rs +++ b/src/kernel_evm/kernel/src/blueprint.rs @@ -4,6 +4,7 @@ // // SPDX-License-Identifier: MIT +use crate::block_in_progress::BlockInProgress; use crate::inbox::{read_inbox, KernelUpgrade, Transaction, TransactionContent}; use crate::Error; use primitive_types::U256; @@ -14,13 +15,16 @@ use tezos_smart_rollup_host::runtime::Runtime; pub struct Blueprint { pub transactions: Vec, } - +pub enum QueueElement { + Blueprint(Blueprint), + BlockInProgress(BlockInProgress), +} #[derive(Default)] pub struct Queue { // In our case, to make it simple and straightforward it will be // an array of pendings transactions even though it'll be only a // singleton for our needs. - pub proposals: Vec, + pub proposals: Vec, pub kernel_upgrade: Option, } @@ -33,7 +37,9 @@ impl Queue { } pub fn add(queue: &mut Queue, transactions: Vec) { - queue.proposals.push(Blueprint { transactions }) + queue + .proposals + .push(QueueElement::Blueprint(Blueprint { transactions })) } } @@ -60,7 +66,7 @@ pub fn fetch( ) -> Result { let inbox_content = read_inbox(host, smart_rollup_address, ticketer)?; let transactions = filter_invalid_chain_id(inbox_content.transactions, chain_id); - let blueprint = Blueprint { transactions }; + let blueprint = QueueElement::Blueprint(Blueprint { transactions }); Ok(Queue { proposals: vec![blueprint], kernel_upgrade: inbox_content.kernel_upgrade, diff --git a/src/kernel_evm/kernel/src/inbox.rs b/src/kernel_evm/kernel/src/inbox.rs index 64cedde5240dc9dc45420bf392058635e6d9c848..acf31c5ba9ce2df082487e06abe3929b87db2d0a 100644 --- a/src/kernel_evm/kernel/src/inbox.rs +++ b/src/kernel_evm/kernel/src/inbox.rs @@ -17,8 +17,10 @@ use crate::tick_model; use crate::upgrade::check_dictator_signature; use crate::Error; use primitive_types::{H160, U256}; +use rlp::{Decodable, DecoderError, Encodable}; use sha3::{Digest, Keccak256}; use tezos_crypto_rs::hash::ContractKt1Hash; +use tezos_ethereum::rlp_helpers::{decode_field, decode_tx_hash, next}; use tezos_ethereum::signatures::EthereumTransactionCommon; use tezos_ethereum::transaction::TransactionHash; use tezos_evm_logging::{log, Level::*}; @@ -32,12 +34,84 @@ pub struct Deposit { pub receiver: H160, } +impl Encodable for Deposit { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(3); + stream.append(&self.amount); + stream.append(&self.gas_price); + stream.append(&self.receiver); + } +} + +impl Decodable for Deposit { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 3 { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let amount: U256 = decode_field(&next(&mut it)?, "amount")?; + let gas_price: U256 = decode_field(&next(&mut it)?, "gas_price")?; + let receiver: H160 = decode_field(&next(&mut it)?, "receiver")?; + Ok(Deposit { + amount, + gas_price, + receiver, + }) + } +} + #[derive(Debug, PartialEq, Clone)] pub enum TransactionContent { Ethereum(EthereumTransactionCommon), Deposit(Deposit), } +const ETHEREUM_TX_TAG: u8 = 1; +const DEPOSIT_TX_TAG: u8 = 2; + +impl Encodable for TransactionContent { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + match &self { + TransactionContent::Ethereum(eth) => { + stream.append(ÐEREUM_TX_TAG); + eth.rlp_append(stream) + } + TransactionContent::Deposit(dep) => { + stream.append(&DEPOSIT_TX_TAG); + dep.rlp_append(stream) + } + } + } +} + +impl Decodable for TransactionContent { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 2 { + return Err(DecoderError::RlpIncorrectListLen); + } + let tag: u8 = decoder.at(0)?.as_val()?; + let tx = decoder.at(1)?; + match tag { + DEPOSIT_TX_TAG => { + let deposit = Deposit::decode(&tx)?; + Ok(Self::Deposit(deposit)) + } + ETHEREUM_TX_TAG => { + let eth = EthereumTransactionCommon::decode(&tx)?; + Ok(Self::Ethereum(eth)) + } + _ => Err(DecoderError::Custom("Unknown transaction tag.")), + } + } +} #[derive(Debug, PartialEq, Clone)] pub struct Transaction { pub tx_hash: TransactionHash, @@ -53,6 +127,30 @@ impl Transaction { } } +impl Encodable for Transaction { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + stream.append_iter(self.tx_hash); + stream.append(&self.content); + } +} + +impl Decodable for Transaction { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 2 { + return Err(DecoderError::RlpIncorrectListLen); + } + let mut it = decoder.iter(); + let tx_hash: TransactionHash = decode_tx_hash(next(&mut it)?)?; + let content: TransactionContent = + decode_field(&next(&mut it)?, "Transaction content")?; + Ok(Transaction { tx_hash, content }) + } +} + #[derive(Debug, PartialEq, Clone)] pub struct KernelUpgrade { pub nonce: [u8; UPGRADE_NONCE_SIZE], diff --git a/src/kernel_evm/kernel/src/lib.rs b/src/kernel_evm/kernel/src/lib.rs index 3af8caa1cd77c240d241ecf362876781ecfa2144..f0f00a1fb7428d378776b763b9e2c96d419eb113 100644 --- a/src/kernel_evm/kernel/src/lib.rs +++ b/src/kernel_evm/kernel/src/lib.rs @@ -95,14 +95,23 @@ pub fn stage_one( // if rebooted, don't fetch inbox let queue = fetch(host, smart_rollup_address, chain_id, ticketer)?; - for (i, blueprint) in queue.proposals.iter().enumerate() { - log!( - host, - Info, - "Blueprint {} contains {} transactions.", - i, - blueprint.transactions.len() - ); + for (i, queue_elt) in queue.proposals.iter().enumerate() { + match queue_elt { + blueprint::QueueElement::Blueprint(b) => log!( + host, + Info, + "Blueprint {} contains {} transactions.", + i, + b.transactions.len() + ), + blueprint::QueueElement::BlockInProgress(bip) => log!( + host, + Info, + "Block in progress {} has {} transactions left.", + i, + bip.queue_length() + ), + } } Ok(queue) @@ -191,16 +200,41 @@ fn retrieve_chain_id(host: &mut Host) -> Result { } } +fn fetch_queue_left(host: &mut Host) -> Result { + let mut queue = Queue::new(); + // fetch rest of queue + // TODO: https://gitlab.com/tezos/tezos/-/issues/5873 + // reload the queue + + // fetch Bip + let bip = storage::read_block_in_progress(host)?; + queue.proposals = vec![blueprint::QueueElement::BlockInProgress(bip)]; + Ok(queue) +} + pub fn main(host: &mut Host) -> Result<(), anyhow::Error> { - stage_zero(host)?; - set_kernel_version(host)?; - let smart_rollup_address = retrieve_smart_rollup_address(host) - .context("Failed to retrieve smart rollup address")?; - let chain_id = retrieve_chain_id(host).context("Failed to retrieve chain id")?; - let ticketer = read_ticketer(host); - - let queue = stage_one(host, smart_rollup_address, chain_id, ticketer) - .context("Failed during stage 1")?; + let queue = if storage::was_rebooted(host)? { + // kernel was rebooted + log!( + host, + Info, + "Kernel was rebooted. Reboot left: {}\n", + host.reboot_left()? + ); + storage::delete_reboot_flag(host)?; + fetch_queue_left(host)? + } else { + // first kernel run of the level + stage_zero(host)?; + set_kernel_version(host)?; + let smart_rollup_address = retrieve_smart_rollup_address(host) + .context("Failed to retrieve smart rollup address")?; + let chain_id = retrieve_chain_id(host).context("Failed to retrieve chain id")?; + let ticketer = read_ticketer(host); + + stage_one(host, smart_rollup_address, chain_id, ticketer) + .context("Failed during stage 1")? + }; stage_two(host, queue).context("Failed during stage 2") } diff --git a/src/kernel_evm/kernel/src/storage.rs b/src/kernel_evm/kernel/src/storage.rs index 3edc9d35ba6a8d6586dd257e670fae2461882876..20d84b2ee1ad7794f72004f8a88966e664a3fd09 100644 --- a/src/kernel_evm/kernel/src/storage.rs +++ b/src/kernel_evm/kernel/src/storage.rs @@ -5,6 +5,7 @@ #![allow(dead_code)] use crate::indexable_storage::IndexableStorage; +use anyhow::Context; use evm_execution::account_storage::EthereumAccount; use libsecp256k1::PublicKey; use tezos_crypto_rs::hash::{ContractKt1Hash, HashTrait}; @@ -14,9 +15,10 @@ use tezos_smart_rollup_encoding::timestamp::Timestamp; use tezos_smart_rollup_host::path::*; use tezos_smart_rollup_host::runtime::{Runtime, RuntimeError, ValueType}; +use crate::block_in_progress::BlockInProgress; use crate::error::{Error, StorageError}; use crate::parsing::UPGRADE_NONCE_SIZE; -use rlp::Encodable; +use rlp::{Decodable, Encodable, Rlp}; use tezos_ethereum::block::L2Block; use tezos_ethereum::transaction::{ TransactionHash, TransactionObject, TransactionReceipt, TransactionStatus, @@ -42,6 +44,12 @@ const DICTATOR_KEY: RefPath = RefPath::assert_from(b"/dictator_key"); // Size of the dictator public key in full length. const DICTATOR_KEY_SIZE: usize = 65; +// Path to the block in progress, used between reboots +const EVM_BLOCK_IN_PROGRESS: RefPath = RefPath::assert_from(b"/blocks/in_progress"); + +// flag denoting reboot +const REBOOTED: RefPath = RefPath::assert_from(b"/reboot"); + const EVM_CURRENT_BLOCK: RefPath = RefPath::assert_from(b"/blocks/current"); const EVM_BLOCKS: RefPath = RefPath::assert_from(b"/blocks"); const BLOCK_NUMBER: RefPath = RefPath::assert_from(b"/number"); @@ -783,6 +791,48 @@ pub fn store_kernel_version( .map_err(Error::from) } +pub fn store_block_in_progress( + host: &mut Host, + block: &BlockInProgress, +) -> Result<(), anyhow::Error> { + host.store_write_all(&EVM_BLOCK_IN_PROGRESS, &block.rlp_bytes()) + .context("Failed to store BlockInProgress") +} + +pub fn read_block_in_progress( + host: &mut Host, +) -> Result { + let bytes = host + .store_read_all(&EVM_BLOCK_IN_PROGRESS) + .context("Failed to read stored BlockInProgress")?; + let decoder = Rlp::new(bytes.as_slice()); + BlockInProgress::decode(&decoder).context("Failed to decode stored BlockInProgress") +} + +pub fn delete_block_in_progress( + host: &mut Host, +) -> Result<(), anyhow::Error> { + if host.store_read(&REBOOTED, 0, 0).is_ok() { + host.store_delete(&EVM_BLOCK_IN_PROGRESS) + .context("Failed to delete Block in progress")? + } + Ok(()) +} + +pub fn add_reboot_flag(host: &mut Host) -> Result<(), anyhow::Error> { + host.store_write(&REBOOTED, &[1], 0) + .context("Failed to set reboot flag") +} + +pub fn delete_reboot_flag(host: &mut Host) -> Result<(), anyhow::Error> { + host.store_delete(&REBOOTED) + .context("Failed to delete reboot flag") +} + +pub fn was_rebooted(host: &mut Host) -> Result { + Ok(host.store_read(&REBOOTED, 0, 0).is_ok()) +} + pub(crate) mod internal_for_tests { use super::*; @@ -823,3 +873,28 @@ pub(crate) mod internal_for_tests { Ok(receipt) } } + +#[cfg(test)] +mod tests { + use tezos_smart_rollup_mock::MockHost; + + use super::*; + #[test] + fn test_reboot_flag() { + let mut host = MockHost::default(); + + add_reboot_flag(&mut host).expect("Should have been able to set flag"); + + assert!(was_rebooted(&mut host).expect("should have found reboot flag")); + + delete_reboot_flag(&mut host).expect("Should have been able to delete flag"); + + assert!( + !was_rebooted(&mut host).expect("should not have failed without reboot flag") + ); + + add_reboot_flag(&mut host).expect("Should have been able to set flag"); + + assert!(was_rebooted(&mut host).expect("should have found reboot flag")); + } +} diff --git a/src/kernel_evm/kernel/src/tick_model.rs b/src/kernel_evm/kernel/src/tick_model.rs index 41f5bcd937b5c3e37bb8d60394a341fad0b79005..bfb371e6c4e37b9273947c467709b226cb64ee3d 100644 --- a/src/kernel_evm/kernel/src/tick_model.rs +++ b/src/kernel_evm/kernel/src/tick_model.rs @@ -38,7 +38,7 @@ const SAFETY_MARGIN: u64 = 2_000_000_000; /// estimate value using benchmarks const INITIALISATION_OVERHEAD: u64 = 1_000_000_000; /// see [maximum_reboots_per_input] -const _MAXIMUM_REBOOTS: u64 = 1_000; +pub const _MAXIMUM_NUMBER_OF_REBOOTS: u32 = 1_000; /// The minimum amount of gas for an ethereum transaction. const BASE_GAS: u64 = Config::london().gas_transaction_call; @@ -76,7 +76,7 @@ pub fn estimate_would_overflow(estimated_ticks: u64, transaction: &Transaction) /// Initial amount for the tick accumulator, corresponding to the overhead of a /// block -pub fn block_overhead_ticks() -> u64 { +pub fn top_level_overhead_ticks() -> u64 { INITIALISATION_OVERHEAD }