diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index a979814014851..7b7fe99b4dc2e 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -58,8 +58,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("node"), impl_name: create_runtime_str!("substrate-node"), authoring_version: 10, - spec_version: 78, - impl_version: 79, + spec_version: 80, + impl_version: 80, apis: RUNTIME_API_VERSIONS, }; diff --git a/srml/contract/src/exec.rs b/srml/contract/src/exec.rs index 6c55608b093a3..9d63c037c06d7 100644 --- a/srml/contract/src/exec.rs +++ b/srml/contract/src/exec.rs @@ -29,6 +29,9 @@ pub type CallOf = ::Call; pub type MomentOf = ::Moment; pub type SeedOf = ::Hash; +/// A type that represents a topic of an event. At the moment a hash is used. +pub type TopicOf = ::Hash; + #[cfg_attr(test, derive(Debug))] pub struct InstantiateReceipt { pub address: AccountId, @@ -106,8 +109,10 @@ pub trait Ext { /// Returns a reference to the random seed for the current block fn random_seed(&self) -> &SeedOf; - /// Deposit an event. - fn deposit_event(&mut self, data: Vec); + /// Deposit an event with the given topics. + /// + /// There should not be any duplicates in `topics`. + fn deposit_event(&mut self, topics: Vec>, data: Vec); /// Set rent allowance of the contract fn set_rent_allowance(&mut self, rent_allowance: BalanceOf); @@ -189,6 +194,15 @@ impl VmExecResult { } } +/// Struct that records a request to deposit an event with a list of topics. +#[cfg_attr(any(feature = "std", test), derive(Debug, PartialEq, Eq))] +pub struct IndexedEvent { + /// A list of topics this event will be deposited with. + pub topics: Vec, + /// The event to deposit. + pub event: Event, +} + /// A trait that represent a virtual machine. /// /// You can view a virtual machine as something that takes code, an input data buffer, @@ -238,7 +252,7 @@ pub struct ExecutionContext<'a, T: Trait + 'a, V, L> { pub self_trie_id: Option, pub overlay: OverlayAccountDb<'a, T>, pub depth: usize, - pub events: Vec>, + pub events: Vec>, pub calls: Vec<(T::AccountId, T::Call)>, pub config: &'a Config, pub vm: &'a V, @@ -418,7 +432,10 @@ where .into_result()?; // Deposit an instantiation event. - nested.events.push(RawEvent::Instantiated(self.self_account.clone(), dest.clone())); + nested.events.push(IndexedEvent { + event: RawEvent::Instantiated(self.self_account.clone(), dest.clone()), + topics: Vec::new(), + }); (nested.overlay.into_change_set(), nested.events, nested.calls) }; @@ -545,8 +562,10 @@ fn transfer<'a, T: Trait, V: Vm, L: Loader>( if transactor != dest { ctx.overlay.set_balance(transactor, new_from_balance); ctx.overlay.set_balance(dest, new_to_balance); - ctx.events - .push(RawEvent::Transfer(transactor.clone(), dest.clone(), value)); + ctx.events.push(IndexedEvent { + event: RawEvent::Transfer(transactor.clone(), dest.clone(), value), + topics: Vec::new(), + }); } Ok(()) @@ -631,8 +650,11 @@ where &self.timestamp } - fn deposit_event(&mut self, data: Vec) { - self.ctx.events.push(RawEvent::Contract(self.ctx.self_account.clone(), data)); + fn deposit_event(&mut self, topics: Vec, data: Vec) { + self.ctx.events.push(IndexedEvent { + topics, + event: RawEvent::Contract(self.ctx.self_account.clone(), data), + }); } fn set_rent_allowance(&mut self, rent_allowance: BalanceOf) { @@ -659,7 +681,7 @@ where mod tests { use super::{ BalanceOf, ExecFeeToken, ExecutionContext, Ext, Loader, EmptyOutputBuf, TransferFeeKind, TransferFeeToken, - Vm, VmExecResult, InstantiateReceipt, RawEvent, + Vm, VmExecResult, InstantiateReceipt, RawEvent, IndexedEvent, }; use crate::account_db::AccountDb; use crate::gas::GasMeter; @@ -1262,8 +1284,14 @@ mod tests { // there are instantiation event. assert_eq!(ctx.overlay.get_code_hash(&created_contract_address).unwrap(), dummy_ch); assert_eq!(&ctx.events, &[ - RawEvent::Transfer(ALICE, created_contract_address, 100), - RawEvent::Instantiated(ALICE, created_contract_address), + IndexedEvent { + event: RawEvent::Transfer(ALICE, created_contract_address, 100), + topics: Vec::new(), + }, + IndexedEvent { + event: RawEvent::Instantiated(ALICE, created_contract_address), + topics: Vec::new(), + } ]); } ); @@ -1314,9 +1342,18 @@ mod tests { // there are instantiation event. assert_eq!(ctx.overlay.get_code_hash(&created_contract_address).unwrap(), dummy_ch); assert_eq!(&ctx.events, &[ - RawEvent::Transfer(ALICE, BOB, 20), - RawEvent::Transfer(BOB, created_contract_address, 15), - RawEvent::Instantiated(BOB, created_contract_address), + IndexedEvent { + event: RawEvent::Transfer(ALICE, BOB, 20), + topics: Vec::new(), + }, + IndexedEvent { + event: RawEvent::Transfer(BOB, created_contract_address, 15), + topics: Vec::new(), + }, + IndexedEvent { + event: RawEvent::Instantiated(BOB, created_contract_address), + topics: Vec::new(), + }, ]); } ); @@ -1362,7 +1399,10 @@ mod tests { // The contract wasn't created so we don't expect to see an instantiation // event here. assert_eq!(&ctx.events, &[ - RawEvent::Transfer(ALICE, BOB, 20), + IndexedEvent { + event: RawEvent::Transfer(ALICE, BOB, 20), + topics: Vec::new(), + }, ]); } ); diff --git a/srml/contract/src/lib.rs b/srml/contract/src/lib.rs index b6e8c70e8d84b..2516f87045142 100644 --- a/srml/contract/src/lib.rs +++ b/srml/contract/src/lib.rs @@ -393,7 +393,12 @@ decl_module! { DirectAccountDb.commit(ctx.overlay.into_change_set()); // Then deposit all events produced. - ctx.events.into_iter().for_each(Self::deposit_event); + ctx.events.into_iter().for_each(|indexed_event| { + >::deposit_event_indexed( + &*indexed_event.topics, + ::Event::from(indexed_event.event).into(), + ); + }); } // Refund cost of the unused gas. @@ -447,7 +452,12 @@ decl_module! { DirectAccountDb.commit(ctx.overlay.into_change_set()); // Then deposit all events produced. - ctx.events.into_iter().for_each(Self::deposit_event); + ctx.events.into_iter().for_each(|indexed_event| { + >::deposit_event_indexed( + &*indexed_event.topics, + ::Event::from(indexed_event.event).into(), + ); + }); } // Refund cost of the unused gas. @@ -653,8 +663,11 @@ pub struct Schedule { /// Gas cost to deposit an event; the per-byte portion. pub event_data_per_byte_cost: Gas, + /// Gas cost to deposit an event; the cost per topic. + pub event_per_topic_cost: Gas, + /// Gas cost to deposit an event; the base. - pub event_data_base_cost: Gas, + pub event_base_cost: Gas, /// Gas cost per one byte read from the sandbox memory. pub sandbox_data_read_cost: Gas, @@ -662,6 +675,9 @@ pub struct Schedule { /// Gas cost per one byte written to the sandbox memory. pub sandbox_data_write_cost: Gas, + /// The maximum number of topics supported by an event. + pub max_event_topics: u32, + /// Maximum allowed stack height. /// /// See https://wiki.parity.io/WebAssembly-StackHeight to find out @@ -685,9 +701,11 @@ impl> Default for Schedule { regular_op_cost: Gas::sa(1), return_data_per_byte_cost: Gas::sa(1), event_data_per_byte_cost: Gas::sa(1), - event_data_base_cost: Gas::sa(1), + event_per_topic_cost: Gas::sa(1), + event_base_cost: Gas::sa(1), sandbox_data_read_cost: Gas::sa(1), sandbox_data_write_cost: Gas::sa(1), + max_event_topics: 4, max_stack_height: 64 * 1024, max_memory_pages: 16, enable_println: false, diff --git a/srml/contract/src/tests.rs b/srml/contract/src/tests.rs index 149dc4b26bc0e..4eed6777b409e 100644 --- a/srml/contract/src/tests.rs +++ b/srml/contract/src/tests.rs @@ -64,7 +64,7 @@ impl_outer_dispatch! { } } -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, Debug)] pub struct Test; impl system::Trait for Test { type Origin = Origin; @@ -313,14 +313,16 @@ fn account_removal_removes_storage() { const CODE_RETURN_FROM_START_FN: &str = r#" (module (import "env" "ext_return" (func $ext_return (param i32 i32))) - (import "env" "ext_deposit_event" (func $ext_deposit_event (param i32 i32))) + (import "env" "ext_deposit_event" (func $ext_deposit_event (param i32 i32 i32 i32))) (import "env" "memory" (memory 1 1)) (start $start) (func $start (call $ext_deposit_event - (i32.const 8) - (i32.const 4) + (i32.const 0) ;; The topics buffer + (i32.const 0) ;; The topics buffer's length + (i32.const 8) ;; The data buffer + (i32.const 4) ;; The data buffer's length ) (call $ext_return (i32.const 8) @@ -337,7 +339,7 @@ const CODE_RETURN_FROM_START_FN: &str = r#" (data (i32.const 8) "\01\02\03\04") ) "#; -const HASH_RETURN_FROM_START_FN: [u8; 32] = hex!("abb4194bdea47b2904fe90b4fd674bd40d96f423956627df8c39d2b1a791ab9d"); +const HASH_RETURN_FROM_START_FN: [u8; 32] = hex!("66c45bd7c473a1746e1d241176166ef53b1f207f56c5e87d1b6650140704181b"); #[test] fn instantiate_and_call_and_deposit_event() { diff --git a/srml/contract/src/wasm/mod.rs b/srml/contract/src/wasm/mod.rs index 27b8577221c6d..2776f4041b572 100644 --- a/srml/contract/src/wasm/mod.rs +++ b/srml/contract/src/wasm/mod.rs @@ -204,7 +204,8 @@ mod tests { creates: Vec, transfers: Vec, dispatches: Vec, - events: Vec>, + // (topics, data) + events: Vec<(Vec, Vec)>, next_account_id: u64, random_seed: H256, } @@ -279,8 +280,8 @@ mod tests { &self.random_seed } - fn deposit_event(&mut self, data: Vec) { - self.events.push(data) + fn deposit_event(&mut self, topics: Vec, data: Vec) { + self.events.push((topics, data)) } fn set_rent_allowance(&mut self, rent_allowance: u64) { @@ -1182,26 +1183,29 @@ mod tests { const CODE_DEPOSIT_EVENT: &str = r#" (module - (import "env" "ext_deposit_event" (func $ext_deposit_event (param i32 i32))) + (import "env" "ext_deposit_event" (func $ext_deposit_event (param i32 i32 i32 i32))) (import "env" "memory" (memory 1 1)) (func (export "call") (call $ext_deposit_event - (i32.const 8) ;; Pointer to the start of encoded call buffer + (i32.const 32) ;; Pointer to the start of topics buffer + (i32.const 33) ;; The length of the topics buffer. + (i32.const 8) ;; Pointer to the start of the data buffer (i32.const 13) ;; Length of the buffer ) ) (func (export "deploy")) (data (i32.const 8) "\00\01\2A\00\00\00\00\00\00\00\E5\14\00") + + ;; Encoded Vec>, the buffer has length of 33 bytes. + (data (i32.const 32) "\04\33\33\33\33\33\33\33\33\33\33\33\33\33\33\33\33\33\33\33\33\33\33\33" + "\33\33\33\33\33\33\33\33\33") ) "#; #[test] fn deposit_event() { - // This test can fail due to the encoding changes. In case it becomes too annoying - // let's rewrite so as we use this module controlled call or we serialize it in runtime. - let mut mock_ext = MockExt::default(); let mut gas_meter = GasMeter::with_limit(50_000, 1); execute( @@ -1212,11 +1216,105 @@ mod tests { &mut gas_meter ) .unwrap(); + + assert_eq!(mock_ext.events, vec![ + (vec![H256::repeat_byte(0x33)], + vec![0x00, 0x01, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe5, 0x14, 0x00]) + ]); + assert_eq!(gas_meter.gas_left(), 50_000 - - 4 // Explicit - - 13 - 1 // Deposit event - - 13 // read memory + - 6 // Explicit + - 13 - 1 - 1 // Deposit event + - (13 + 33) // read memory + ); + } + + const CODE_DEPOSIT_EVENT_MAX_TOPICS: &str = r#" +(module + (import "env" "ext_deposit_event" (func $ext_deposit_event (param i32 i32 i32 i32))) + (import "env" "memory" (memory 1 1)) + + (func (export "call") + (call $ext_deposit_event + (i32.const 32) ;; Pointer to the start of topics buffer + (i32.const 161) ;; The length of the topics buffer. + (i32.const 8) ;; Pointer to the start of the data buffer + (i32.const 13) ;; Length of the buffer + ) + ) + (func (export "deploy")) + + (data (i32.const 8) "\00\01\2A\00\00\00\00\00\00\00\E5\14\00") + + ;; Encoded Vec>, the buffer has length of 161 bytes. + (data (i32.const 32) "\14" +"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01" +"\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02" +"\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03\03" +"\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04" +"\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05\05") +) +"#; + + #[test] + fn deposit_event_max_topics() { + // Checks that the runtime traps if there are more than `max_topic_events` topics. + let mut mock_ext = MockExt::default(); + let mut gas_meter = GasMeter::with_limit(50_000, 1); + + assert_eq!( + execute( + CODE_DEPOSIT_EVENT_MAX_TOPICS, + &[], + &mut Vec::new(), + &mut mock_ext, + &mut gas_meter + ), + Err("during execution"), + ); + } + + const CODE_DEPOSIT_EVENT_DUPLICATES: &str = r#" +(module + (import "env" "ext_deposit_event" (func $ext_deposit_event (param i32 i32 i32 i32))) + (import "env" "memory" (memory 1 1)) + + (func (export "call") + (call $ext_deposit_event + (i32.const 32) ;; Pointer to the start of topics buffer + (i32.const 129) ;; The length of the topics buffer. + (i32.const 8) ;; Pointer to the start of the data buffer + (i32.const 13) ;; Length of the buffer + ) + ) + (func (export "deploy")) + + (data (i32.const 8) "\00\01\2A\00\00\00\00\00\00\00\E5\14\00") + + ;; Encoded Vec>, the buffer has length of 129 bytes. + (data (i32.const 32) "\10" +"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01" +"\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02\02" +"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01" +"\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04\04") +) +"#; + + #[test] + fn deposit_event_duplicates() { + // Checks that the runtime traps if there are duplicates. + let mut mock_ext = MockExt::default(); + let mut gas_meter = GasMeter::with_limit(50_000, 1); + + assert_eq!( + execute( + CODE_DEPOSIT_EVENT_DUPLICATES, + &[], + &mut Vec::new(), + &mut mock_ext, + &mut gas_meter + ), + Err("during execution"), ); - assert_eq!(mock_ext.events, vec![vec![0, 1, 42, 0, 0, 0, 0, 0, 0, 0, 229, 20, 0]]); } } diff --git a/srml/contract/src/wasm/runtime.rs b/srml/contract/src/wasm/runtime.rs index 3f7b10045c296..6327d8f2cecbe 100644 --- a/srml/contract/src/wasm/runtime.rs +++ b/srml/contract/src/wasm/runtime.rs @@ -17,7 +17,10 @@ //! Environment definition of the wasm smart-contract runtime. use crate::{Schedule, Trait, CodeHash, ComputeDispatchFee, BalanceOf}; -use crate::exec::{Ext, VmExecResult, OutputBuf, EmptyOutputBuf, CallReceipt, InstantiateReceipt, StorageKey}; +use crate::exec::{ + Ext, VmExecResult, OutputBuf, EmptyOutputBuf, CallReceipt, InstantiateReceipt, StorageKey, + TopicOf, +}; use crate::gas::{GasMeter, Token, GasMeterResult, approx_gas_for_balance}; use sandbox; use system; @@ -108,9 +111,9 @@ pub enum RuntimeToken { ReturnData(u32), /// Dispatch fee calculated by `T::ComputeDispatchFee`. ComputedDispatchFee(Gas), - /// The given number of bytes is read from the sandbox memory and - /// deposit in as an event. - DepositEvent(u32), + /// (topic_count, data_bytes): A buffer of the given size is posted as an event indexed with the + /// given number of topics. + DepositEvent(u32, u32), } impl Token for RuntimeToken { @@ -129,10 +132,25 @@ impl Token for RuntimeToken { ReturnData(byte_count) => metadata .return_data_per_byte_cost .checked_mul(&>::sa(byte_count)), - DepositEvent(byte_count) => metadata - .event_data_per_byte_cost - .checked_mul(&>::sa(byte_count)) - .and_then(|e| e.checked_add(&metadata.event_data_base_cost)), + DepositEvent(topic_count, data_byte_count) => { + let data_cost = metadata + .event_data_per_byte_cost + .checked_mul(&>::sa(data_byte_count)); + + let topics_cost = metadata + .event_per_topic_cost + .checked_mul(&>::sa(topic_count)); + + data_cost + .and_then(|data_cost| { + topics_cost.and_then(|topics_cost| { + data_cost.checked_add(&topics_cost) + }) + }) + .and_then(|data_and_topics_cost| + data_and_topics_cost.checked_add(&metadata.event_base_cost) + ) + }, ComputedDispatchFee(gas) => Some(gas), }; @@ -610,21 +628,47 @@ define_env!(Env, , Ok(()) }, - // Deposit a contract event with the data buffer. - ext_deposit_event(ctx, data_ptr: u32, data_len: u32) => { + // Deposit a contract event with the data buffer and optional list of topics. There is a limit + // on the maximum number of topics specified by `max_event_topics`. + // + // - topics_ptr - a pointer to the buffer of topics encoded as `Vec`. The value of this + // is ignored if `topics_len` is set to 0. The topics list can't contain duplicates. + // - topics_len - the length of the topics buffer. Pass 0 if you want to pass an empty vector. + // - data_ptr - a pointer to a raw data buffer which will saved along the event. + // - data_len - the length of the data buffer. + ext_deposit_event(ctx, topics_ptr: u32, topics_len: u32, data_ptr: u32, data_len: u32) => { + let mut topics = match topics_len { + 0 => Vec::new(), + _ => { + let topics_buf = read_sandbox_memory(ctx, topics_ptr, topics_len)?; + Vec::::T>>::decode(&mut &topics_buf[..]) + .ok_or_else(|| sandbox::HostError)? + } + }; + + // If there are more than `max_event_topics`, then trap. + if topics.len() > ctx.schedule.max_event_topics as usize { + return Err(sandbox::HostError); + } + + // Check for duplicate topics. If there are any, then trap. + if has_duplicates(&mut topics) { + return Err(sandbox::HostError); + } + + let event_data = read_sandbox_memory(ctx, data_ptr, data_len)?; + match ctx .gas_meter .charge( ctx.schedule, - RuntimeToken::DepositEvent(data_len) + RuntimeToken::DepositEvent(topics.len() as u32, data_len) ) { GasMeterResult::Proceed => (), GasMeterResult::OutOfGas => return Err(sandbox::HostError), } - - let event_data = read_sandbox_memory(ctx, data_ptr, data_len)?; - ctx.ext.deposit_event(event_data); + ctx.ext.deposit_event(topics, event_data); Ok(()) }, @@ -665,3 +709,21 @@ define_env!(Env, , Ok(()) }, ); + +/// Finds duplicates in a given vector. +/// +/// This function has complexity of O(n log n) and no additional memory is required, although +/// the order of items is not preserved. +fn has_duplicates>(items: &mut Vec) -> bool { + // Sort the vector + items.sort_unstable_by(|a, b| { + Ord::cmp(a.as_ref(), b.as_ref()) + }); + // And then find any two consecutive equal elements. + items.windows(2).any(|w| { + match w { + &[ref a, ref b] => a == b, + _ => false, + } + }) +}