Conversation
…g and direct usage.
| } | ||
|
|
||
| /// Place the value in storage under `key`. | ||
| fn store(&self, key: &[u8]); |
There was a problem hiding this comment.
having this as an implicit function on the objects is a little weird. You end up with code like value.store("some_data") which reads a more like the value is having data added to it.
invocations like storage::store("some_key", &value) and storage::load("some_key") better indicate that implicit globals are being accessed.
There was a problem hiding this comment.
currently every object that implements this trait has to manually perform storage, when really the only common functionality needed is serialization/deserialization. this will reduce duplication and make things cleaner in general
There was a problem hiding this comment.
i'm happy with:
let count: usize = database::lookup_default(key);
database::put(key, count + 1);
but i definitely don't think this is good:
let value_bytes = database::lookup_default(key);
let count = <usize>::deserialise(value_bytes);
let value_bytes = database::serialise(count + 1);
database::store(key, &value_bytes);
| self.as_slice_then(|s| s.to_vec()) | ||
| } | ||
| fn set_as_slice<F: FnOnce(&mut [u8]) -> bool>(set_slice: F) -> Option<Self>; | ||
| fn set_as_slice<F: Fn(&mut [u8], usize) -> bool>(set_slice: &F) -> Option<Self>; |
There was a problem hiding this comment.
It's more restrictive to take &F than F here because Fn is implemented for &F already.
| let mut result: T = unsafe { mem::zeroed() }; | ||
| let result_slice = unsafe { | ||
| let ptr = &mut result as *mut _ as *mut u8; | ||
| slice::from_raw_parts_mut(ptr, size) |
There was a problem hiding this comment.
if size is zero then this is UB
There was a problem hiding this comment.
the size is never zero - EndianSensitive is only implemented for "real" types. if there's a static check to ensure this is the case, great. Or some other way of preventing external code from using EndianSensitive. For now i'll make a runtime-assert.
| u32::set_as_slice(fill_slice).and_then(|len| { | ||
| let mut v = Vec::with_capacity(len as usize); | ||
| unsafe { v.set_len(len as usize); } | ||
| if fill_slice(&mut v, 4) { |
There was a problem hiding this comment.
better to just zero the vector:
- it's 4 bytes
fill_slicemight cause undefined behavior reading from uninitialized memory- unsafe code is bad
- is this a bottleneck?
There was a problem hiding this comment.
it's not 4 bytes. the 4 is the four byte offset; the vector could be huge e.g. a multi-MB block.
| /// Proposal is by the `transactor` and will automatically count as an approval. Transactor must | ||
| /// be a current validator. It is illegal to propose when there is already a proposal in effect. | ||
| pub fn propose(validator: &AccountID, proposal: &Proposal) { | ||
| if Proposal::lookup(b"gov:pro").is_some() { |
There was a problem hiding this comment.
i'm in favor of extracting out all storage keys to useful constants.
There was a problem hiding this comment.
yeah - i'm planning to explore a better storage keying mechanism in later PoCs.
| /// Propose a sensitive action to be taken. Any action that is enactable by `Proposal` is valid. | ||
| /// Proposal is by the `transactor` and will automatically count as an approval. Transactor must | ||
| /// be a current validator. It is illegal to propose when there is already a proposal in effect. | ||
| pub fn propose(validator: &AccountID, proposal: &Proposal) { |
There was a problem hiding this comment.
is proposal here used to mean something like a referendum? in the consensus layer we also have the term "proposal" used to mean something else.
There was a problem hiding this comment.
it's meant to be "proposal for some arbitrary action, embodied as a privileged call".
| /// than once for any validator in an era. | ||
| pub fn approve(validator: &AccountID, era_index: BlockNumber) { | ||
| if era_index != staking::current_era() { | ||
| panic!("approval vote applied on non-current era.") |
There was a problem hiding this comment.
is panic being used as the signal for block processing failure? seems dangerous if we want to run it in a native context.
It would be better to propagate errors up using something like error_chain and use the bail! macro.
There was a problem hiding this comment.
disagree vehemently. that would add a tonne of noise for no reason. within the context of the wasm runtime, a panic is always tantamount to block failure. native has a panic handler. unless there are cirsumstances where an intentional panic in the runtime would cause the main node to panic, then i see no reason to add it.
| /// Derive `Some` value from a `u8`, or `None` if it's invalid. | ||
| pub fn from_u8(value: u8) -> Option<InternalFunction> { | ||
| match value { | ||
| x if x == InternalFunction::SystemSetCode as u8 => Some(InternalFunction::SystemSetCode), |
There was a problem hiding this comment.
since the enum variants aren't strictly defined it seems easy for these to change in a non backwards-compatible way.
| } | ||
|
|
||
| impl Slicable for Proposal { | ||
| fn set_as_slice<F: Fn(&mut[u8], usize) -> bool>(fill_slice: &F) -> Option<Self> { |
There was a problem hiding this comment.
same here -- prefer F over &F
There was a problem hiding this comment.
couldn't make it work as i need to recurse the argument into itself multiple times in at least one implementation.
| let mut params = StreamReader::new(&self.input_data); | ||
| match self.function { | ||
| InternalFunction::SystemSetCode => { | ||
| let code: Vec<u8> = params.read().unwrap(); |
There was a problem hiding this comment.
panic proofs are missing in this match
There was a problem hiding this comment.
no need as this is runtime code. if anything goes wrong, the block is invalid and it'll be by the panic handler.
| /// Lookup `Some` value in storage and deserialise; `None` if it's not there. | ||
| fn lookup(_key: &[u8]) -> Option<Self> where Self: Sized { unimplemented!() } | ||
| fn lookup(_key: &[u8]) -> Option<Self> where Self: Sized { | ||
| unimplemented!() |
There was a problem hiding this comment.
having a default trait function be unimplemented! is strange. why not just leave it unimplemented?
There was a problem hiding this comment.
👍 We had that in RPC some time ago and it caused some of the RPC methods to be unintentionally unimplemented because of that.
There was a problem hiding this comment.
because this is not always implementable (i.e. when Self is not Sized). in that case, i can't leave it unimplemented but also can't implement it. having a default implementation is the only way around this i found. happy to change if there's a cleaner way.
There was a problem hiding this comment.
(point is moot now with the new storage API)
|
|
||
| /// Retrives and returns the serialised value of a key from storage, removing it immediately. | ||
| fn take(key: &[u8]) -> Option<Self> where Self: Sized { | ||
| if let Some(value) = Self::lookup(key) { |
There was a problem hiding this comment.
Using Self in this way is pretty unidiomatic. A trait should provide the core functionality and then a module can expose free functions to leverage it.
There was a problem hiding this comment.
storage API refactor can happen in later PR.
There was a problem hiding this comment.
A few issues in unsafe code -- also worth addressing the same issue in Slicable::as_slice_then in this PR.
Style grumbles:
- Usage of
&FwhereF: Fn - The whole
Storableinterface is very awkward. I would suggest constructing our strongly-typed storage interface somewhat differently using the combination of three parts:
- a utility for putting/getting raw bytes (
storage::put_raw,storage::get_raw) - traits for serialization and deserialization
- utilities for putting/getting strongly-typed data (
storage::put<T: Serializable>,storage::get<T: Deserializable>)
As it stands, every type we want to put into and get from storage has to have the code which actually reads and writes it duplicated, complicating their implementations. It also improves readability IMO -- compare storage::put(b"key", my_value_of_any_type) vs. my_value.store(b"key"). The second option looks like we are just constructing an in-memory map, not like the value is being written to persistent global storage.
The governance logic looks fine.
| ext_get_storage_into(key_data: *const u8, key_len: u32, value_data: *mut u8, value_len: u32, value_offset: u32) -> u32 => { | ||
| if let Ok(key) = this.memory.get(key_data, key_len as usize) { | ||
| if let Ok(value) = this.ext.storage(&key) { | ||
| let value = &value[value_offset as usize..]; |
There was a problem hiding this comment.
Won't it panic if value.len() <= value_offset?
There was a problem hiding this comment.
yup, and that's fine. panicking is an indication that the block is invalid.
| /// Approve the current era's proposal. Transactor must be a validator. This may not be done more | ||
| /// than once for any validator in an era. | ||
| pub fn approve(validator: &AccountID, era_index: BlockNumber) { | ||
| if era_index != staking::current_era() { |
There was a problem hiding this comment.
Sorry to chime in so late, first time reviewing any contract, but IMHO relying on some globally accessible APIs (especially if they can do alterations like u32::store(&str)) seem super bug-prone for me.
It also makes the code hard to analyze and encourages hacky changes instead of clean code design (or rather it doesn't enforce good design).
There was a problem hiding this comment.
In general, I agree. But it might make sense for these specific externalities if you compare them to e.g. filesystem or network APIs which also rely on some global context. I just think we need sane wrappers. Things like true.store(b"foo") don't indicate that global state is being leaned on enough.
There was a problem hiding this comment.
happy to try some other API, but in general i'd like to keep the storage API lean - perhaps not quite as lean as it is in solidity, but preferably not far off.
| /// validator. `1000` would require the approval of all validators; `667` would require two-thirds | ||
| /// (or there abouts) of validators. | ||
| pub fn set_approval_ppm_required(ppm: u32) { | ||
| ppm.store(b"gov:apr"); |
There was a problem hiding this comment.
That looks super hacky, why there is no context object at least? IMHO it's super nice in rust that you see if by calling a function you're gonna modify some other object that is borrowed to it.
There was a problem hiding this comment.
well, modifying storage is a bare function. i could create a dummy context and then pointlessly hand it around from function to function, but it makes little sense to add all of that noise.
| kill(b"gov:pro"); | ||
| let approvals_required = approvals_required(); | ||
| let approved = session::validators().into_iter() | ||
| .filter_map(|v| bool::take(&v.to_keyed_vec(b"gov:app:"))) |
There was a problem hiding this comment.
+1 for constans, gov:apr vs gov:app: is quite difficult to even distinguish visually. also what's app ro apr? why not full words?
There was a problem hiding this comment.
sure - it's a fairly big job though so i'd prefer to leave it for a later PR.
| use testing::{one, two, TestExternalities}; | ||
| use primitives::AccountID; | ||
| use proposal::InternalFunction; | ||
| use runtime::{staking, session}; |
There was a problem hiding this comment.
Already imported via super::* don't need to repeat the imports in submodules.
| impl StorageVec for IntentionStorageVec { | ||
| type Item = AccountID; | ||
| const PREFIX: &'static[u8] = b"ses:wil:"; | ||
| const PREFIX: &'static[u8] = b"sta:wil:"; |
There was a problem hiding this comment.
ses? wil? sta = staking? Another +1 for contants.
There was a problem hiding this comment.
i'm planning on abstracting all this in the not-too-distant future to readable static values, but would prefer to leave it to another PR
| pub fn bondage(who: &AccountID) -> Bondage { | ||
| Storable::lookup_default(&who.to_keyed_vec(b"sta:bon:")) | ||
| } | ||
| // TRANSACTION API |
There was a problem hiding this comment.
Why not use modules instead of formatting comments?
| // PRIVILEDGED API | ||
|
|
||
| /// Set the number of sessions in an era. | ||
| pub fn set_sessions_per_era(new: BlockNumber) { |
There was a problem hiding this comment.
priviledged::set_sessions_per_era() would imho look nicer and would made it more visible to see that it's a priviledged API (especially at call site) than the comment above.
There was a problem hiding this comment.
done in the latest PR.
| x if x == Function::StakingTransfer as u8 => Some(Function::StakingTransfer), | ||
| x if x == Function::SessionSetKey as u8 => Some(Function::SessionSetKey), | ||
| x if x == Function::TimestampSet as u8 => Some(Function::TimestampSet), | ||
| x if x == Function::GovernancePropose as u8 => Some(Function::GovernancePropose), |
There was a problem hiding this comment.
Why not
let functions = [Function::StakingStake, ...];
if value as usize < functions.len() {
Some(functions[value as usize]);
} else {
None
}
doesn't it need #[repr(u8)] anyway for that to be safe?
There was a problem hiding this comment.
yes (done in latest PR)
| /// Lookup `Some` value in storage and deserialise; `None` if it's not there. | ||
| fn lookup(_key: &[u8]) -> Option<Self> where Self: Sized { unimplemented!() } | ||
| fn lookup(_key: &[u8]) -> Option<Self> where Self: Sized { | ||
| unimplemented!() |
There was a problem hiding this comment.
👍 We had that in RPC some time ago and it caused some of the RPC methods to be unintentionally unimplemented because of that.
|
closed in favour of #47 where all major grumbles have been resolved. |
…reate_only_as_root Feature/tokens mint and create only as root
* Add clean consensus * Fix build wasm error * Evaluate block every 1s * Fix bft consensus about time poll and 2s evaluate block
…runtime-upgrade Release/second runtime upgrade
NOTE: Grumbles addressed in #47 .
At this point "governance" is simply a qualified majority vote of the validators. This buys us the ability to do secure "decentralised" code upgrades. With this functionality, further (better) governance can be introduced in the PoC over time.
Still todo: