Convention
Convention (/convention)
The zkWasm Convention Library provides essential traits and implementations for building zkWasm applications with standardized state management, event handling, and settlement processing. This library serves as the foundation for creating consistent and reliable zkWasm applications.
Core Components
CommonState Trait
A fundamental trait that defines the interface for managing application state.
pub trait CommonState: Serialize + StorageData + Sized {
type PlayerData: StorageData + Default + Serialize;
// ... implementation methods
// Global State Management
fn get_global<'a>() -> Ref<'a, Self>;
fn get_global_mut<'a>() -> RefMut<'a, Self>;
fn snapshot() -> String
// Player State Management
fn get_state(pkey: Vec<u64>) -> String
// Rand Seed
fn rand_seed() -> u64
// Rollup State Management
fn preempt() -> bool
fn store(&self)
fn initialize()
}
Key Features
- Global State Management: Access and modify global application state
- Player State Handling: Manage individual player states
- State Serialization: Convert states to/from JSON format
- State Persistence: Store and retrieve state from merkle tree storage
- Rollup State Management: Handle rollup-specific state operations
By implementing this trait, you can ensure that your application adheres to a consistent structure and can be easily integrated with the zkWasm Mini Rollup.
Settlement
The code below manages withdrawal information and settlement processing, where append_settlement
is used to add a new withdrawal to the list, and flush_settlement
process and clear all pending settlements, it returns the transaction data to be sent to the blockchain for settlement.
use zkwasm_rest_abi::WithdrawInfo;
pub struct SettlementInfo(Vec<WithdrawInfo>);
pub static mut SETTLEMENT: SettlementInfo = SettlementInfo(vec![]);
impl SettlementInfo {
pub fn append_settlement(info: WithdrawInfo) {
unsafe { SETTLEMENT.0.push(info) };
}
pub fn flush_settlement() -> Vec<u8> {
zkwasm_rust_sdk::dbg!("flush settlement\n");
let sinfo = unsafe { &mut SETTLEMENT };
let mut bytes: Vec<u8> = Vec::with_capacity(sinfo.0.len() * 32);
for s in &sinfo.0 {
s.flush(&mut bytes);
}
sinfo.0 = vec![];
bytes
}
}
Event
Event are used to handle time-based events, such as timers, scheduled actions, or periodic updates, as well as their side effects.
There are two things important in the event handling:
-
The EventHandler Trait defines the interface for handling time-based events
pub trait EventHandler: Clone + StorageData { fn get_delta(&self) -> usize; // get the delta time of the event fn progress(&mut self, d: usize); // progress the event by the given delta time fn handle(&mut self, counter: u64) -> Option<Self>; // handle the event and maybe return the next event fn u64size() -> usize; // get the size (fields) of the event type in u64 }
-
The EventQueue Struct: implements a differential time queue (DTQ) for efficient event scheduling and processing.
pub struct EventQueue<T: EventHandler + Sized> { pub counter: u64, // the counter of the event queue represents the total number of timeticks. pub list: std::collections::LinkedList<T>, // the event queue }
In an EventQueue which implements the EventHandler Trait, we have serveral methods to handle the events:
dump
fn dump(&self, counter: u64) {
zkwasm_rust_sdk::dbg!("dump queue: {}, ", counter);
for m in self.list.iter() {
let delta = m.get_delta();
zkwasm_rust_sdk::dbg!(" {}", delta);
}
zkwasm_rust_sdk::dbg!("\n");
}
insert
/// Insert a event into the event queue
/// The event queue is a differential time queue (DTQ) and the event will
/// be inserted into its proper position based on its delta time
pub fn insert(&mut self, node: E) {
let mut event = node.clone();
let mut cursor = self.list.cursor_front_mut();
while cursor.current().is_some()
&& cursor.current().as_ref().unwrap().get_delta() <= event.get_delta()
{
event.progress(cursor.current().as_ref().unwrap().get_delta());
cursor.move_next();
}
match cursor.current() {
Some(t) => {
t.progress(event.get_delta());
}
None => (),
};
cursor.insert_before(event);
}
The above code is used to insert a event into the event queue, the event will be inserted into its proper position based on its delta time.
Info
The event queue is a differential time queue (DTQ), which means in the event queue, the events are sorted by their delta time. For example:
- Initial queue state (numbers represent delta time):
[2] -> [3] -> [4]
- When inserting an event with delta=5:
[2] -> [3] -> [4] -> [5]
- When inserting an event with delta=1:
[1] -> [2] -> [3] -> [4] -> [5]
Note: The deltas are adjusted during insertion to maintain relative time differences.
Key characteristics of DTQ:
- Each node stores the time difference from its previous node
- Total time to an event = sum of deltas from start to that event
- Efficient for time-based event scheduling
- Maintains sorted order automatically
- Updates are O(n) in worst case but typically much faster in practice
tick
The tick()
method is the core processing function of the event queue, responsible for advancing time and handling events. Each call increments the counter by 1 and processes all due events.
The processing flow consists of four main steps:
-
Retrieve and Process Historical Events
/// Perform tick: /// 1. get old entries and peform event handlers on each event /// 2. insert new generated events into the event queue /// 3. handle all events whose counter are zero /// 4. insert new generated envets into the event queue pub fn tick(&mut self) { let counter = self.counter; self.dump(counter); let mut entries_data = self.get_old_entries(counter); let entries_nb = entries_data.len() / E::u64size(); let mut dataiter = entries_data.iter_mut(); let mut entries = Vec::with_capacity(entries_nb); ....// handle the events self.counter += 1; }
The above code get the historical events data by
get_old_entries
, and calculate the number of the events by using divide the length of the data by the size of the event type in u64, for example:struct MyEvent { field1: u64, // takes 1 u64 field2: u64, // takes 1 u64 } impl EventHandler for MyEvent { fn u64size() -> usize { 2 // Each MyEvent takes 2 u64s to store } // ... other implementations } // If entries_data contains [1, 2, 3, 4, 5, 6] (6 u64 values) // And each MyEvent takes 2 u64s // Then entries_nb = 6 / 2 = 3 events
After getting the number of the events, a entries vector is created to store the historical events.
let mut entries = Vec::with_capacity(entries_nb); for _ in 0..entries_nb { entries.push(E::from_data(&mut dataiter)); }
Then, the code will iterate over the historical or existing events and call the
handle
method of each event:for mut e in entries { let m = e.handle(counter); if let Some(event) = m { self.insert(event); } }
-
Handle the events in the event queue
while let Some(head) = self.list.front_mut() { if head.get_delta() == 0 { let m = head.handle(counter); self.list.pop_front(); if let Some(event) = m { self.insert(event); } } else { head.progress(1); break; } }
The above code is straightforward and intuitive, it will iterate over the events in the event queue, if the delta of the event is 0, it will call the
handle
method of the event, probably insert a new event into the event queue (if there is a new event generated), and then pop the event from the event queue. If the delta of the event is not 0, it will progress the event by 1 (the delta of the event will be reduced by 1).
You can also notice that we have other implementations of the EventHandler
trait, such as:
impl<T: EventHandler + Sized> StorageData for EventQueue<T> {
fn to_data(&self, buf: &mut Vec<u64>)
fn from_data(u64data: &mut IterMut<u64>) -> Self
}
Where to_data
is used to convert the event queue to a u64 array and store it in the storage, and from_data
is used to convert the u64 array to an event queue from the storage.
And:
impl<T: EventHandler + Sized> EventQueue<T>{
fn get_old_entries(&self, counter: u64) -> Vec<u64>
fn set_entries(&self, entries: &Vec<u64>, counter: u64)
pub fn store(&mut self)
}
Where get_old_entries
is used to get the historical events data by the given counter, set_entries
is used to set the existing events data by the given counter, and store
is used to store the event queue to the storage, which is a merkle key-value pair storage:
// In impl<T: EventHandler + Sized> EventQueue<T>{..}
fn set_entries(&self, entries: &Vec<u64>, counter: u64) {
let kvpair = unsafe { &mut MERKLE_MAP };
kvpair.set(
&[counter & 0xeffffff, EVENTS_LEAF_INDEX, 0, EVENTS_LEAF_INDEX],
entries.as_slice(),
);
zkwasm_rust_sdk::dbg!("store {} entries at counter {}", { entries.len() }, counter);
}
The above code can store all the events that shall be processed in a same specific counter to the storage. This is particularly useful to save the memory space and improve the performance.
Info
We will cover how to leverage the event queue and time-based events in Implementing Time-Driven Events chapter.