Skip to content

Commit

Permalink
feat: State update component / events (#20)
Browse files Browse the repository at this point in the history
* Added new component state and related events + testing

* Implemented state update method + testing
  • Loading branch information
thomas192 authored Mar 8, 2024
1 parent 0d8af6e commit 3579a23
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 5 deletions.
48 changes: 46 additions & 2 deletions src/appchain.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
);
Expand All @@ -43,6 +46,8 @@ mod appchain {
impl ConfigImpl = config_cpt::ConfigImpl<ContractState>;
#[abi(embed_v0)]
impl MessagingImpl = messaging_cpt::MessagingImpl<ContractState>;
#[abi(embed_v0)]
impl StateImpl = state_cpt::StateImpl<ContractState>;

#[storage]
struct Storage {
Expand All @@ -54,6 +59,8 @@ mod appchain {
messaging: messaging_cpt::Storage,
#[substorage(v0)]
reentrancy_guard: ReentrancyGuardComponent::Storage,
#[substorage(v0)]
state: state_cpt::Storage,
}

#[event]
Expand All @@ -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.
Expand All @@ -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)]
Expand All @@ -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(
Expand All @@ -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(),
}
);
}
}
}
15 changes: 15 additions & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion src/snos_output.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const MESSAGE_TO_APPCHAIN_HEADER_SIZE: usize = 5;
/// <https://github.com/starkware-libs/cairo-lang/blob/caba294d82eeeccc3d86a158adb8ba209bf2d8fc/src/starkware/starknet/core/os/output.cairo#L52>.
/// The names are taken from SNOS repository:
/// <https://github.com/keep-starknet-strange/snos/blob/ad9a7df5fdbb63c813db285346eb667e032762e0/src/io/output.rs#L17>.
#[derive(Serde)]
#[derive(Drop, Serde)]
struct ProgramOutput {
/// The state commitment before this block.
prev_state_root: felt252,
Expand Down
84 changes: 84 additions & 0 deletions src/state/component.cairo
Original file line number Diff line number Diff line change
@@ -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<TContractState>,
> of IState<ComponentState<TContractState>> {
fn update(ref self: ComponentState<TContractState>, program_output: Span<felt252>,) {
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<TContractState>) -> (StateRoot, BlockNumber, BlockHash) {
(self.state_root.read(), self.block_number.read(), self.block_hash.read())
}
}

#[generate_trait]
impl InternalImpl<
TContractState, +HasComponent<TContractState>,
> of InternalTrait<TContractState> {
/// 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<TContractState>,
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);
}
}
}
21 changes: 21 additions & 0 deletions src/state/interface.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! SPDX-License-Identifier: MIT
//!
//! Interface for Appchain - Starknet state.

#[starknet::interface]
trait IState<T> {
/// 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<felt252>,);

/// Gets the current state.
///
/// # Returns
///
/// The state root, the block number and the block hash.
fn get_state(self: @T) -> (felt252, felt252, felt252);
}
29 changes: 29 additions & 0 deletions src/state/mock.cairo
Original file line number Diff line number Diff line change
@@ -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<ContractState>;

#[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);
}
}
84 changes: 84 additions & 0 deletions src/state/tests/test_state.cairo
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 3579a23

Please sign in to comment.