Pass topics along with events (#2563)

* Introduce an IndexedEvent

* Plumb topics through the Ext interface.

* Add topics to ext_deposit_event

* Charging for events.

* Check the number of topics.

* Check for duplicate topics.

* Bump API version.

* Move derive(*Eq) under test.

* Use sorting for finding duplicates.
This commit is contained in:
Sergei Pepyakin
2019-05-14 12:09:51 +02:00
committed by Gavin Wood
parent 6ccdbea01c
commit d57f6f9305
6 changed files with 272 additions and 52 deletions
+110 -12
View File
@@ -204,7 +204,8 @@ mod tests {
creates: Vec<CreateEntry>,
transfers: Vec<TransferEntry>,
dispatches: Vec<DispatchEntry>,
events: Vec<Vec<u8>>,
// (topics, data)
events: Vec<(Vec<H256>, Vec<u8>)>,
next_account_id: u64,
random_seed: H256,
}
@@ -279,8 +280,8 @@ mod tests {
&self.random_seed
}
fn deposit_event(&mut self, data: Vec<u8>) {
self.events.push(data)
fn deposit_event(&mut self, topics: Vec<H256>, data: Vec<u8>) {
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<TopicOf<T>>, 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<TopicOf<T>>, 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<TopicOf<T>>, 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]]);
}
}
+76 -14
View File
@@ -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<Gas> {
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<T: Trait> Token<T> for RuntimeToken<T::Gas> {
@@ -129,10 +132,25 @@ impl<T: Trait> Token<T> for RuntimeToken<T::Gas> {
ReturnData(byte_count) => metadata
.return_data_per_byte_cost
.checked_mul(&<T::Gas as As<u32>>::sa(byte_count)),
DepositEvent(byte_count) => metadata
.event_data_per_byte_cost
.checked_mul(&<T::Gas as As<u32>>::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(&<T::Gas as As<u32>>::sa(data_byte_count));
let topics_cost = metadata
.event_per_topic_cost
.checked_mul(&<T::Gas as As<u32>>::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, <E: Ext>,
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<T::Hash>`. 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::<TopicOf<<E as Ext>::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, <E: Ext>,
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<T: PartialEq + AsRef<[u8]>>(items: &mut Vec<T>) -> 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,
}
})
}