Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VoterSet and Bitfield Refactoring #93

Merged
merged 12 commits into from
Jan 17, 2020
Merged

Conversation

romanb
Copy link
Contributor

@romanb romanb commented Nov 21, 2019

This is a proposal for a refactoring (or rather rewrite) of the voter_set and bitfield modules alongside smaller related changes in the round and vote_graph, targeting the v0.11.0 branch (i.e. based on #81).

Below is a rough outline of the main changes.

Bitfield

  • Simplify definition, as Bitfield::Blank and LiveBitfield(vec![])
    are equivalent.
  • Dynamically-sized with lazy allocation makes for a more ergonomic
    API and keeps bitfields smaller with larger voter sets. The reasoning
    here is the following: If there are few voters, most votes
    are likely on the same or very few nodes / blocks but in that case bitfields
    are almost never resized anyway. As there are more and more voters,
    votes are likely spread across more and more nodes / blocks and thus not
    all nodes need a full-sized bitfield (whose size grows with the voter set).
  • Avoid allocations when merging bitfields. Previously merging would
    always allocate a new bitfield, now the other bitfield is merged
    into self and only allocates if self needs to be resized.
  • Avoid allocations when recording votes in the vote graph and computing weights.
    Previously both recording of new votes and computing weights
    involved creating and merging bitfields. Now incorporating a
    vote into an existing vote-node is a set_bit operation while
    computing the vote weight on a node is based on a bitfield-merging
    iterator that does not allocate.
  • Better tests.

VoterSet

  • Make it impossible to construct inconsistent sets as well as
    empty sets. Both were previously possible.
  • Better tests.

Related changes

  • Introduce a weights module for encapsulating vote-weight
    arithmetic behind newtypes.
  • Introduce a round::context module for encapsulating the
    context of a round in which vote weights are computed,
    consisting of the voter set and recorded equivocations.
    This combines some functionality from the previous bitfield
    module and some from the parent round module.

@rphmeier
Copy link
Contributor

Looks good at a skim - going to give it a more in-depth review soon. Thanks!

N: Copy + Debug + BlockNumberOps,
{
/// Create a new `VoteGraph` with base node as given.
pub fn new(base_hash: H, base_number: N) -> Self {
pub fn new(base_hash: H, base_number: N, base_node: V) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

do we have any use-case where we are initializing this to non-default? The Default ~ Zero property is important for a lot of the GHOST-stuff and this may discourage that.

Copy link
Contributor Author

@romanb romanb Nov 25, 2019

Choose a reason for hiding this comment

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

This was primarily motivated by the fact that making use of a type parameter constraint (i.e. trait bound) in a method whereby the type parameter appears nowhere in the method's arguments necessarily leaves the parameter uninstantiated in expressions of the form

let graph = VoteGraph::new(...);

and thus makes type inference harder. Previously V could then be instantiated by the type system through observing subsequent occurrences of graph.insert(...). Now the parameter of insert is some W such that V: for<'a> AddAssign<&'a W>, so V is still undetermined. It can of course be solved by giving an explicit type ascription to expressions such as the above (let graph: ... = ), it just seemed to me that passing the base node in the constructor is preferable as it is more explicit and avoids this type ambiguity.

Generally the VoteGraph has no clue what V::default() produces, i.e. there was and is no Default ~ Zero property. However, it is certainly true that this allows constructing a VoteGraph with a base node that is different from what the default instance returns and the latter is used inside the VoteGraph to construct intermediate new nodes on demand. If that is undesirable, I can certainly go with the extra type annotations.

src/voter_set.rs Outdated Show resolved Hide resolved
src/voter_set.rs Outdated Show resolved Hide resolved
src/voter_set.rs Outdated Show resolved Hide resolved
@rphmeier
Copy link
Contributor

rphmeier commented Nov 25, 2019

I've been fuzz-testing the old implementation for the last 5 days on the rh-fuzzing branch without exposing any issues. I'm worried that the rewrite opens new opportunities for logic errors, particularly in equivocation counting, GHOST, and completability. I would rather not merge this until it has at least the same degree of testing. I was glad to see the property-based quickcheck testing on individual components.

@codecov-io
Copy link

codecov-io commented Nov 25, 2019

Codecov Report

❗ No coverage uploaded for pull request base (master@79bb39b). Click here to learn what that means.
The diff coverage is 93.76%.

Impacted file tree graph

@@            Coverage Diff            @@
##             master      #93   +/-   ##
=========================================
  Coverage          ?   90.12%           
=========================================
  Files             ?       12           
  Lines             ?     3787           
  Branches          ?        0           
=========================================
  Hits              ?     3413           
  Misses            ?      374           
  Partials          ?        0
Impacted Files Coverage Δ
src/fuzz_helpers.rs 95.83% <100%> (ø)
src/vote_graph.rs 98.03% <100%> (ø)
src/lib.rs 81.22% <100%> (ø)
src/voter/mod.rs 87.28% <100%> (ø)
src/voter/voting_round.rs 81.55% <100%> (ø)
src/weights.rs 70% <70%> (ø)
src/bitfield.rs 89.77% <91.19%> (ø)
src/voter_set.rs 97.91% <97.63%> (ø)
src/round/context.rs 99.05% <99.05%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 79bb39b...7ded42e. Read the comment docs.

@romanb
Copy link
Contributor Author

romanb commented Nov 25, 2019

I would rather not merge this until it has at least the same degree of testing. [..]

I'm fine with waiting until the fuzz-testing is in master and the v0.11 and this branch subsequently rebased on top.

@romanb
Copy link
Contributor Author

romanb commented Jan 16, 2020

If there is still interest in this PR, I can at some point rebase it over the current master. Let me know.

@rphmeier
Copy link
Contributor

Yep, there's still interest! Please do rebase it.

@rphmeier
Copy link
Contributor

Could you re-target the PR again against master as well?

@romanb
Copy link
Contributor Author

romanb commented Jan 16, 2020

Could you re-target the PR again against master as well?

Of course.

Roman S. Borschel and others added 7 commits January 17, 2020 10:11
Bitfield:

  * Simplify definition, as Bitfield::Blank and LiveBitfield(vec![])
    are equivalent.
  * Dynamically-sized with lazy allocation makes for a more ergonomic
    API and keeps bitfields smaller. If there are few voters, most votes
	are likely on the same nodes / blocks but in that case bitfields
	are almost never resized anyway. As there are more and more voters,
	votes are spread across more and more nodes / blocks and thus not
	all nodes need a fully-sized bitfield.
  * Avoid allocations when merging bitfields. Previously merging would
    always allocate a new bitfield, now the other bitfield is merged
	into `self` and only allocates if `self` needs to be resized.
  * Avoid allocations when casting votes and computing weights.
    Previously both casting of new votes and computing weights
	involved creating and merging bitfields. Now incorporating a
	vote into the vote graph is a `set_bit` operation while
	computing the vote weight on a node is based a bitfield-merging
	iterator that does not allocate.
  * Better tests.

VoterSet:

  * Make it impossible to construct inconsistent sets as well as
    empty sets. Both were previously possible.
  * Better tests.

Related changes:

  * Introduce a `weights` module for encapsulating vote-weight
    arithmetic behind newtypes.
  * Introduce a `round::context` module for encapsulating the
    context of a round in which vote weights are computed,
	consisting of the voter set and recorded equivocations.
@romanb romanb changed the base branch from v0.11.0 to master January 17, 2020 09:25
@rphmeier rphmeier merged commit 152c495 into paritytech:master Jan 17, 2020
rphmeier added a commit that referenced this pull request Jan 17, 2020
#93 introduced some backwards-incompatible changes to the API and we've already release v0.11.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants