From 3579a23d763354dadd35a0aa44d53b0b5ce7e318 Mon Sep 17 00:00:00 2001 From: 0xK2 <65908739+thomas192@users.noreply.github.com> Date: Fri, 8 Mar 2024 14:42:19 +0100 Subject: [PATCH] feat: State update component / events (#20) * Added new component state and related events + testing * Implemented state update method + testing --- src/appchain.cairo | 48 +++++++++++++++++- src/lib.cairo | 15 ++++++ src/snos_output.cairo | 2 +- src/state/component.cairo | 84 ++++++++++++++++++++++++++++++++ src/state/interface.cairo | 21 ++++++++ src/state/mock.cairo | 29 +++++++++++ src/state/tests/test_state.cairo | 84 ++++++++++++++++++++++++++++++++ tests/test_appchain.cairo | 42 +++++++++++++++- 8 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 src/state/component.cairo create mode 100644 src/state/interface.cairo create mode 100644 src/state/mock.cairo create mode 100644 src/state/tests/test_state.cairo diff --git a/src/appchain.cairo b/src/appchain.cairo index 769871d..5b97284 100644 --- a/src/appchain.cairo +++ b/src/appchain.cairo @@ -26,6 +26,8 @@ mod appchain { output_process, output_process::{MessageToStarknet, MessageToAppchain}, }; use piltover::snos_output; + use piltover::state::component::state_cpt::HasComponent; + use piltover::state::{state_cpt, state_cpt::InternalTrait as StateInternal, IState}; use starknet::ContractAddress; use super::errors; @@ -35,6 +37,7 @@ mod appchain { component!(path: ownable_cpt, storage: ownable, event: OwnableEvent); component!(path: config_cpt, storage: config, event: ConfigEvent); component!(path: messaging_cpt, storage: messaging, event: MessagingEvent); + component!(path: state_cpt, storage: state, event: StateEvent); component!( path: ReentrancyGuardComponent, storage: reentrancy_guard, event: ReentrancyGuardEvent ); @@ -43,6 +46,8 @@ mod appchain { impl ConfigImpl = config_cpt::ConfigImpl; #[abi(embed_v0)] impl MessagingImpl = messaging_cpt::MessagingImpl; + #[abi(embed_v0)] + impl StateImpl = state_cpt::StateImpl; #[storage] struct Storage { @@ -54,6 +59,8 @@ mod appchain { messaging: messaging_cpt::Storage, #[substorage(v0)] reentrancy_guard: ReentrancyGuardComponent::Storage, + #[substorage(v0)] + state: state_cpt::Storage, } #[event] @@ -67,6 +74,22 @@ mod appchain { MessagingEvent: messaging_cpt::Event, #[flat] ReentrancyGuardEvent: ReentrancyGuardComponent::Event, + #[flat] + StateEvent: state_cpt::Event, + LogStateUpdate: LogStateUpdate, + LogStateTransitionFact: LogStateTransitionFact, + } + + #[derive(Drop, starknet::Event)] + struct LogStateUpdate { + state_root: felt252, + block_number: felt252, + block_hash: felt252, + } + + #[derive(Drop, starknet::Event)] + struct LogStateTransitionFact { + state_transition_fact: felt252, } /// Initializes the contract. @@ -75,9 +98,16 @@ mod appchain { /// /// * `address` - The contract address of the owner. #[constructor] - fn constructor(ref self: ContractState, owner: ContractAddress) { + fn constructor( + ref self: ContractState, + owner: ContractAddress, + state_root: felt252, + block_number: felt252, + block_hash: felt252, + ) { self.ownable.initializer(owner); self.messaging.initialize(CANCELLATION_DELAY_SECS); + self.state.initialize(state_root, block_number, block_hash); } #[abi(embed_v0)] @@ -86,7 +116,12 @@ mod appchain { self.reentrancy_guard.start(); self.config.assert_only_owner_or_operator(); // TODO(#3): facts verification. - // TODO(#4): update the current state (component needed). + + let state_transition_fact: felt252 = 0; // Done in another PR. + self.emit(LogStateTransitionFact { state_transition_fact }); + + // Perform state update + self.state.update(program_output); // Header size + 2 messages segments len. assert( @@ -109,6 +144,15 @@ mod appchain { self.messaging.process_messages_to_starknet(messages_to_starknet); self.messaging.process_messages_to_appchain(messages_to_appchain); self.reentrancy_guard.end(); + + self + .emit( + LogStateUpdate { + state_root: self.state.state_root.read(), + block_number: self.state.block_number.read(), + block_hash: self.state.block_hash.read(), + } + ); } } } diff --git a/src/lib.cairo b/src/lib.cairo index 3f888c5..a505bdb 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -34,3 +34,18 @@ mod messaging { mod test_messaging; } } + +mod state { + mod component; + mod interface; + mod mock; + + use component::state_cpt; + use interface::{IState, IStateDispatcher, IStateDispatcherTrait}; + use mock::state_mock; + + #[cfg(test)] + mod tests { + mod test_state; + } +} diff --git a/src/snos_output.cairo b/src/snos_output.cairo index e27c381..b963d77 100644 --- a/src/snos_output.cairo +++ b/src/snos_output.cairo @@ -15,7 +15,7 @@ const MESSAGE_TO_APPCHAIN_HEADER_SIZE: usize = 5; /// . /// The names are taken from SNOS repository: /// . -#[derive(Serde)] +#[derive(Drop, Serde)] struct ProgramOutput { /// The state commitment before this block. prev_state_root: felt252, diff --git a/src/state/component.cairo b/src/state/component.cairo new file mode 100644 index 0000000..76618a1 --- /dev/null +++ b/src/state/component.cairo @@ -0,0 +1,84 @@ +//! SPDX-License-Identifier: MIT +//! +//! Appchain - Starknet state component. + +/// Errors. +mod errors { + const INVALID_BLOCK_NUMBER: felt252 = 'State: invalid block number'; + const INVALID_PREVIOUS_ROOT: felt252 = 'State: invalid previous root'; +} + +/// State component. +#[starknet::component] +mod state_cpt { + use piltover::snos_output::ProgramOutput; + use piltover::state::interface::IState; + use super::errors; + + type StateRoot = felt252; + type BlockNumber = felt252; + type BlockHash = felt252; + + #[storage] + struct Storage { + state_root: StateRoot, + block_number: BlockNumber, + block_hash: BlockHash, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[embeddable_as(StateImpl)] + impl State< + TContractState, +HasComponent, + > of IState> { + fn update(ref self: ComponentState, program_output: Span,) { + let mut program_output = program_output; + let program_output: ProgramOutput = Serde::deserialize(ref program_output).unwrap(); + + // Check the blockNumber first as the error is less ambiguous then INVALID_PREVIOUS_ROOT. + self.block_number.write(self.block_number.read() + 1); + assert( + self.block_number.read() == program_output.block_number, + errors::INVALID_BLOCK_NUMBER + ); + + self.block_hash.write(program_output.block_hash); + + assert( + self.state_root.read() == program_output.prev_state_root, + errors::INVALID_PREVIOUS_ROOT + ); + + self.state_root.write(program_output.new_state_root); + } + + fn get_state(self: @ComponentState) -> (StateRoot, BlockNumber, BlockHash) { + (self.state_root.read(), self.block_number.read(), self.block_hash.read()) + } + } + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent, + > of InternalTrait { + /// Initialized the messaging component. + /// # Arguments + /// + /// * `state_root` - The state root. + /// * `block_number` - The current block number. + /// * `block_hash` - The hash of the current block. + fn initialize( + ref self: ComponentState, + state_root: StateRoot, + block_number: BlockNumber, + block_hash: BlockHash, + ) { + self.state_root.write(state_root); + self.block_number.write(block_number); + self.block_hash.write(block_hash); + } + } +} diff --git a/src/state/interface.cairo b/src/state/interface.cairo new file mode 100644 index 0000000..fdc4c5f --- /dev/null +++ b/src/state/interface.cairo @@ -0,0 +1,21 @@ +//! SPDX-License-Identifier: MIT +//! +//! Interface for Appchain - Starknet state. + +#[starknet::interface] +trait IState { + /// Validates that the 'blockNumber' and the previous root are consistent with the + /// current state and updates the state. + /// + /// # Arguments + /// + /// * `program_output` - The StarknetOS state update output. + fn update(ref self: T, program_output: Span,); + + /// Gets the current state. + /// + /// # Returns + /// + /// The state root, the block number and the block hash. + fn get_state(self: @T) -> (felt252, felt252, felt252); +} diff --git a/src/state/mock.cairo b/src/state/mock.cairo new file mode 100644 index 0000000..236459b --- /dev/null +++ b/src/state/mock.cairo @@ -0,0 +1,29 @@ +#[starknet::contract] +mod state_mock { + use piltover::state::{state_cpt, state_cpt::InternalTrait as StateInternal, IState}; + + component!(path: state_cpt, storage: state, event: StateEvent); + + #[abi(embed_v0)] + impl StateImpl = state_cpt::StateImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + state: state_cpt::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + StateEvent: state_cpt::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, state_root: felt252, block_number: felt252, block_hash: felt252, + ) { + self.state.initialize(state_root, block_number, block_hash); + } +} diff --git a/src/state/tests/test_state.cairo b/src/state/tests/test_state.cairo new file mode 100644 index 0000000..885701a --- /dev/null +++ b/src/state/tests/test_state.cairo @@ -0,0 +1,84 @@ +use piltover::state::{ + state_cpt, state_cpt::InternalTrait as StateInternal, IState, IStateDispatcher, + IStateDispatcherTrait, state_mock, +}; +use snforge_std as snf; +use snforge_std::{ContractClassTrait}; + +/// Deploys the mock with a specific state. +fn deploy_mock_with_state( + state_root: felt252, block_number: felt252, block_hash: felt252, +) -> IStateDispatcher { + let contract = snf::declare('state_mock'); + let calldata = array![state_root, block_number, block_hash]; + let contract_address = contract.deploy(@calldata).unwrap(); + IStateDispatcher { contract_address } +} + +#[test] +fn state_update_ok() { + let mock = deploy_mock_with_state( + state_root: 'state_root', block_number: 1, block_hash: 'block_hash_1', + ); + + let mut valid_state_update = array![ + 'state_root', + 'new_state_root', + 2, + 'block_hash_2', + 'config_hash', // Header. + 0, // appc to sn messages segment. + 0, // sn to appc messages segment. + ] + .span(); + + mock.update(valid_state_update); + + let (state_root, block_number, block_hash) = mock.get_state(); + + assert(state_root == 'new_state_root', 'invalid state root'); + assert(block_number == 2, 'invalid block number'); + assert(block_hash == 'block_hash_2', 'invalid block hash'); +} + +#[test] +#[should_panic(expected: ('State: invalid block number',))] +fn state_update_invalid_block_number() { + let mock = deploy_mock_with_state( + state_root: 'state_root', block_number: 1, block_hash: 'block_hash_1', + ); + + let mut invalid_state_update = array![ + 'state_root', + 'new_state_root', + 99999, + 'block_hash_2', + 'config_hash', // Header. + 0, // appc to sn messages segment. + 0, // sn to appc messages segment. + ] + .span(); + + mock.update(invalid_state_update); +} + +#[test] +#[should_panic(expected: ('State: invalid previous root',))] +fn state_update_invalid_previous_root() { + let mock = deploy_mock_with_state( + state_root: 'state_root', block_number: 1, block_hash: 'block_hash_1', + ); + + let mut invalid_state_update = array![ + 'invalid_state_root', + 'new_state_root', + 2, + 'block_hash_2', + 'config_hash', // Header. + 0, // appc to sn messages segment. + 0, // sn to appc messages segment. + ] + .span(); + + mock.update(invalid_state_update); +} diff --git a/tests/test_appchain.cairo b/tests/test_appchain.cairo index c118fba..677cccf 100644 --- a/tests/test_appchain.cairo +++ b/tests/test_appchain.cairo @@ -1,6 +1,7 @@ //! Appchain testing. //! use openzeppelin::tests::utils::constants as c; +use piltover::appchain::appchain::{Event, LogStateUpdate, LogStateTransitionFact}; use piltover::config::{IConfig, IConfigDispatcherTrait, IConfigDispatcher}; use piltover::interface::{IAppchain, IAppchainDispatcherTrait, IAppchainDispatcher}; use piltover::messaging::{IMessaging, IMessagingDispatcherTrait, IMessagingDispatcher}; @@ -14,7 +15,20 @@ use starknet::{ContractAddress, storage::StorageMemberAccessTrait}; /// Deploys the appchain contract. fn deploy_with_owner(owner: felt252) -> (IAppchainDispatcher, EventSpy) { let contract = snf::declare('appchain'); - let calldata = array![owner]; + let calldata = array![owner, 0, 0, 0]; + let contract_address = contract.deploy(@calldata).unwrap(); + + let mut spy = snf::spy_events(SpyOn::One(contract_address)); + + (IAppchainDispatcher { contract_address }, spy) +} + +/// Deploys the appchain contract. +fn deploy_with_owner_and_state( + owner: felt252, state_root: felt252, block_number: felt252, block_hash: felt252, +) -> (IAppchainDispatcher, EventSpy) { + let contract = snf::declare('appchain'); + let calldata = array![owner, state_root, block_number, block_hash]; let contract_address = contract.deploy(@calldata).unwrap(); let mut spy = snf::spy_events(SpyOn::One(contract_address)); @@ -114,7 +128,12 @@ fn appchain_owner_only() { #[test] fn update_state_ok() { - let (appchain, _spy) = deploy_with_owner(c::OWNER().into()); + let (appchain, mut _spy) = deploy_with_owner_and_state( + owner: c::OWNER().into(), + state_root: 2308509181970242579758367820250590423941246005755407149765148974993919671160, + block_number: 535682, + block_hash: 0 + ); let imsg = IMessagingDispatcher { contract_address: appchain.contract_address }; @@ -154,6 +173,25 @@ fn update_state_ok() { snf::start_prank(CheatTarget::One(appchain.contract_address), c::OWNER()); appchain.update_state(output); + let expected_log_state_update = LogStateUpdate { + state_root: 1400208033537979038273563301858781654076731580449174584651309975875760580865, + block_number: 535683, + block_hash: 2885081770536693045243577840233106668867645710434679941076039698247255604327 + }; + + let expected_state_transition_fact = LogStateTransitionFact { state_transition_fact: 0 }; + + _spy + .assert_emitted( + @array![ + (appchain.contract_address, Event::LogStateUpdate(expected_log_state_update)), + ( + appchain.contract_address, + Event::LogStateTransitionFact(expected_state_transition_fact) + ) + ] + ); + snf::start_prank(CheatTarget::One(appchain.contract_address), contract_sn); imsg.consume_message_from_appchain(contract_appc, payload_appc_to_sn); }