-
Notifications
You must be signed in to change notification settings - Fork 224
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
spec: Rust like pseudo code for Sequential supervisor and core verification #509
Conversation
Thanks a lot! Both for the proposal of the code and the awesome overview of the issues! I think we should discuss this here, and then incorporate either the proposed pseudo code (or a go variant of it) into a supervisor spec and the existing verification.md. (A go variant of a supervisor lives in detection.md #479, but the way the lightstore is treated and the parameters that are passes here in #509 seem much more to the point in the rust pseudo code in this PR). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is some great work. The amount of potential improvements it touches is amazing to see. Generally the direction is promising, some thoughts inline and here:
- If we start to develop this notion of a linked list a.k.a. chain, we should look closer at how we make it correct to construct. As we want to point to the previous block we ought to encode that in the linked list semantics.
- I'd be curious to see how much simpler the design can be if we assume that the
LightStore
is immutable. This would mean it needs reconstruction, which will make it very easy to reason in the case of fork recovery. Currently it's proposed that theLightStore
itself offers a recovery functionality. There is strong evidence that we might be better served with treating it as immutable and rather lean heavier on type encoding to for example have a deterministic function a la:fn recover(tainted: LightStore::Tainted, boundary: Height) -> LightStore::Trusted
- It's great that we trying to address the divergence we observed when querying the
Supervisor
- yet I think we would benefit from separating these concerns. On the surface it looks like we rework theLightStore
entity to give us historical data and traceability, while when disseminating the latest/highest state it is a really the most trustedChainPair
and we don't depend on the history of how we got there. Clarifying which component depends on what information will bring more clarity.
docs/spec/supervisor/supervisor.md
Outdated
```rust | ||
/// Stores the Light Blocks which could not be confirmed to have originated from | ||
/// the same chain as the predecesor light blocks. | ||
type FetchedStack = Stack<LightBlock>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the semantics of the stack? I don't believe there is a type definition in this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no semantics as we could use a Vec
for this, the goal was just to name it here and explain it is going to be used as a stack (push, pop, peek, is_empty) and not with random access.
docs/spec/supervisor/supervisor.md
Outdated
|
||
/// Pair of light block and height of the light block which was highest | ||
/// highest one for which we believe originated from the same chain | ||
type ChainPair = (LightBlock, Height); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming is confusing as it indicates that a Chain
is paired with something. What I'm also wondering doesn't the LightBlock
contain its Height
, what's the benefit of pairing it with the Height
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Height in this case is the height of the block which was used to check the light. For example if we inserted a light block A
into the Trace
after passing all the checks in verify_to_target using light block B of height h
. The ChainPair
for A
would be (A
, h
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it always the height of B
-1? In any case the term Height
is so ubiquitous everywhere that we should encode what height we are referring to here, which in turn an help challenge how it can be constructed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still the term ChainPair
for a pairing of block with height is miss-leading, imho.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a custom enum VerificationChain
.
|
||
// insert the light block of target height, simplifies the loop | ||
// we can say that at each iteration there should be something on stack | ||
fetch_stack.push(fetch_light_block(primary, target_height)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does fetch_light_block
do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just copied from the current specifications. In current code it's from the Io
component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would greatly benefit if this side-effect is not done in this function, it could be a parameter to be called with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was an error. The structure is changed a but now.
/// LightStore no longer has notions of Trusted, Verified, Unverified and Failed. | ||
/// The LightBlocks which are stored are all assumed to have originated from the | ||
/// same chain, irrespective of the peer from which we received it. The store | ||
/// provides the invariant that there is a 1-1 mapping from height to a LightBlock. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to ensure the 1-1 invariant we should think in terms of a Set
where we have a uniqueness constraint on the height, unless I miss-understand this part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would make more sense to have a Map<Height, LightBlock>
where keys would be automatically unique.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For clarification, every height can only exist once? And there is only exactly one LightBlock
per unique height?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that was the intention. At each point in time, there can be only one LightBlock
which can be considered to be from the valid blockchain. Conflict on a single height would mean there is a fork.
Co-authored-by: Alexander Simmerl <[email protected]>
@xla thanks for the comments. One thing which might not be clear just from the pseudo code and which I was not sure how to capture. This PR only addresses the case when we are doing forward verification without implying how the backward verification case will be done. For example lets say we start from height 1 and want to verify with height 10. One possible trace could be (1<- 5 <- 10) and after verifying 10 we get a request for height 8. Now this case is not clear for me yet, so I didn't imply anything about the LightStore. For example one can imagine a case where for height 8 we just used the previously fetched light block of height 5. In this case the 1 <- 5 <- 10 ^ - - 8 It does not seem reasonable to just insert 8 between 5 and 10 (1 <- 5 <-8 <- 10) because in the case of validator set change such that skipping trust can not be established between 8 and 10 and somebody wants to replay the verification process just with the To solve this we would need to also verify 10 starting from 8, however in this case the question is what do we do with the previous information that originally the |
// Verify | ||
let mut result = Err(NoPeer); | ||
// try with primaries until you succeed or there are no more peers | ||
while result.is_err() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like this will never terminate, making the rest of the code unreachable
|
||
// Verify | ||
let mut result = Err(NoPeer); | ||
// try with primaries until you succeed or there are no more peers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't you have a single primary at any moment?
fn recover(&mut self, boundary: Height) -> Result<VerificationChain, LightStoreError>; | ||
|
||
/// Returns the firsts LightBlock above if it exists. | ||
fn above(&self, height: Height) -> Result<&LightBlock, ListStoreError>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do you need above
and below
when you already have get
?
The ideas in the PR are great. I tried to incorporate them into tendermint/spec#159. |
Thanks for the discussions. Let's continue that the other PR tendermint/spec#159. |
rendered
As @josef-widder mentioned here is the pseudo code proposal for specification and code changes. I was not sure how to name this and where to put it so we can change it as others see fit.
Goals?
The proposed pseudo code should solve the main issues of
LightStore
having an unbounded interface which makes code of skipping forward verification more complicated than needed. If implemented it should solve several issues which came out of it.Possible issues?
LightStore
requires that when inserting new information it is always possible to build a verification chain with what is already in itLightBlocks
are impossible to recreate in this situationProposal details
LightStore
is now split:1. what were once verified LightBlocks are now stored in
Trace
2.
Unverified
LightBlocks are now on theFetchedStack
3. There are no longer
Failed
LightBlocks as there can be only one and that one is returned as an error reason inverify_to_target
4. The notion of
Trusted
is removed, and with the sequential supervisor everything in what is nowLightStore
is what were previously trusted.LightBlocks
were verified can be retrieved from theLightStore
LightStore
can recover from possible forks in the future by removingLightBlocks
which are now considered to be from a fork.Order in which to review for easier understanding
LightStore
(excluding)ForkDetection
andRelayer
requirementsIssue which prompted this PR
Issues this approach should solve.
LightStore
nowLightBlock
are no longer possible as we always ask for the highest from the supervisorLightStore
Should help with easier implementation of
Issues which should be reworded depending on the changes here
LightStore
is assumed to have passed verification at some point