use alloc::{
collections::{BTreeMap, btree_map::Iter},
vec::Vec,
};
use anyhow::{Result, anyhow};
use core::num::NonZero;
use ethexe_common::{
MAILBOX_VALIDITY_VERSION_2, ProgramStates, Schedule, ScheduledTask, StateHashWithQueueSize,
gear::{Message, StateTransition, ValueClaim},
};
use gprimitives::{ActorId, CodeId, H256};
pub(crate) const GEAR_SAILS_EVENT: ActorId = ActorId::new([0; 32]);
pub(crate) const ETH_SAILS_EVENT: ActorId = ActorId::new([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
]);
pub(crate) fn is_event_destination(destination: ActorId) -> bool {
matches!(destination, GEAR_SAILS_EVENT | ETH_SAILS_EVENT)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransitionsConfig {
pub block_height: u32,
pub mailbox_validity: NonZero<u32>,
pub event_destinations_autoreply: bool,
}
impl Default for TransitionsConfig {
fn default() -> Self {
Self {
block_height: 0,
mailbox_validity: MAILBOX_VALIDITY_VERSION_2,
event_destinations_autoreply: false,
}
}
}
#[derive(Debug, Default)]
pub struct InBlockTransitions {
cfg: TransitionsConfig,
states: ProgramStates,
schedule: Schedule,
modifications: BTreeMap<ActorId, NonFinalTransition>,
program_creations: BTreeMap<ActorId, CodeId>,
}
#[derive(Debug, Clone, Default)]
pub struct FinalizedBlockTransitions {
pub transitions: Vec<StateTransition>,
pub states: ProgramStates,
pub schedule: Schedule,
pub program_creations: Vec<(ActorId, CodeId)>,
}
impl InBlockTransitions {
pub fn new(cfg: TransitionsConfig, states: ProgramStates, schedule: Schedule) -> Self {
Self {
cfg,
states,
schedule,
..Default::default()
}
}
pub fn is_program(&self, actor_id: &ActorId) -> bool {
self.states.contains_key(actor_id)
}
pub fn state_of(&self, actor_id: &ActorId) -> Option<StateHashWithQueueSize> {
self.states.get(actor_id).copied()
}
pub fn states_amount(&self) -> usize {
self.states.len()
}
pub fn states_iter(&self) -> Iter<'_, ActorId, StateHashWithQueueSize> {
self.states.iter()
}
pub fn known_programs(&self) -> Vec<ActorId> {
self.states.keys().copied().collect()
}
pub fn current_messages(&self) -> Vec<(ActorId, Message)> {
self.modifications
.iter()
.flat_map(|(id, trans)| trans.messages.iter().map(|message| (*id, message.clone())))
.collect()
}
pub fn modifications_len(&self) -> usize {
self.modifications.len()
}
pub fn take_actual_tasks(&mut self) -> Vec<ScheduledTask> {
let cutoff = self.cfg.block_height.saturating_add(1);
let kept = self.schedule.split_off(&cutoff);
let due = core::mem::replace(&mut self.schedule, kept);
due.into_values().flatten().collect()
}
pub fn schedule_task(&mut self, in_blocks: NonZero<u32>, task: ScheduledTask) -> u32 {
let scheduled_block = self.cfg.block_height + u32::from(in_blocks);
self.schedule
.entry(scheduled_block)
.or_default()
.insert(task);
scheduled_block
}
pub fn remove_task(&mut self, expiry: u32, task: &ScheduledTask) -> Result<()> {
let block_tasks = self
.schedule
.get_mut(&expiry)
.ok_or_else(|| anyhow!("No tasks found scheduled for a given block"))?;
block_tasks
.remove(task)
.then_some(())
.ok_or_else(|| anyhow!("Requested task wasn't found scheduled for a given block"))?;
if block_tasks.is_empty() {
self.schedule.remove(&expiry);
}
Ok(())
}
pub fn register_new(&mut self, actor_id: ActorId, code_id: CodeId) {
self.states.insert(actor_id, StateHashWithQueueSize::zero());
self.modifications.insert(actor_id, Default::default());
self.program_creations.insert(actor_id, code_id);
}
pub fn registered_programs(&self) -> &BTreeMap<ActorId, CodeId> {
&self.program_creations
}
pub fn modify_state(
&mut self,
actor_id: ActorId,
new_state_hash: H256,
canonical_queue_size: u8,
injected_queue_size: u8,
) {
self.modify(actor_id, |state, _transition| {
state.hash = new_state_hash;
state.canonical_queue_size = canonical_queue_size;
state.injected_queue_size = injected_queue_size;
})
}
pub fn modify_transition<T>(
&mut self,
actor_id: ActorId,
f: impl FnOnce(&mut NonFinalTransition) -> T,
) -> T {
self.modify(actor_id, |_state, transition| f(transition))
}
pub fn claim_value(&mut self, actor_id: ActorId, claim: ValueClaim) {
self.modify(actor_id, |_state, transition| {
transition.value_to_receive = transition
.value_to_receive
.checked_add(
i128::try_from(claim.value).expect("claimed_value doesn't fit in i128"),
)
.expect("Overflow in transition.value_to_receive += claimed_value");
transition.claims.push(claim);
});
}
pub fn modify<T>(
&mut self,
actor_id: ActorId,
f: impl FnOnce(&mut StateHashWithQueueSize, &mut NonFinalTransition) -> T,
) -> T {
let initial_state = self
.states
.get_mut(&actor_id)
.expect("couldn't modify transition for unknown actor");
let transition = self
.modifications
.entry(actor_id)
.or_insert(NonFinalTransition {
initial_state: initial_state.hash,
..Default::default()
});
f(initial_state, transition)
}
pub fn finalize(self) -> FinalizedBlockTransitions {
let Self {
states,
schedule,
modifications,
program_creations,
..
} = self;
let mut transitions = Vec::with_capacity(modifications.len());
for (actor_id, modification) in modifications {
let new_state = states
.get(&actor_id)
.cloned()
.expect("failed to find state record for modified state");
if !modification.is_noop(new_state.hash) {
transitions.push(StateTransition {
actor_id,
new_state_hash: new_state.hash,
exited: modification.inheritor.is_some(),
inheritor: modification.inheritor.unwrap_or_default(),
value_to_receive: modification.value_to_receive.unsigned_abs(),
value_to_receive_negative_sign: modification.value_to_receive < 0,
value_claims: modification.claims,
messages: modification.messages,
});
}
}
FinalizedBlockTransitions {
transitions,
states,
schedule,
program_creations: program_creations.into_iter().collect(),
}
}
pub fn cfg(&self) -> &TransitionsConfig {
&self.cfg
}
#[cfg(any(test, feature = "mock"))]
pub fn from_parts(
cfg: TransitionsConfig,
states: ProgramStates,
schedule: Schedule,
modifications: BTreeMap<ActorId, NonFinalTransition>,
program_creations: BTreeMap<ActorId, CodeId>,
) -> Self {
Self {
cfg,
states,
schedule,
modifications,
program_creations,
}
}
#[cfg(any(test, feature = "mock"))]
pub fn modifications_mut(&mut self) -> &mut BTreeMap<ActorId, NonFinalTransition> {
&mut self.modifications
}
#[cfg(any(test, feature = "mock"))]
pub fn cfg_mut(&mut self) -> &mut TransitionsConfig {
&mut self.cfg
}
}
#[derive(Debug, Default, Clone)]
pub struct NonFinalTransition {
initial_state: H256,
pub inheritor: Option<ActorId>,
pub value_to_receive: i128,
pub claims: Vec<ValueClaim>,
pub messages: Vec<Message>,
}
impl NonFinalTransition {
pub fn is_noop(&self, current_state: H256) -> bool {
!self.initial_state.is_zero()
&& current_state == self.initial_state
&& (self.inheritor.is_none() && self.value_to_receive == 0 && self.claims.is_empty() && self.messages.is_empty())
}
#[cfg(any(test, feature = "mock"))]
pub fn initial_state(&self) -> H256 {
self.initial_state
}
#[cfg(any(test, feature = "mock"))]
pub fn new(
initial_state: H256,
inheritor: Option<ActorId>,
value_to_receive: i128,
claims: Vec<ValueClaim>,
messages: Vec<Message>,
) -> Self {
Self {
initial_state,
inheritor,
value_to_receive,
claims,
messages,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::collections::BTreeSet;
use ethexe_common::ScheduledTask;
use gprimitives::MessageId;
fn wake(actor: u8, msg: u8) -> ScheduledTask {
ScheduledTask::WakeMessage(ActorId::from([actor; 32]), MessageId::from([msg; 32]))
}
fn transitions_with_schedule(block_height: u32, schedule: Schedule) -> InBlockTransitions {
let cfg = TransitionsConfig {
block_height,
..Default::default()
};
InBlockTransitions::new(cfg, ProgramStates::default(), schedule)
}
#[test]
fn take_actual_tasks_single_height() {
let mut schedule = Schedule::new();
schedule
.entry(10)
.or_default()
.extend([wake(1, 1), wake(2, 2)]);
let mut t = transitions_with_schedule(10, schedule);
let drained = t.take_actual_tasks();
let drained: BTreeSet<_> = drained.into_iter().collect();
assert_eq!(drained, BTreeSet::from([wake(1, 1), wake(2, 2)]));
assert!(t.schedule.is_empty(), "all due heights drained");
}
#[test]
fn take_actual_tasks_drains_past_heights_keeps_future() {
let mut schedule = Schedule::new();
schedule.entry(5).or_default().insert(wake(1, 1));
schedule.entry(8).or_default().insert(wake(2, 2));
schedule.entry(10).or_default().insert(wake(3, 3));
schedule.entry(15).or_default().insert(wake(4, 4));
schedule.entry(20).or_default().insert(wake(5, 5));
let mut t = transitions_with_schedule(10, schedule);
let drained = t.take_actual_tasks();
assert_eq!(drained, vec![wake(1, 1), wake(2, 2), wake(3, 3)]);
assert_eq!(t.schedule.len(), 2);
assert!(t.schedule.contains_key(&15));
assert!(t.schedule.contains_key(&20));
}
#[test]
fn take_actual_tasks_ordering_is_height_major() {
let mut schedule = Schedule::new();
schedule.entry(20).or_default().insert(wake(0, 9));
schedule
.entry(5)
.or_default()
.extend([wake(2, 2), wake(1, 1)]);
schedule.entry(15).or_default().insert(wake(3, 3));
let mut t = transitions_with_schedule(20, schedule);
let drained = t.take_actual_tasks();
assert_eq!(
drained,
vec![wake(1, 1), wake(2, 2), wake(3, 3), wake(0, 9)]
);
assert!(t.schedule.is_empty());
}
#[test]
fn take_actual_tasks_empty() {
let mut t = transitions_with_schedule(42, Schedule::new());
assert!(t.take_actual_tasks().is_empty());
}
#[test]
fn take_actual_tasks_at_genesis() {
let mut schedule = Schedule::new();
schedule.entry(0).or_default().insert(wake(1, 1));
schedule.entry(1).or_default().insert(wake(2, 2));
let mut t = transitions_with_schedule(0, schedule);
assert_eq!(t.take_actual_tasks(), vec![wake(1, 1)]);
assert!(t.schedule.contains_key(&1));
}
#[test]
fn ethereum_event_destination_matches_eth_event_addr() {
use gprimitives::H160;
let eth_event_addr = H160::repeat_byte(0xff);
assert_eq!(ETH_SAILS_EVENT, ActorId::from(eth_event_addr));
assert_ne!(ETH_SAILS_EVENT, ActorId::new([u8::MAX; 32]));
assert!(is_event_destination(ETH_SAILS_EVENT));
}
}