Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.

Governance MVP#42

Closed
gavofyork wants to merge 94 commits intomasterfrom
governance
Closed

Governance MVP#42
gavofyork wants to merge 94 commits intomasterfrom
governance

Conversation

@gavofyork
Copy link
Copy Markdown
Member

@gavofyork gavofyork commented Jan 21, 2018

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:

  • Tests
  • Docs
  • Additional internal functions.

@gavofyork gavofyork mentioned this pull request Jan 23, 2018
1 task
}

/// Place the value in storage under `key`.
fn store(&self, key: &[u8]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@rphmeier rphmeier Jan 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more restrictive to take &F than F here because Fn is implemented for &F already.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see later comment.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if size is zero then this is UB

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Contributor

@rphmeier rphmeier Jan 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to just zero the vector:

  1. it's 4 bytes
  2. fill_slice might cause undefined behavior reading from uninitialized memory
  3. unsafe code is bad
  4. is this a bottleneck?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm in favor of extracting out all storage keys to useful constants.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the enum variants aren't strictly defined it seems easy for these to change in a non backwards-compatible way.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in latest.

}

impl Slicable for Proposal {
fn set_as_slice<F: Fn(&mut[u8], usize) -> bool>(fill_slice: &F) -> Option<Self> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here -- prefer F over &F

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

panic proofs are missing in this match

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having a default trait function be unimplemented! is strange. why not just leave it unimplemented?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 We had that in RPC some time ago and it caused some of the RPC methods to be unintentionally unimplemented because of that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storage API refactor can happen in later PR.

Copy link
Copy Markdown
Contributor

@rphmeier rphmeier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few issues in unsafe code -- also worth addressing the same issue in Slicable::as_slice_then in this PR.

Style grumbles:

  • Usage of &F where F: Fn
  • The whole Storable interface is very awkward. I would suggest constructing our strongly-typed storage interface somewhat differently using the combination of three parts:
  1. a utility for putting/getting raw bytes (storage::put_raw, storage::get_raw)
  2. traits for serialization and deserialization
  3. 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..];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't it panic if value.len() <= value_offset?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:")))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ses? wil? sta = staking? Another +1 for contants.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use modules instead of formatting comments?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in the latest PR

// PRIVILEDGED API

/// Set the number of sessions in an era.
pub fn set_sessions_per_era(new: BlockNumber) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 We had that in RPC some time ago and it caused some of the RPC methods to be unintentionally unimplemented because of that.

@gavofyork
Copy link
Copy Markdown
Member Author

closed in favour of #47 where all major grumbles have been resolved.

@gavofyork gavofyork closed this Jan 28, 2018
@gavofyork gavofyork deleted the governance branch January 30, 2018 09:58
iStrike7 referenced this pull request in gasp-xyz/substrate Aug 3, 2021
…reate_only_as_root

Feature/tokens mint and create only as root
liuchengxu pushed a commit to chainx-org/substrate that referenced this pull request Aug 23, 2021
* Add clean consensus

* Fix build wasm error

* Evaluate block every 1s

* Fix bft consensus about time poll and 2s evaluate block
liuchengxu referenced this pull request in autonomys/substrate Jun 3, 2022
imstar15 pushed a commit to imstar15/substrate that referenced this pull request Jan 3, 2023
…runtime-upgrade

Release/second runtime upgrade
helin6 pushed a commit to boolnetwork/substrate that referenced this pull request Jul 25, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

A0-please_review Pull request needs code review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants