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

Rework PeerList, improve its API, and fix a bug in swap_primary #397

Merged
merged 7 commits into from
Jul 2, 2020

Conversation

romac
Copy link
Member

@romac romac commented Jun 30, 2020

  • Removes the newly assigned primary from the witnesses.

  • Ensures the primary is not part of any of the peer lists (witnesses, full_nodes, faulty_nodes).

  • Fixes a bug in swap_primary (now replace_faulty_primary) where we would iterate the first element of witnesses over and over.

  • Rework and generalize the PeerList for easier testing and safer API

    This commit changes the PeerList type to be parametrized by the type of values associated with a PeerId. This enables easier testing of the peer list without having to construct light clientInstances.

    This commit additionally introduces a safer API by taking into account the invariant associated with a PeerList, which lets us return unconditionally the value associated with the primary peer.

    In turn, this simplifies the verification loop in the supervisor, which is now expressed more simply as a recursive function whose correctness is more obvious than the imperative one, at least to me.


  • Referenced an issue explaining the need for the change
  • Updated all relevant documentation in docs
  • Updated all code comments where relevant
  • Wrote tests
  • Updated CHANGES.md

@romac romac added the light-client Issues/features which involve the light client label Jun 30, 2020
@romac romac requested review from brapse and liamsi June 30, 2020 11:07
@romac
Copy link
Member Author

romac commented Jun 30, 2020

Thanks @OStevan for prompting this little refactor and reviewing it.

@OStevan
Copy link
Contributor

OStevan commented Jun 30, 2020

@romac thanks for following up on this.

xla
xla previously approved these changes Jun 30, 2020
Copy link
Contributor

@xla xla left a comment

Choose a reason for hiding this comment

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

As far as I can tell this looks correct. Module level tests would help asserting the right behaviour.

🕶 😼 🖍 🅿️

@romac romac marked this pull request as draft June 30, 2020 11:42
@romac
Copy link
Member Author

romac commented Jun 30, 2020

Module level tests would help asserting the right behaviour.

Yep, working on those :) Thanks for the review!

liamsi
liamsi previously approved these changes Jun 30, 2020
Copy link
Member

@liamsi liamsi left a comment

Choose a reason for hiding this comment

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

👏 LGTM

Will revisit after tests were added.

@romac romac dismissed stale reviews from liamsi and xla via e371271 June 30, 2020 12:39
This commit changes the `PeerList` type to be parametrized by
the type of values associated with a `PeerId`. This enables
easier testing of the peer list without having to construct
light client`Instance`s.

This commit additionally introduces a safer API by taking into
account the invariant associated with a `PeerList`, which lets
us return unconditionally the value associated with the primary peer.

In turn, this simplifies the verification loop in the supervisor,
which is now expressed more simply as a recursive function whose
correctness is more obvious than the imperative one, at least to me.
@romac romac marked this pull request as ready for review July 1, 2020 13:35
@romac romac requested review from xla and liamsi July 1, 2020 13:37
@romac romac changed the title Simplify swap of primary/witness Rework PeerList, improve its API, and fix a bug in swap_primary Jul 1, 2020
@romac
Copy link
Member Author

romac commented Jul 1, 2020

Adding tests prompted me to refactor the PeerList definition and API in cbf9753.

@romac romac mentioned this pull request Jul 1, 2020
5 tasks
Copy link
Contributor

@xla xla left a comment

Choose a reason for hiding this comment

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

This is a great refactoring and I agree it makes the core of the verification indeed easier to follow and comprehend. My main concern is the very constrained T on the PeerList. Could it not give stronger guarantees if Instance would be a trait and PeerList expects that as its type parameter?

witnesses: HashSet<PeerId>,
full_nodes: HashSet<PeerId>,
faulty_nodes: HashSet<PeerId>,
witnesses: BTreeSet<PeerId>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I missed some context in a commit, but what's the motivation to move to a BTreeSet? Asking purely out of curioisity.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is to provide a stable ordering on the peer ids, to make the whole process (more) deterministic.

Good question by the way, I failed to mention that in the commit.

}

