mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-08 06:15:33 +00:00
179 lines
6.9 KiB
Markdown
179 lines
6.9 KiB
Markdown
# Results of Discussions and Decisions
|
|
|
|
- Generating a minimal proof of fork (as suggested in [Issue #5083](https://github.com/tendermint/tendermint/issues/5083)) is too costly at the light client
|
|
- we do not know all lightblocks from the primary
|
|
- therefore there are many scenarios. we might even need to ask
|
|
the primary again for additional lightblocks to isolate the
|
|
branch.
|
|
|
|
> For instance, the light node starts with block at height 1 and the
|
|
> primary provides a block of height 10 that the light node can
|
|
> verify immediately. In cross-checking, a secondary now provides a
|
|
> conflicting header b10 of height 10 that needs another header b5
|
|
> of height 5 to
|
|
> verify. Now, in order for the light node to convince the primary:
|
|
>
|
|
> - The light node cannot just sent b5, as it is not clear whether
|
|
> the fork happened before or after 5
|
|
> - The light node cannot just send b10, as the primary would also
|
|
> need b5 for verification
|
|
> - In order to minimize the evidence, the light node may try to
|
|
> figure out where the branch happens, e.g., by asking the primary
|
|
> for height 5 (it might be that more queries are required, also
|
|
> to the secondary. However, assuming that in this scenario the
|
|
> primary is faulty it may not respond.
|
|
|
|
As the main goal is to catch misbehavior of the primary,
|
|
evidence generation and punishment must not depend on their
|
|
cooperation. So the moment we have proof of fork (even if it
|
|
contains several light blocks) we should submit right away.
|
|
|
|
- decision: "full" proof of fork consists of two traces that originate in the
|
|
same lightblock and lead to conflicting headers of the same height.
|
|
|
|
- For submission of proof of fork, we may do some optimizations, for
|
|
instance, we might just submit a trace of lightblocks that verifies a block
|
|
different from the one the full node knows (we do not send the trace
|
|
the primary gave us back to the primary)
|
|
|
|
- The light client attack is via the primary. Thus we try to
|
|
catch if the primary installs a bad light block
|
|
- We do not check secondary against secondary
|
|
- For each secondary, we check the primary against one secondary
|
|
|
|
- Observe that just two blocks for the same height are not
|
|
sufficient proof of fork.
|
|
One of the blocks may be bogus [TMBC-BOGUS.1] which does
|
|
not constitute slashable behavior.
|
|
Which leads to the question whether the light node should try to do
|
|
fork detection on its initial block (from subjective
|
|
initialization). This could be done by doing backwards verification
|
|
(with the hashes) until a bifurcation block is found.
|
|
While there are scenarios where a
|
|
fork could be found, there is also the scenario where a faulty full
|
|
node feeds the light node with bogus light blocks and forces the light
|
|
node to check hashes until a bogus chain is out of the trusting period.
|
|
As a result, the light client
|
|
should not try to detect a fork for its initial header. **The initial
|
|
header must be trusted as is.**
|
|
|
|
# Light Client Sequential Supervisor
|
|
|
|
**TODO:** decide where (into which specification) to put the
|
|
following:
|
|
|
|
We describe the context on which the fork detector is called by giving
|
|
a sequential version of the supervisor function.
|
|
Roughly, it alternates two phases namely:
|
|
|
|
- Light Client Verification. As a result, a header of the required
|
|
height has been downloaded from and verified with the primary.
|
|
- Light Client Fork Detections. As a result the header has been
|
|
cross-checked with the secondaries. In case there is a fork we
|
|
submit "proof of fork" and exit.
|
|
|
|
#### **[LC-FUNC-SUPERVISOR.1]:**
|
|
|
|
```go
|
|
func Sequential-Supervisor () (Error) {
|
|
loop {
|
|
// get the next height
|
|
nextHeight := input();
|
|
|
|
// Verify
|
|
result := NoResult;
|
|
while result != ResultSuccess {
|
|
lightStore,result := VerifyToTarget(primary, lightStore, nextHeight);
|
|
if result == ResultFailure {
|
|
// pick new primary (promote a secondary to primary)
|
|
/// and delete all lightblocks above
|
|
// LastTrusted (they have not been cross-checked)
|
|
Replace_Primary();
|
|
}
|
|
}
|
|
|
|
// Cross-check
|
|
PoFs := Forkdetector(lightStore, PoFs);
|
|
if PoFs.Empty {
|
|
// no fork detected with secondaries, we trust the new
|
|
// lightblock
|
|
LightStore.Update(testedLB, StateTrusted);
|
|
}
|
|
else {
|
|
// there is a fork, we submit the proofs and exit
|
|
for i, p range PoFs {
|
|
SubmitProofOfFork(p);
|
|
}
|
|
return(ErrorFork);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**TODO:** finish conditions
|
|
|
|
- Implementation remark
|
|
- Expected precondition
|
|
- *lightStore* initialized with trusted header
|
|
- *PoFs* empty
|
|
- Expected postcondition
|
|
- runs forever, or
|
|
- is terminated by user and satisfies LightStore invariant, or **TODO**
|
|
- has submitted proof of fork upon detecting a fork
|
|
- Error condition
|
|
- none
|
|
|
|
----
|
|
|
|
# Semantics of the LightStore
|
|
|
|
Currently, a lightblock in the lightstore can be in one of the
|
|
following states:
|
|
|
|
- StateUnverified
|
|
- StateVerified
|
|
- StateFailed
|
|
- StateTrusted
|
|
|
|
The intuition is that `StateVerified` captures that the lightblock has
|
|
been verified with the primary, and `StateTrusted` is the state after
|
|
successful cross-checking with the secondaries.
|
|
|
|
Assuming there is **always one correct node among primary and
|
|
secondaries**, and there is no fork on the blockchain, lightblocks that
|
|
are in `StateTrusted` can be used by the user with the guarantee of
|
|
"finality". If a block in `StateVerified` is used, it might be that
|
|
detection later finds a fork, and a roll-back might be needed.
|
|
|
|
**Remark:** The assumption of one correct node, does not render
|
|
verification useless. It is true that if the primary and the
|
|
secondaries return the same block we may trust it. However, if there
|
|
is a node that provides a different block, the light node still needs
|
|
verification to understand whether there is a fork, or whether the
|
|
different block is just bogus (without any support of some previous
|
|
validator set).
|
|
|
|
**Remark:** A light node may choose the full nodes it communicates
|
|
with (the light node and the full node might even belong to the same
|
|
stakeholder) so the assumption might be justified in some cases.
|
|
|
|
In the future, we will do the following changes
|
|
|
|
- we assume that only from time to time, the light node is
|
|
connected to a correct full node
|
|
- this means for some limited time, the light node might have no
|
|
means to defend against light client attacks
|
|
- as a result we do not have finality
|
|
- once the light node reconnects with a correct full node, it
|
|
should detect the light client attack and submit evidence.
|
|
|
|
Under these assumptions, `StateTrusted` loses its meaning. As a
|
|
result, it should be removed from the API. We suggest that we replace
|
|
it with a flag "trusted" that can be used
|
|
|
|
- internally for efficiency reasons (to maintain
|
|
[LCD-INV-TRUSTED-AGREED.1] until a fork is detected)
|
|
- by light client based on the "one correct full node" assumption
|
|
|
|
----
|