Skip to content

Commit

Permalink
[RFC][move] Implement user-defined events emitted by FastX modules
Browse files Browse the repository at this point in the history
Simple approach to events based on the discussion in Slack. Feedback welcome/desired!

- Introduce the `Event` module, which has an `event` function allowing an end-user to emit any struct with the `copy` and `drop` abilities as an event. The ability restrictions prevent a user from accidentally or intentionally destroying an asset.
- Add an example in the Hero code to demonstrate how events can be used: emit an event every time a Boar is slain with the ID of the hero + dead boar, and the address of the user that owns the Hero. An external system could process these events to (e.g.) create a boar-slaying leaderboard. Maybe we can show that in the Hero demo.
- Change the adapter to distinguish between user-defined events and system events. User-defined events get written to a new field in the authority temporary store.
- Clients will see an ordered log of the events emitted by a particular tx in `TransactionEffects`.
- The authority durably stores the events inside `signed_effects: Map<TransactionDigest,TransactionEffects}`.
  • Loading branch information
sblackshear committed Jan 29, 2022
1 parent 8d5c75e commit c26969d
Show file tree
Hide file tree
Showing 18 changed files with 257 additions and 134 deletions.
3 changes: 2 additions & 1 deletion fastpay_core/src/authority/authority_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,8 @@ impl AuthorityStore {
signed_effects: SignedOrderEffects,
) -> Result<OrderInfoResponse, FastPayError> {
// Extract the new state from the execution
let (objects, active_inputs, written, deleted) = temporary_store.into_inner();
// TODO: events are already stored in the TxDigest -> TransactionEffects store. Is that enough?
let (objects, active_inputs, written, deleted, _events) = temporary_store.into_inner();
let mut write_batch = self.order_lock.batch();

// Archive the old lock.
Expand Down
20 changes: 19 additions & 1 deletion fastpay_core/src/authority/temporary_store.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use fastx_types::event::Event;

use super::*;

pub type InnerTemporaryStore = (
BTreeMap<ObjectID, Object>,
Vec<ObjectRef>,
BTreeMap<ObjectID, Object>,
BTreeSet<ObjectID>,
Vec<Event>,
);

pub struct AuthorityTemporaryStore {
Expand All @@ -16,6 +19,8 @@ pub struct AuthorityTemporaryStore {
// Object reference calculation involves hashing which could be expensive.
written: BTreeMap<ObjectID, Object>, // Objects written
deleted: BTreeSet<ObjectID>, // Objects actively deleted
/// Ordered sequence of events emitted by execution
events: Vec<Event>,
}

impl AuthorityTemporaryStore {
Expand All @@ -35,6 +40,7 @@ impl AuthorityTemporaryStore {
.collect(),
written: BTreeMap::new(),
deleted: BTreeSet::new(),
events: Vec::new(),
}
}

Expand All @@ -58,7 +64,13 @@ impl AuthorityTemporaryStore {
{
self.check_invariants();
}
(self.objects, self.active_inputs, self.written, self.deleted)
(
self.objects,
self.active_inputs,
self.written,
self.deleted,
self.events,
)
}

/// For every object from active_inputs (i.e. all mutable objects), if they are not
Expand Down Expand Up @@ -106,6 +118,7 @@ impl AuthorityTemporaryStore {
.map(|id| self.objects[id].to_object_reference())
.collect(),
gas_object: (gas_object.to_object_reference(), gas_object.owner),
events: self.events.clone(),
};
let signature = Signature::new(&effects, secret);

Expand Down Expand Up @@ -160,6 +173,7 @@ impl Storage for AuthorityTemporaryStore {
self.active_inputs.clear();
self.written.clear();
self.deleted.clear();
self.events.clear();
}

fn read_object(&self, id: &ObjectID) -> Option<Object> {
Expand Down Expand Up @@ -211,6 +225,10 @@ impl Storage for AuthorityTemporaryStore {
debug_assert!(self.objects.get(id).is_some());
self.deleted.insert(*id);
}

fn log_event(&mut self, event: Event) {
self.events.push(event)
}
}

impl ModuleResolver for AuthorityTemporaryStore {
Expand Down
4 changes: 4 additions & 0 deletions fastpay_core/src/unit_tests/authority_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,10 @@ async fn test_hero() {
)
.await;
assert_eq!(effects.status, ExecutionStatus::Success);
let events = effects.events;
// should emit one BoarSlainEvent
assert_eq!(events.len(), 1);
assert_eq!(events[0].type_.name.to_string(), "BoarSlainEvent")
}

// helpers
Expand Down
116 changes: 82 additions & 34 deletions fastx_programmability/adapter/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
use anyhow::Result;

use crate::bytecode_rewriter::ModuleHandleRewriter;
use fastx_framework::EventType;
use fastx_types::{
base_types::{
FastPayAddress, ObjectID, TxContext, TX_CONTEXT_MODULE_NAME, TX_CONTEXT_STRUCT_NAME,
FastPayAddress, ObjectID, TransactionDigest, TxContext, TX_CONTEXT_MODULE_NAME,
TX_CONTEXT_STRUCT_NAME,
},
error::{FastPayError, FastPayResult},
event::Event,
gas,
messages::ExecutionStatus,
object::{Data, MoveObject, Object},
Expand Down Expand Up @@ -296,7 +299,7 @@ pub fn generate_package_id(
Ok(package_id)
}

type Event = (Vec<u8>, u64, TypeTag, Vec<u8>);
type MoveEvent = (Vec<u8>, u64, TypeTag, Vec<u8>);

/// Update `state_view` with the effects of successfully executing a transaction:
/// - Look for each input in `by_value_objects` to determine whether the object was transferred, frozen, or deleted
Expand All @@ -311,7 +314,7 @@ fn process_successful_execution<
state_view: &mut S,
mut by_value_objects: BTreeMap<ObjectID, Object>,
mutable_refs: impl Iterator<Item = (Object, Vec<u8>)>,
events: Vec<Event>,
events: Vec<MoveEvent>,
ctx: &TxContext,
) -> (u64, u64) {
for (mut obj, new_contents) in mutable_refs {
Expand All @@ -324,38 +327,38 @@ fn process_successful_execution<
}
// process events to identify transfers, freezes
let mut gas_used = 0;
let tx_digest = ctx.digest();
for e in events {
let (recipient, should_freeze, type_, event_bytes) = e;
debug_assert!(!recipient.is_empty() && should_freeze < 2);
match type_ {
TypeTag::Struct(s_type) => {
let contents = event_bytes;
let should_freeze = should_freeze != 0;
// unwrap safe due to size enforcement in Move code for `Authenticator
let recipient = FastPayAddress::try_from(recipient.borrow()).unwrap();
let mut move_obj = MoveObject::new(s_type, contents);
let old_object = by_value_objects.remove(&move_obj.id());

#[cfg(debug_assertions)]
{
check_transferred_object_invariants(&move_obj, &old_object)
}

// increment the object version. note that if the transferred object was
// freshly created, this means that its version will now be 1.
// thus, all objects in the global object pool have version > 0
move_obj.increment_version();
if should_freeze {
move_obj.freeze();
}
let obj = Object::new_move(move_obj, recipient, ctx.digest());
if old_object.is_none() {
// Charge extra gas based on object size if we are creating a new object.
gas_used += gas::calculate_object_creation_cost(&obj);
}
state_view.write_object(obj);
}
_ => unreachable!("Only structs can be transferred"),
let (recipient, event_type, type_, event_bytes) = e;
match EventType::try_from(event_type as u8)
.expect("Safe because event_type is derived from an EventType enum")
{
EventType::Transfer => handle_transfer(
recipient,
type_,
event_bytes,
false, /* should_freeze */
tx_digest,
&mut by_value_objects,
&mut gas_used,
state_view,
),
EventType::TransferAndFreeze => handle_transfer(
recipient,
type_,
event_bytes,
true, /* should_freeze */
tx_digest,
&mut by_value_objects,
&mut gas_used,
state_view,
),
EventType::User => match type_ {
TypeTag::Struct(s) => state_view.log_event(Event::new(s, event_bytes)),
_ => unreachable!(
"Native function emit_event<T> ensures that T is always bound to structs"
),
},
}
}

Expand All @@ -372,6 +375,51 @@ fn process_successful_execution<
(gas_used, gas_refund)
}

#[allow(clippy::too_many_arguments)]
fn handle_transfer<
E: Debug,
S: ResourceResolver<Error = E> + ModuleResolver<Error = E> + Storage,
>(
recipient: Vec<u8>,
type_: TypeTag,
contents: Vec<u8>,
should_freeze: bool,
tx_digest: TransactionDigest,
by_value_objects: &mut BTreeMap<ObjectID, Object>,
gas_used: &mut u64,
state_view: &mut S,
) {
debug_assert!(!recipient.is_empty());
match type_ {
TypeTag::Struct(s_type) => {
// unwrap safe due to size enforcement in Move code for `Authenticator
let recipient = FastPayAddress::try_from(recipient.borrow()).unwrap();
let mut move_obj = MoveObject::new(s_type, contents);
let old_object = by_value_objects.remove(&move_obj.id());

#[cfg(debug_assertions)]
{
check_transferred_object_invariants(&move_obj, &old_object)
}

// increment the object version. note that if the transferred object was
// freshly created, this means that its version will now be 1.
// thus, all objects in the global object pool have version > 0
move_obj.increment_version();
if should_freeze {
move_obj.freeze();
}
let obj = Object::new_move(move_obj, recipient, tx_digest);
if old_object.is_none() {
// Charge extra gas based on object size if we are creating a new object.
*gas_used += gas::calculate_object_creation_cost(&obj);
}
state_view.write_object(obj);
}
_ => unreachable!("Only structs can be transferred"),
}
}

struct TypeCheckSuccess {
module_id: ModuleId,
args: Vec<Vec<u8>>,
Expand Down
15 changes: 15 additions & 0 deletions fastx_programmability/adapter/src/unit_tests/adapter_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct ScratchPad {
updated: BTreeMap<ObjectID, Object>,
created: BTreeMap<ObjectID, Object>,
deleted: Vec<ObjectID>,
events: Vec<Event>,
}

#[derive(Default, Debug)]
Expand Down Expand Up @@ -87,6 +88,10 @@ impl InMemoryStorage {
&self.temporary.deleted
}

pub fn events(&self) -> &[Event] {
&self.temporary.events
}

pub fn get_created_keys(&self) -> Vec<ObjectID> {
self.temporary.created.keys().cloned().collect()
}
Expand All @@ -111,6 +116,10 @@ impl Storage for InMemoryStorage {
}
}

fn log_event(&mut self, event: Event) {
self.temporary.events.push(event)
}

// buffer delete
fn delete_object(&mut self, id: &ObjectID) {
self.temporary.deleted.push(*id)
Expand Down Expand Up @@ -298,6 +307,12 @@ fn test_object_basics() {
assert_eq!(storage.updated().len(), 2);
assert!(storage.created().is_empty());
assert!(storage.deleted().is_empty());
// test than an event was emitted as expected
assert_eq!(storage.events().len(), 1);
assert_eq!(
storage.events()[0].clone().type_.name.to_string(),
"NewValueEvent"
);
storage.flush();
let updated_obj = storage.read_object(&id1).unwrap();
assert_eq!(updated_obj.owner, addr2);
Expand Down
23 changes: 20 additions & 3 deletions fastx_programmability/examples/sources/Hero.move
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ module Examples::Hero {
use Examples::TrustedCoin::EXAMPLE;
use FastX::Address::{Self, Address};
use FastX::Coin::{Self, Coin};
use FastX::ID::ID;
use FastX::Event;
use FastX::ID::{Self, ID, IDBytes};
use FastX::Math;
use FastX::Transfer;
use FastX::TxContext::{Self, TxContext};
Expand Down Expand Up @@ -56,6 +57,16 @@ module Examples::Hero {
potions_created: u64
}

/// Event emitted each time a Hero slays a Boar
struct BoarSlainEvent has copy, drop {
/// Address of the user that slayed the boar
slayer_address: Address,
/// ID of the Hero that slayed the boar
hero: IDBytes,
/// ID of the now-deceased boar
boar: IDBytes,
}

/// Address of the admin account that receives payment for swords
const ADMIN: vector<u8> = vector[189, 215, 127, 86, 129, 189, 1, 4, 90, 106, 17, 10, 123, 200, 40, 18, 34, 173, 240, 91, 213, 72, 183, 249, 213, 210, 39, 181, 105, 254, 59, 163];
/// Upper bound on player's HP
Expand Down Expand Up @@ -101,8 +112,8 @@ module Examples::Hero {

/// Slay the `boar` with the `hero`'s sword, get experience.
/// Aborts if the hero has 0 HP or is not strong enough to slay the boar
public fun slay(hero: &mut Hero, boar: Boar, _ctx: &mut TxContext) {
let Boar { id: _, strength: boar_strength, hp } = boar;
public fun slay(hero: &mut Hero, boar: Boar, ctx: &mut TxContext) {
let Boar { id: boar_id, strength: boar_strength, hp } = boar;
let hero_strength = hero_strength(hero);
let boar_hp = hp;
let hero_hp = hero.hp;
Expand All @@ -124,6 +135,12 @@ module Examples::Hero {
if (Option::is_some(&hero.sword)) {
level_up_sword(Option::borrow_mut(&mut hero.sword), 1)
};
// let the world know about the hero's triumph by emitting an event!
Event::emit(BoarSlainEvent {
slayer_address: TxContext::get_signer_address(ctx),
hero: *ID::get_inner(&hero.id),
boar: *ID::get_inner(&boar_id),
})
}

/// Strength of the hero when attacking
Expand Down
1 change: 1 addition & 0 deletions fastx_programmability/framework/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ publish = false
[dependencies]
anyhow = "1.0.52"
smallvec = "1.7.0"
num_enum = "0.5.6"

fastx-types = { path = "../../fastx_types" }
fastx-verifier = { path = "../verifier" }
Expand Down
Loading

0 comments on commit c26969d

Please sign in to comment.