impl PeerList {
impl<T> PeerList<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any constraints we want to have on T that would help make the construction of the PeerList even more safe?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see any constraints which would make it safer, but perhaps I am not fully understanding/seeing what you mean by safer in this context?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think what I'm getting at is partially what I said in #397 (review) - given this is the PeerList of the light client crate, it might not be all that beneficial to generalise entirely over T and rather constraint it to a trait (capabilities) that indicate that whatever is held as a peer value can be used as an Instance, or whatever other abstraction has the right surface for other components to rely/integrate on. It is in the same direction as making the Handle a trait, I suspect these changes to make it easier to build up the object graph in tests and safer to construct. It's mostly food for thought and can be iterated on over time.

pub struct PeerList {
instances: HashMap<PeerId, Instance>,
pub struct PeerList<T> {
values: HashMap<PeerId, T>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the quite generic rename here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Since the peer list can now hold to any datatype rather than just light client instances, I thought values was more appropriate than instances, but I am open to suggestions.

faulty_nodes: HashSet<PeerId>,
witnesses: BTreeSet<PeerId>,
full_nodes: BTreeSet<PeerId>,
faulty_nodes: BTreeSet<PeerId>,
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems wasteful to see the exact same fields as in PeerList, why does the builder not hold on to a fresh PeerList and returning it on build instead of constructing it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I am more or less following the internal representation used by the derive_builder crate, and didn't give it much more thought. But I agree that it's a bit wasteful and that what you suggest would work as well. I don't feel strongly either way.

}
}

fn a() -> PeerId {
Copy link
Contributor

Choose a reason for hiding this comment

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

You could name these according to their use case further down.

Copy link
Member Author

@romac romac Jul 1, 2020

Choose a reason for hiding this comment

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

Previous comment was not posted in the right place

@romac
Copy link
Member Author

romac commented Jul 1, 2020

@xla Thanks for the review! :) See my answers inline.

This is a great refactoring and I agree it makes the core of the verification indeed easier to follow and comprehend. My main concern is the very constrained T on the PeerList. Could it not give stronger guarantees if Instance would be a trait and PeerList expects that as its type parameter?

I don't really see the value of having Instance defined as a trait and constraining T over that, since the behavior of PeerList does not depend in any way on the choice of T. My general approach to building data structures is to keep them as generic as possible, and only constrain on types if absolutely required by the definition of the struct, or on a per-method basis. Admittedly, this is a mindset I inherited from working in Haskell, where it really pays off to have very generic data structures with fully unconstrained type parameters, but it seems to be mostly shared by the Rust community as well: rust-lang/rust-clippy#1689.

Of course, if there is actual (type) safety to be gained by having a bound on T (that I am at the moment not seeing, but feel free to expand on that), I'd be happy to revisit that decision in that case.

Copy link
Contributor

@xla xla left a comment

Choose a reason for hiding this comment

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

Thought you might approach it like that and it's a common way esp. for people coming from Haskell. It's a sound way of building datastructures and I'm merely challenging from the perspective of domain coherence. As we not building general container datastructures but rather specific ones, i.e. the peer list of what the light client understands as a peer.

In any case my comments are just food for thought. All my review comments are sufficiently addressed.

➿ 🐬 🎈 🗝

@romac
Copy link
Member Author

romac commented Jul 1, 2020

As we not building general container datastructures but rather specific ones, i.e. the peer list of what the light client understands as a peer.

I agree, and that's why the peer list was initially specialized to Instance, but in this case having it being generic over the value associated with peers helped a lot to keep the tests concise as well, without eg. having to introduce an Instance trait.

I still wonder what you had in mind w.r.t. to safety, because I don't see any constraints on T that would improve on the current implementation, ie. PeerList<Instance> is functionally equivalent to PeerList<T> for any T. Would you mind expanding on what you had in mind, because I might very well be missing something?

let _ = peer_list.replace_faulty_witness(d());
unreachable!();
}
}
Copy link
Member

@liamsi liamsi Jul 1, 2020

Choose a reason for hiding this comment

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

Thanks for adding tests 👍

@liamsi
Copy link
Member

liamsi commented Jul 1, 2020

I still wonder what you had in mind w.r.t. to safety,

My understanding is that this isn't necessarily about safety per se, only that a completely generic T might be too general where an actual constraining trait could make sense. On the other hand, no particular behaviour (or capabilities) seem to be required by T, so it can be that generic.

Copy link
Member

@liamsi liamsi left a comment

Choose a reason for hiding this comment

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

👏 LGTM 👍

@brapse brapse merged commit 9e6c446 into master Jul 2, 2020
@brapse brapse deleted the romain/simplify-witness-swap branch July 2, 2020 07:50
@xla
Copy link
Contributor

xla commented Jul 2, 2020

My understanding is that this isn't necessarily about safety per se, only that a completely generic T might be too general where an actual constraining trait could make sense. On the other hand, no particular behaviour (or capabilities) seem to be required by T, so it can be that generic.

Spot on, sorry @romac - what I was going at is not so much safety but communication of intent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
light-client Issues/features which involve the light client
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants