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

Light Client model-based testing #529

Closed
wants to merge 62 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d9c6b5e
#414: factor out common parts from tests/lite.rs into tests/lite_tests
andrey-kuprianov Jul 31, 2020
0ac86a3
#414: lite_tests: add Command/CommandRun wrapper for running external…
andrey-kuprianov Jul 31, 2020
e435ae3
#414: add history variables to Lightclient_A_1.tla
andrey-kuprianov Aug 4, 2020
f0e7599
#414: add use default index in testgen vote
andrey-kuprianov Aug 4, 2020
b361b51
#414: add first model-based test
andrey-kuprianov Aug 4, 2020
08450a7
#414: more structure for test cases
andrey-kuprianov Aug 5, 2020
04ab1aa
#414: simplify running external commands
andrey-kuprianov Aug 6, 2020
f7b3165
#414: add simple TLA+ test
andrey-kuprianov Aug 6, 2020
cb478f2
#414: run apalache on TLA+ test; fix Command spawn errors
andrey-kuprianov Aug 6, 2020
d65a72e
#414: fix small typos
andrey-kuprianov Aug 6, 2020
a720487
#414: refactor: add commonly used test utils
andrey-kuprianov Aug 6, 2020
49ef36d
#414: more refactoring
andrey-kuprianov Aug 6, 2020
b80159c
#414: add tests/utils/jsonatr.rs
andrey-kuprianov Aug 7, 2020
6a3831b
#414: add tests/utils/jsonatr-lib
andrey-kuprianov Aug 7, 2020
e36ba00
#414: full model-based test TLA+ -> Apalache -> Jsonatr -> lite::veri…
andrey-kuprianov Aug 7, 2020
368975c
#414: check for tendermint-testgen in PATH
andrey-kuprianov Aug 7, 2020
da11fc1
#414: testgen: add absent votes in the commit
andrey-kuprianov Aug 10, 2020
8ff0f93
#414: testgen: add time command; make timestamps abstract
andrey-kuprianov Aug 10, 2020
4715e18
#414: make lite test timestamps abstract
andrey-kuprianov Aug 10, 2020
d223773
#414: LightClient_A_1: add time to history variables
andrey-kuprianov Aug 10, 2020
ec2ba4c
#414: Blockchain_A_1.tla: fix discrepancy between the model and the …
andrey-kuprianov Aug 10, 2020
0df4320
#414: Lightclient_A_1.tla: store whole state sequence in history
andrey-kuprianov Aug 10, 2020
e9a099f
#414: more TLA+ lite tests
andrey-kuprianov Aug 10, 2020
f04a163
#414: Blockchain_A_1.tla: disallow generating headers with negative time
andrey-kuprianov Aug 11, 2020
b860976
#414 adjust static test to the new format
andrey-kuprianov Aug 11, 2020
119088c
#414 adapt jsonatr-lib to history variables in the lite model
andrey-kuprianov Aug 11, 2020
9932a1d
#414 testgen header: handle the case of empty validators
andrey-kuprianov Aug 11, 2020
249a24f
#414 lite-model-based: account for apalache run w/o counterexample
andrey-kuprianov Aug 11, 2020
0ff0b99
#414: auto-run model-based test batches
andrey-kuprianov Aug 11, 2020
2f03aef
#414: add first test batch
andrey-kuprianov Aug 11, 2020
0f9f187
#414 Merge branch 'master' into andrey/lite-model-based
andrey-kuprianov Aug 12, 2020
ae847f3
Merge branch 'andrey/testgen-tester' into andrey/lite-model-based
andrey-kuprianov Aug 18, 2020
43706ca
#414 move utilities for running commands into testgen
andrey-kuprianov Aug 18, 2020
3c4d6cf
#414 move model-based tests into light-client crate
andrey-kuprianov Aug 18, 2020
e8a897e
#414 move files around
andrey-kuprianov Aug 19, 2020
3074bde
#414 cargo fmt
andrey-kuprianov Aug 19, 2020
f440ed4
#414: testgen/Tester: better handling of test environments
andrey-kuprianov Aug 19, 2020
377d6f2
#414: factor out verify_single/bisection into tests.rs
andrey-kuprianov Aug 19, 2020
4a0a27a
#414 adapted LightClient model-based tests to the new testgen/tester
andrey-kuprianov Aug 19, 2020
3a7d486
#414 align LightClient spec verdicts with the implementation verdicts
andrey-kuprianov Aug 19, 2020
af36c98
#414 add symlink to jsonatr-lib
andrey-kuprianov Aug 19, 2020
d432816
#414 Merge branch 'master' into andrey/lite-model-based
andrey-kuprianov Aug 19, 2020
6efc959
#414 better handling of Apalache runs
andrey-kuprianov Aug 20, 2020
99e51a4
#414 adapt model-based tests to changes in LightClient model
andrey-kuprianov Aug 20, 2020
67c1b1b
#414 testgen/tester: remove redundant test environments from function…
andrey-kuprianov Aug 20, 2020
2673138
#414 testgen/tester: add test batches
andrey-kuprianov Aug 20, 2020
f6e5b91
#414 adapt LightClient model-based test to use test batches
andrey-kuprianov Aug 20, 2020
64b10e0
#414 adapt LightClient model to implementation: headers with equal ti…
andrey-kuprianov Aug 20, 2020
ba4349d
#414 model-based test: add model mutation to negate test into invariant
andrey-kuprianov Aug 20, 2020
380c33b
#414 testgen/tester: add logging
andrey-kuprianov Aug 20, 2020
aac7970
#414 testgen/tester: add reporting
andrey-kuprianov Aug 20, 2020
d049a07
#414 testgen/tester: better reporting
andrey-kuprianov Aug 20, 2020
e6ae06c
#414 cargo fmt
andrey-kuprianov Aug 20, 2020
c1cb954
#414 cargo clippy
andrey-kuprianov Aug 20, 2020
f63a7e0
#414 shift model mutation and error messages from model-based test to…
andrey-kuprianov Aug 21, 2020
e2fbe20
#414: remove first-model-based-test
andrey-kuprianov Aug 21, 2020
6e3c83a
#414 add some static tests
andrey-kuprianov Aug 21, 2020
f271177
#414 add model-based README.md
andrey-kuprianov Aug 21, 2020
389de68
#414 update link in model-based README.md
andrey-kuprianov Aug 21, 2020
47875aa
#414 cargo fmt
andrey-kuprianov Aug 21, 2020
935f785
#414 add some docs to testgen/Tester
andrey-kuprianov Aug 21, 2020
dbee1c9
#414: remove redundant files
andrey-kuprianov Aug 21, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/spec/lightclient/verification/Blockchain_A_1.tla
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ LBT == [header |-> BT, Commits |-> {NT}]

(* the header is still within the trusting period *)
InTrustingPeriod(header) ==
now <= header.time + TRUSTING_PERIOD
now < header.time + TRUSTING_PERIOD
Copy link
Member

Choose a reason for hiding this comment

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

Does this change come from the implementation or the English spec? Are those consistent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this comes from the implementation, as otherwise I get a testing error: spec behavior is different from what we have in the implementation. And the English spec contains this trusted.Header.Time > now - trustingPeriod, so I guess this means that the English spec and the implementation agree, but the TLA+ spec is wrong in that respect; could you please verify that it's true?

Copy link
Member

Choose a reason for hiding this comment

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

will do.

Copy link
Member

Choose a reason for hiding this comment

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

So now we do model-based testing as conformance test between English spec, TLA+, and the implementation. Amazing 😉


(*
Given a function pVotingPower \in D -> Powers for some D \subseteq AllNodes
Expand Down Expand Up @@ -110,7 +110,7 @@ FaultAssumption(pFaultyNodes, pNow, pBlockchain) ==
(* Can a block be produced by a correct peer, or an authenticated Byzantine peer *)
IsLightBlockAllowedByDigitalSignatures(ht, block) ==
\/ block.header = blockchain[ht] \* signed by correct and faulty (maybe)
\/ block.Commits \subseteq Faulty /\ block.header.height = ht \* signed only by faulty
\/ block.Commits \subseteq Faulty /\ block.header.height = ht /\ block.header.time > 0 \* signed only by faulty
Copy link
Member

Choose a reason for hiding this comment

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

I guess faulty processes may be allowed to write any time, also negative. Or is the time stored in some unsigned domain in the implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it is indeed stored and serialized/deserialized as an unsigned integer, so signed integers do no make sense and only add confusion, I am afraid

Copy link
Member

Choose a reason for hiding this comment

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

I guess in the English spec we implicitly also assume positive times. Perhaps we should clarify it there explicitly.


(*
Initialize the blockchain to the ultimate height right in the initial states.
Expand Down
49 changes: 36 additions & 13 deletions docs/spec/lightclient/verification/Lightclient_A_1.tla
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,28 @@ VARIABLES
lightBlockStatus, (* a function from heights to block statuses *)
latestVerified (* the latest verified block *)

(* the variables of the lite client *)
lcvars == <<state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified>>
(* the variables of the lite client *)
lcvars == <<state, nextHeight, fetchedLightBlocks, lightBlockStatus, latestVerified>>

(* The light client history, which is the function mapping states (0 .. nprobes) to the record with fields:
- verified: the latest verified block in this state
- current: the block that is being checked in this state
- now: the time point in this state
- verdict: the light client verdict for the `current` block in this state
*)
VARIABLE
history

InitHistory(verified, current, now, verdict) ==
LET first == [ verified |-> verified, current |-> current, now |-> now, verdict |-> verdict ]
IN history = [ n \in {0} |-> first ]

NextHistory(verified, current, now, verdict) ==
LET last == [ verified |-> verified, current |-> current, now |-> now, verdict |-> verdict ]
np == nprobes+1
IN history' = [ n \in DOMAIN history \union {np} |->
IF n = np THEN last ELSE history[n]]


(******************* Blockchain instance ***********************************)

Expand Down Expand Up @@ -74,7 +94,7 @@ ValidAndVerifiedPre(trusted, untrusted) ==
/\ BC!InTrustingPeriod(thdr)
/\ thdr.height < uhdr.height
\* the trusted block has been created earlier (no drift here)
/\ thdr.time <= uhdr.time
/\ thdr.time < uhdr.time
/\ untrusted.Commits \subseteq uhdr.VS
/\ LET TP == Cardinality(uhdr.VS)
SP == Cardinality(untrusted.Commits)
Expand Down Expand Up @@ -106,7 +126,7 @@ SignedByOneThirdOfTrusted(trusted, untrusted) ==
*)
ValidAndVerified(trusted, untrusted) ==
IF ~ValidAndVerifiedPre(trusted, untrusted)
THEN "FAILED_VERIFICATION"
THEN "INVALID"
ELSE IF ~BC!InTrustingPeriod(untrusted.header)
(* We leave the following test for the documentation purposes.
The implementation should do this test, as signature verification may be slow.
Expand All @@ -115,8 +135,8 @@ ValidAndVerified(trusted, untrusted) ==
THEN "FAILED_TRUSTING_PERIOD"
ELSE IF untrusted.header.height = trusted.header.height + 1
\/ SignedByOneThirdOfTrusted(trusted, untrusted)
THEN "OK"
ELSE "CANNOT_VERIFY"
THEN "SUCCESS"
ELSE "NOT_ENOUGH_TRUST"
Comment on lines +138 to +139
Copy link
Member

Choose a reason for hiding this comment

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

I think OK and CANNOT_VERIFY are used in the English spec. Are SUCCESS and NOT_ENOUGH_TRUST used in the implementation? If yes, we should adapt I guess the English spec.

Copy link
Contributor Author

@andrey-kuprianov andrey-kuprianov Aug 26, 2020

Choose a reason for hiding this comment

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

I've taken the verdicts from the implementation. There is another issue I've got, namely that according to the TLA+ constraints the verdict FAILED_TRUSTING_PERIOD cannot ever be produced. At least it's impossible to obtain a test with such verdict from Apalache, and my quick manual check on the TLA+ constraints tells me it's indeed logically impossible. Could you please verify that? In case it's true, it may be makes sense to remove such a verdict. And I would actually update the English spec as well, as I find the implementation names a bit more informative

Copy link
Member

Choose a reason for hiding this comment

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

I will have a look with @konnov

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see why FAILED_TRUSTING_PERIOD cannot be produced. It is produced in line 115. However, it is not propagated into state. Do you like to see it in state?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this PR is closed; let's discuss on another PR: 546


(*
Initial states of the light client.
Expand All @@ -135,6 +155,7 @@ LCInit ==
/\ lightBlockStatus = [h \in {TRUSTED_HEIGHT} |-> "StateVerified"]
\* the latest verified block the the trusted block
/\ latestVerified = trustedLightBlock
/\ InitHistory(trustedLightBlock, trustedLightBlock, now, "SUCCESS")

\* block should contain a copy of the block from the reference chain, with a matching commit
CopyLightBlockFromChain(block, height) ==
Expand Down Expand Up @@ -207,8 +228,9 @@ VerifyToTargetLoop ==
/\ nprobes' = nprobes + 1
\* Verify the current block
/\ LET verdict == ValidAndVerified(latestVerified, current) IN
NextHistory(latestVerified, current, now, verdict) /\
\* Decide whether/how to continue
CASE verdict = "OK" ->
CASE verdict = "SUCCESS" ->
/\ lightBlockStatus' = LightStoreUpdateStates(lightBlockStatus, nextHeight, "StateVerified")
/\ latestVerified' = current
/\ state' =
Expand All @@ -219,7 +241,7 @@ VerifyToTargetLoop ==
/\ CanScheduleTo(newHeight, current, nextHeight, TARGET_HEIGHT)
/\ nextHeight' = newHeight

[] verdict = "CANNOT_VERIFY" ->
[] verdict = "NOT_ENOUGH_TRUST" ->
(*
do nothing: the light block current passed validation, but the validator
set is too different to verify it. We keep the state of
Expand All @@ -244,7 +266,8 @@ VerifyToTargetLoop ==
VerifyToTargetDone ==
/\ latestVerified.header.height >= TARGET_HEIGHT
/\ state' = "finishedSuccess"
/\ UNCHANGED <<nextHeight, nprobes, fetchedLightBlocks, lightBlockStatus, latestVerified>>
/\ UNCHANGED <<nextHeight, nprobes, fetchedLightBlocks, lightBlockStatus, latestVerified>>
/\ UNCHANGED <<history>>

(********************* Lite client + Blockchain *******************)
Init ==
Expand Down Expand Up @@ -310,7 +333,7 @@ CorrectnessInv ==
fetchedLightBlocks[h].header = blockchain[h]

(**
Check that the sequence of the headers in storedLightBlocks satisfies ValidAndVerified = "OK" pairwise
Check that the sequence of the headers in storedLightBlocks satisfies ValidAndVerified = "SUCCESS" pairwise
This property is easily violated, whenever a header cannot be trusted anymore.
*)
StoredHeadersAreVerifiedInv ==
Expand All @@ -322,7 +345,7 @@ StoredHeadersAreVerifiedInv ==
\/ \E mh \in DOMAIN fetchedLightBlocks:
lh < mh /\ mh < rh
\* or we can verify the right one using the left one
\/ "OK" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh])
\/ "SUCCESS" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh])

\* An improved version of StoredHeadersAreSound, assuming that a header may be not trusted.
\* This invariant candidate is also violated,
Expand All @@ -336,7 +359,7 @@ StoredHeadersAreVerifiedOrNotTrustedInv ==
\/ \E mh \in DOMAIN fetchedLightBlocks:
lh < mh /\ mh < rh
\* or we can verify the right one using the left one
\/ "OK" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh])
\/ "SUCCESS" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh])
\* or the left header is outside the trusting period, so no guarantees
\/ ~BC!InTrustingPeriod(fetchedLightBlocks[lh].header)

Expand All @@ -359,7 +382,7 @@ ProofOfChainOfTrustInv ==
\* or the left header is outside the trusting period, so no guarantees
\/ ~(BC!InTrustingPeriod(fetchedLightBlocks[lh].header))
\* or we can verify the right one using the left one
\/ "OK" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh])
\/ "SUCCESS" = ValidAndVerified(fetchedLightBlocks[lh], fetchedLightBlocks[rh])

(**
* When the light client terminates, there are no failed blocks. (Otherwise, someone lied to us.)
Expand Down
46 changes: 46 additions & 0 deletions light-client/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ use tendermint_rpc as rpc;

use crate::components::clock::Clock;
use crate::components::io::{AtHeight, Io, IoError};
use crate::components::verifier::{ProdVerifier, Verdict, Verifier};
use crate::errors::Error;
use crate::evidence::EvidenceReporter;
use crate::light_client::{LightClient, Options};
use crate::state::State;
use contracts::contract_trait;
use std::collections::HashMap;
use std::time::Duration;
use tendermint::block::Height as HeightStr;
use tendermint::evidence::{Duration as DurationStr, Evidence};

Expand Down Expand Up @@ -148,6 +153,47 @@ impl MockEvidenceReporter {
}
}

pub fn verify_single(
trusted_state: Trusted,
input: LightBlock,
trust_threshold: TrustThreshold,
trusting_period: Duration,
clock_drift: Duration,
now: Time,
) -> Result<LightBlock, Verdict> {
let verifier = ProdVerifier::default();

let trusted_state = LightBlock::new(
trusted_state.signed_header,
trusted_state.next_validators.clone(),
trusted_state.next_validators,
default_peer_id(),
);

let options = Options {
trust_threshold,
trusting_period,
clock_drift,
};

let result = verifier.verify(&input, &trusted_state, &options, now);

match result {
Verdict::Success => Ok(input),
error => Err(error),
}
}

pub fn verify_bisection(
untrusted_height: Height,
light_client: &mut LightClient,
state: &mut State,
) -> Result<Vec<LightBlock>, Error> {
light_client
.verify_to_target(untrusted_height, state)
.map(|_| state.get_trace(untrusted_height))
}

// -----------------------------------------------------------------------------
// Everything below is a temporary workaround for the lack of `provider` field
// in the light blocks serialized in the JSON fixtures.
Expand Down
53 changes: 6 additions & 47 deletions light-client/tests/light_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ use tendermint_light_client::{
components::{
io::{AtHeight, Io},
scheduler,
verifier::{ProdVerifier, Verdict, Verifier},
verifier::ProdVerifier,
},
errors::{Error, ErrorKind},
light_client::{LightClient, Options},
state::State,
store::{memory::MemoryStore, LightStore},
tests::{Trusted, *},
types::{Height, LightBlock, Status, Time, TrustThreshold},
types::{LightBlock, Status, TrustThreshold},
};

use tendermint_testgen::Tester;
Expand All @@ -21,47 +21,6 @@ use tendermint_testgen::Tester;
// https://github.com/informalsystems/conformance-tests
const TEST_FILES_PATH: &str = "./tests/support/";

fn verify_single(
trusted_state: Trusted,
input: LightBlock,
trust_threshold: TrustThreshold,
trusting_period: Duration,
clock_drift: Duration,
now: Time,
) -> Result<LightBlock, Verdict> {
let verifier = ProdVerifier::default();

let trusted_state = LightBlock::new(
trusted_state.signed_header,
trusted_state.next_validators.clone(),
trusted_state.next_validators,
default_peer_id(),
);

let options = Options {
trust_threshold,
trusting_period,
clock_drift,
};

let result = verifier.verify(&input, &trusted_state, &options, now);

match result {
Verdict::Success => Ok(input),
error => Err(error),
}
}

fn verify_bisection(
untrusted_height: Height,
light_client: &mut LightClient,
state: &mut State,
) -> Result<Vec<LightBlock>, Error> {
light_client
.verify_to_target(untrusted_height, state)
.map(|_| state.get_trace(untrusted_height))
}

struct BisectionTestResult {
untrusted_light_block: LightBlock,
new_states: Result<Vec<LightBlock>, Error>,
Expand Down Expand Up @@ -229,17 +188,17 @@ fn bisection_lower_test(tc: TestBisection<AnonLightBlock>) {

#[test]
fn run_single_step_tests() {
let mut tester = Tester::new(TEST_FILES_PATH);
let mut tester = Tester::new("single_step", TEST_FILES_PATH);
tester.add_test("single-step test", single_step_test);
tester.run_foreach_in_dir("single_step");
tester.print_results();
tester.finalize();
}

#[test]
fn run_bisection_tests() {
let mut tester = Tester::new(TEST_FILES_PATH);
let mut tester = Tester::new("bisection", TEST_FILES_PATH);
tester.add_test("bisection test", bisection_test);
tester.add_test("bisection lower test", bisection_lower_test);
tester.run_foreach_in_dir("bisection/single_peer");
tester.print_results();
tester.finalize();
}
Loading