Compare commits

...

43 Commits

Author SHA1 Message Date
Anca Zamfir
94f0532c44 Merge branch 'wb/proposer-based-timestamps' into anca/prevote_nil_untimely 2022-01-11 11:49:26 +01:00
Anca Zamfir
2617a5cf33 Prevote nil if proposal is not timely (#7415)
* Prevote nil if not timely

* William's suggestion to get the proposal from the proposer instead of
generating it.

* Don't check rhs for genesis block

* Update IsTimely to match the specification

* Fix proposal tests

* Add more timely tests and check votes

* Mark proposal invalid in SetProposal, fix in the future test

* save proposal time on roundstate

* received -> receive

* always reset proposal time

* Add IsTimely test for genesis proposal

* Check timely before ValidateBlock

* Review comments from Daniel

Co-authored-by: William Banfield <wbanfield@gmail.com>
2022-01-11 11:44:07 +01:00
Anca Zamfir
f15ee331b8 Fix comments, logs, timely cleanup 2022-01-11 10:42:15 +01:00
Anca Zamfir
4be630175b Fix comment 2022-01-11 10:09:41 +01:00
Anca Zamfir
693bae7a29 Review comments from Daniel 2022-01-07 12:52:57 +01:00
Anca Zamfir
bd99771b27 Check timely before ValidateBlock 2022-01-07 11:12:29 +01:00
Anca Zamfir
e065fba752 Add IsTimely test for genesis proposal 2022-01-04 17:07:54 +01:00
Anca Zamfir
2678191ccf Merge branch 'wb/proposer-based-timestamps' into anca/prevote_nil_untimely 2022-01-04 16:27:30 +01:00
William Banfield
a145696a9d always reset proposal time 2022-01-04 09:49:04 -05:00
William Banfield
5e83436019 received -> receive 2022-01-04 09:42:45 -05:00
William Banfield
9ade45d81b consensus: check that proposal is non-nil before voting (#7480) 2021-12-22 11:09:57 -05:00
William Banfield
cda3a1bbd0 save proposal time on roundstate 2021-12-22 11:01:03 -05:00
Anca Zamfir
6f23bc8404 Merge branch 'wb/proposer-based-timestamps' into anca/prevote_nil_untimely 2021-12-21 20:51:17 +01:00
Anca Zamfir
e6d8b7c043 Fix pbts tests (#7413)
* Allow nil block ID check in ensureProposalWithTimout

* William's suggestion to get the proposal from the proposer instead of
generating it.

* Remove error check on service stop
2021-12-21 20:39:11 +01:00
Anca Zamfir
9adbd50808 Cleanup 2021-12-21 20:34:25 +01:00
Anca Zamfir
6b8cf31510 Mark proposal invalid in SetProposal, fix in the future test 2021-12-21 20:28:53 +01:00
Anca Zamfir
f95abc275c Add more timely tests and check votes 2021-12-21 18:15:54 +01:00
Anca Zamfir
59e360472a Fix proposal tests 2021-12-20 18:19:56 +01:00
Anca Zamfir
692b2299dd Fix lint errors 2021-12-20 17:34:51 +01:00
Anca Zamfir
f61ed903a8 Update IsTimely to match the specification 2021-12-20 17:29:49 +01:00
Anca Zamfir
8009903357 Update IsTimely comment 2021-12-20 15:44:32 +01:00
Anca Zamfir
9b7ab1d3c2 Revert ensureTimeout change 2021-12-20 15:24:44 +01:00
Anca Zamfir
cd36671a0d Fix TestIsTimely 2021-12-20 00:43:12 +01:00
Anca Zamfir
ea3398bde8 Don't check lhs for genesis block 2021-12-20 00:36:23 +01:00
Anca Zamfir
ba42727a9b Merge branch 'anca/fix_pbts_tests' into anca/prevote_nil_untimely 2021-12-19 14:10:15 +01:00
Anca Zamfir
a587cfddbb Bring back block ID check in ensureProposalWithTimout 2021-12-18 03:26:30 +01:00
Anca Zamfir
c175313621 Remove error check on service stop 2021-12-18 03:19:58 +01:00
Anca Zamfir
8c02f2c2e6 William's suggestion to get the proposal from the proposer instead of
generating it.
2021-12-18 03:01:26 +01:00
Anca Zamfir
bb43c8139d Merge branch 'wb/proposer-based-timestamps' into anca/fix_pbts_tests 2021-12-18 02:30:25 +01:00
Anca Zamfir
e8a37cefb2 Merge commit 'b03dced9a' into wb/proposer-based-timestamps 2021-12-18 02:27:36 +01:00
William Banfield
56a20056ec internal/consensus: prevote nil if proposal timestamp does not match (#7391)
This change updates the proposal logic to use the block's timestamp in the proposal message. It adds an additional piece of validation logic to the prevote step to check that the block's timestamp matches the proposal message's timestamp.
2021-12-15 14:46:55 -05:00
Anca Zamfir
9cd4cfed6b Remove block Id checks and enable tests 2021-12-14 20:09:02 -05:00
Anca Zamfir
2801a2baf1 Merge branch 'wb/proposer-based-timestamps' into anca/fix_pbts_tests 2021-12-10 20:51:32 -05:00
William Banfield
e4598b1de1 internal/consensus: remove proposal wait time (#7418) 2021-12-09 17:18:41 -05:00
Anca Zamfir
7180e47e92 Prevote nil if not timely 2021-12-09 08:25:54 -05:00
Anca Zamfir
b03dced9af Fix compilation 2021-12-09 08:13:49 -05:00
Anca Zamfir
8f204cf5c7 Remove MedianTime, set block time to Now() (#7382)
* Remove MedianTime, set block time to Now()

* Fix goimports

* Fix import ordering
2021-12-09 05:01:27 +01:00
William Banfield
e91bac3565 internal/consensus: proposer waits for previous block time (#7376)
This change introduces the logic to have the proposer wait until the previous block time has passed before attempting to propose the next block.

The change achieves this by by adding a new clause into the enterPropose state machine method. The method now checks if the validator is the proposer and if the validator's clock is behind the previous block's time. If the validator's clock is behind the previous block time, it schedules a timeout to re-enter the enter propose method after enough time has passed.
2021-12-08 11:23:33 -05:00
William Banfield
a9b2bbd70d types: add new consensus params from proto (#7354)
This change adds the new TimingParams proto messages. These new messages were build using the wb/proposer-based-timestamps branch on the spec repo.
This change also adds validation that these values are positive when parsed and adds the new parameters into the existing tests.
2021-12-05 19:20:37 -05:00
William Banfield
a9aab99b41 internal/consensus: refactor ensure functions to use a common function (#7373)
* internal/consensus: refactor the common_test functions to use a single timeout function

* remove ensurePrecommit

* Update internal/consensus/common_test.go

Co-authored-by: M. J. Fromberger <fromberger@interchain.io>

* join lines for fatal messages

Co-authored-by: M. J. Fromberger <fromberger@interchain.io>

Co-authored-by: M. J. Fromberger <fromberger@interchain.io>
2021-12-02 19:09:02 -05:00
Sam Kleinman
884e4e99ca tools: remove tm-signer-harness (#7370) 2021-12-02 15:36:20 -05:00
William Banfield
591cc87669 types: remove accuracy from timestamp params (#7341) 2021-11-30 11:52:51 -05:00
William Banfield
1d68f340a6 consensus: ensure proposal receipt waits for maxWaitingTime (#7307)
* consensus: ensure proposal receipt waits for maxWaitingTime

* rebase fixups

* lint++

* lint++

* register result chan separately

* lint++
2021-11-30 09:45:36 -05:00
28 changed files with 1520 additions and 1531 deletions

View File

@@ -41,6 +41,7 @@ Special thanks to external contributors on this release:
- [cli] [#7033](https://github.com/tendermint/tendermint/pull/7033) Add a `rollback` command to rollback to the previous tendermint state in the event of non-determinstic app hash or reverting an upgrade.
- [mempool, rpc] \#7041 Add removeTx operation to the RPC layer. (@tychoish)
- [consensus] \#7376 Update the proposal logic per the Propose-based timestamps specification so that the proposer will wait for the previous block time to occur before proposing the next block. (@williambanfield)
### IMPROVEMENTS

View File

@@ -569,6 +569,10 @@ var testGenesisFmt = `{
"max_gas": "-1",
"time_iota_ms": "10"
},
"timing": {
"message_delay": "500000000",
"precision": "10000000"
},
"evidence": {
"max_age_num_blocks": "100000",
"max_age_duration": "172800000000000",

View File

@@ -12,7 +12,6 @@ Tendermint has some tools that are associated with it for:
- [Debugging](./debugging/pro.md)
- [Benchmarking](#benchmarking)
- [Testnets](#testnets)
- [Validation of remote signers](./remote-signer-validation.md)
## Benchmarking

View File

@@ -1,156 +0,0 @@
# Remote Signer
Located under the `tools/tm-signer-harness` folder in the [Tendermint
repository](https://github.com/tendermint/tendermint).
The Tendermint remote signer test harness facilitates integration testing
between Tendermint and remote signers such as
[tkkms](https://github.com/iqlusioninc/tmkms). Such remote signers allow for signing
of important Tendermint messages using
[HSMs](https://en.wikipedia.org/wiki/Hardware_security_module), providing
additional security.
When executed, `tm-signer-harness`:
1. Runs a listener (either TCP or Unix sockets).
2. Waits for a connection from the remote signer.
3. Upon connection from the remote signer, executes a number of automated tests
to ensure compatibility.
4. Upon successful validation, the harness process exits with a 0 exit code.
Upon validation failure, it exits with a particular exit code related to the
error.
## Prerequisites
Requires the same prerequisites as for building
[Tendermint](https://github.com/tendermint/tendermint).
## Building
From the `tools/tm-signer-harness` directory in your Tendermint source
repository, simply run:
```bash
make
# To have global access to this executable
make install
```
## Docker Image
To build a Docker image containing the `tm-signer-harness`, also from the
`tools/tm-signer-harness` directory of your Tendermint source repo, simply run:
```bash
make docker-image
```
## Running against KMS
As an example of how to use `tm-signer-harness`, the following instructions show
you how to execute its tests against [tkkms](https://github.com/iqlusioninc/tmkms).
For this example, we will make use of the **software signing module in KMS**, as
the hardware signing module requires a physical
[YubiHSM](https://www.yubico.com/products/yubihsm/) device.
### Step 1: Install KMS on your local machine
See the [tkkms repo](https://github.com/iqlusioninc/tmkms) for details on how to set
KMS up on your local machine.
If you have [Rust](https://www.rust-lang.org/) installed on your local machine,
you can simply install KMS by:
```bash
cargo install tmkms
```
### Step 2: Make keys for KMS
The KMS software signing module needs a key with which to sign messages. In our
example, we will simply export a signing key from our local Tendermint instance.
```bash
# Will generate all necessary Tendermint configuration files, including:
# - ~/.tendermint/config/priv_validator_key.json
# - ~/.tendermint/data/priv_validator_state.json
tendermint init validator
# Extract the signing key from our local Tendermint instance
tm-signer-harness extract_key \ # Use the "extract_key" command
-tmhome ~/.tendermint \ # Where to find the Tendermint home directory
-output ./signing.key # Where to write the key
```
Also, because we want KMS to connect to `tm-signer-harness`, we will need to
provide a secret connection key from KMS' side:
```bash
tmkms keygen secret_connection.key
```
### Step 3: Configure and run KMS
KMS needs some configuration to tell it to use the softer signing module as well
as the `signing.key` file we just generated. Save the following to a file called
`tmkms.toml`:
```toml
[[validator]]
addr = "tcp://127.0.0.1:61219" # This is where we will find tm-signer-harness.
chain_id = "test-chain-0XwP5E" # The Tendermint chain ID for which KMS will be signing (found in ~/.tendermint/config/genesis.json).
reconnect = true # true is the default
secret_key = "./secret_connection.key" # Where to find our secret connection key.
[[providers.softsign]]
id = "test-chain-0XwP5E" # The Tendermint chain ID for which KMS will be signing (same as validator.chain_id above).
path = "./signing.key" # The signing key we extracted earlier.
```
Then run KMS with this configuration:
```bash
tmkms start -c tmkms.toml
```
This will start KMS, which will repeatedly try to connect to
`tcp://127.0.0.1:61219` until it is successful.
### Step 4: Run tm-signer-harness
Now we get to run the signer test harness:
```bash
tm-signer-harness run \ # The "run" command executes the tests
-addr tcp://127.0.0.1:61219 \ # The address we promised KMS earlier
-tmhome ~/.tendermint # Where to find our Tendermint configuration/data files.
```
If the current version of Tendermint and KMS are compatible, `tm-signer-harness`
should now exit with a 0 exit code. If they are somehow not compatible, it
should exit with a meaningful non-zero exit code (see the exit codes below).
### Step 5: Shut down KMS
Simply hit Ctrl+Break on your KMS instance (or use the `kill` command in Linux)
to terminate it gracefully.
## Exit Code Meanings
The following list shows the various exit codes from `tm-signer-harness` and
their meanings:
| Exit Code | Description |
| --- | --- |
| 0 | Success! |
| 1 | Invalid command line parameters supplied to `tm-signer-harness` |
| 2 | Maximum number of accept retries reached (the `-accept-retries` parameter) |
| 3 | Failed to load `${TMHOME}/config/genesis.json` |
| 4 | Failed to create listener specified by `-addr` parameter |
| 5 | Failed to start listener |
| 6 | Interrupted by `SIGINT` (e.g. when hitting Ctrl+Break or Ctrl+C) |
| 7 | Other unknown error |
| 8 | Test 1 failed: public key mismatch |
| 9 | Test 2 failed: signing of proposals failed |
| 10 | Test 3 failed: signing of votes failed |

View File

@@ -214,7 +214,7 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
// Make proposal
propBlockID := types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()}
proposal := types.NewProposal(height, round, lazyNodeState.ValidRound, propBlockID)
proposal := types.NewProposal(height, round, lazyNodeState.ValidRound, propBlockID, block.Header.Time)
p := proposal.ToProto()
if err := lazyNodeState.privValidator.SignProposal(ctx, lazyNodeState.state.ChainID, p); err == nil {
proposal.Signature = p.Signature

View File

@@ -3,6 +3,7 @@ package consensus
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
@@ -247,7 +248,7 @@ func decideProposal(
// Make proposal
polRound, propBlockID := validRound, types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()}
proposal = types.NewProposal(height, round, polRound, propBlockID)
proposal = types.NewProposal(height, round, polRound, propBlockID, block.Header.Time)
p := proposal.ToProto()
if err := vs.SignProposal(ctx, chainID, p); err != nil {
t.Fatalf("error signing proposal: %s", err)
@@ -275,6 +276,7 @@ func signAddVotes(
addVotes(to, signVotes(ctx, voteType, chainID, blockID, vss...)...)
}
// nolint: lll
func validatePrevote(ctx context.Context, t *testing.T, cs *State, round int32, privVal *validatorStub, blockHash []byte) {
t.Helper()
prevotes := cs.Votes.Prevotes(round)
@@ -397,6 +399,35 @@ func subscribeToVoter(ctx context.Context, t *testing.T, cs *State, addr []byte)
return ch
}
func subscribeToVoterBuffered(ctx context.Context, t *testing.T, cs *State, addr []byte) <-chan tmpubsub.Message {
t.Helper()
votesSub, err := cs.eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: testSubscriber,
Query: types.EventQueryVote,
Limit: 10})
if err != nil {
t.Fatalf("failed to subscribe %s to %v", testSubscriber, types.EventQueryVote)
}
ch := make(chan tmpubsub.Message, 10)
go func() {
for {
msg, err := votesSub.Next(ctx)
if err != nil {
if !errors.Is(err, tmpubsub.ErrTerminated) && !errors.Is(err, context.Canceled) {
t.Errorf("error terminating pubsub %s", err)
}
return
}
vote := msg.Data().(types.EventDataVote)
// we only fire for our own votes
if bytes.Equal(addr, vote.Vote.ValidatorAddress) {
ch <- msg
}
}
}()
return ch
}
//-------------------------------------------------------------------------------
// consensus states
@@ -488,6 +519,7 @@ func loadPrivValidator(t *testing.T, config *config.Config) *privval.FilePV {
return privValidator
}
// nolint: lll
func makeState(ctx context.Context, cfg *config.Config, logger log.Logger, nValidators int) (*State, []*validatorStub, error) {
// Get State
state, privVals := makeGenesisState(cfg, genesisStateArgs{
@@ -512,7 +544,7 @@ func makeState(ctx context.Context, cfg *config.Config, logger log.Logger, nVali
//-------------------------------------------------------------------------------
func ensureNoNewEvent(t *testing.T, ch <-chan tmpubsub.Message, timeout time.Duration,
func ensureNoMessageBeforeTimeout(t *testing.T, ch <-chan tmpubsub.Message, timeout time.Duration,
errorMessage string) {
t.Helper()
select {
@@ -525,7 +557,7 @@ func ensureNoNewEvent(t *testing.T, ch <-chan tmpubsub.Message, timeout time.Dur
func ensureNoNewEventOnChannel(t *testing.T, ch <-chan tmpubsub.Message) {
t.Helper()
ensureNoNewEvent(
ensureNoMessageBeforeTimeout(
t,
ch,
ensureTimeout,
@@ -534,7 +566,7 @@ func ensureNoNewEventOnChannel(t *testing.T, ch <-chan tmpubsub.Message) {
func ensureNoNewRoundStep(t *testing.T, stepCh <-chan tmpubsub.Message) {
t.Helper()
ensureNoNewEvent(
ensureNoMessageBeforeTimeout(
t,
stepCh,
ensureTimeout,
@@ -544,7 +576,7 @@ func ensureNoNewRoundStep(t *testing.T, stepCh <-chan tmpubsub.Message) {
func ensureNoNewTimeout(t *testing.T, stepCh <-chan tmpubsub.Message, timeout int64) {
t.Helper()
timeoutDuration := time.Duration(timeout*10) * time.Nanosecond
ensureNoNewEvent(
ensureNoMessageBeforeTimeout(
t,
stepCh,
timeoutDuration,
@@ -553,40 +585,32 @@ func ensureNoNewTimeout(t *testing.T, stepCh <-chan tmpubsub.Message, timeout in
func ensureNewEvent(t *testing.T, ch <-chan tmpubsub.Message, height int64, round int32, timeout time.Duration, errorMessage string) { // nolint: lll
t.Helper()
select {
case <-time.After(timeout):
t.Fatalf("timed out waiting for new event: %s", errorMessage)
case msg := <-ch:
roundStateEvent, ok := msg.Data().(types.EventDataRoundState)
if !ok {
t.Fatalf("expected a EventDataRoundState, got %T. Wrong subscription channel?", msg.Data())
}
if roundStateEvent.Height != height {
t.Fatalf("expected height %v, got %v", height, roundStateEvent.Height)
}
if roundStateEvent.Round != round {
t.Fatalf("expected round %v, got %v", round, roundStateEvent.Round)
}
// TODO: We could check also for a step at this point!
msg := ensureMessageBeforeTimeout(t, ch, ensureTimeout)
roundStateEvent, ok := msg.Data().(types.EventDataRoundState)
if !ok {
t.Fatalf("expected a EventDataRoundState, got %T. Wrong subscription channel?", msg.Data())
}
if roundStateEvent.Height != height {
t.Fatalf("expected height %v, got %v", height, roundStateEvent.Height)
}
if roundStateEvent.Round != round {
t.Fatalf("expected round %v, got %v", round, roundStateEvent.Round)
}
// TODO: We could check also for a step at this point!
}
func ensureNewRound(t *testing.T, roundCh <-chan tmpubsub.Message, height int64, round int32) {
t.Helper()
select {
case <-time.After(ensureTimeout):
t.Fatal("Timeout expired while waiting for NewRound event")
case msg := <-roundCh:
newRoundEvent, ok := msg.Data().(types.EventDataNewRound)
if !ok {
t.Fatalf("expected a EventDataNewRound, got %T. Wrong subscription channel?", msg.Data())
}
if newRoundEvent.Height != height {
t.Fatalf("expected height %v, got %v", height, newRoundEvent.Height)
}
if newRoundEvent.Round != round {
t.Fatalf("expected round %v, got %v", round, newRoundEvent.Round)
}
msg := ensureMessageBeforeTimeout(t, roundCh, ensureTimeout)
newRoundEvent, ok := msg.Data().(types.EventDataNewRound)
if !ok {
t.Fatalf("expected a EventDataNewRound, got %T. Wrong subscription channel?", msg.Data())
}
if newRoundEvent.Height != height {
t.Fatalf("expected height %v, got %v", height, newRoundEvent.Height)
}
if newRoundEvent.Round != round {
t.Fatalf("expected round %v, got %v", round, newRoundEvent.Round)
}
}
@@ -599,21 +623,16 @@ func ensureNewTimeout(t *testing.T, timeoutCh <-chan tmpubsub.Message, height in
func ensureNewProposal(t *testing.T, proposalCh <-chan tmpubsub.Message, height int64, round int32) {
t.Helper()
select {
case <-time.After(ensureTimeout):
t.Fatalf("Timeout expired while waiting for NewProposal event")
case msg := <-proposalCh:
proposalEvent, ok := msg.Data().(types.EventDataCompleteProposal)
if !ok {
t.Fatalf("expected a EventDataCompleteProposal, got %T. Wrong subscription channel?",
msg.Data())
}
if proposalEvent.Height != height {
t.Fatalf("expected height %v, got %v", height, proposalEvent.Height)
}
if proposalEvent.Round != round {
t.Fatalf("expected round %v, got %v", round, proposalEvent.Round)
}
msg := ensureMessageBeforeTimeout(t, proposalCh, ensureTimeout)
proposalEvent, ok := msg.Data().(types.EventDataCompleteProposal)
if !ok {
t.Fatalf("expected a EventDataCompleteProposal, got %T. Wrong subscription channel?", msg.Data())
}
if proposalEvent.Height != height {
t.Fatalf("expected height %v, got %v", height, proposalEvent.Height)
}
if proposalEvent.Round != round {
t.Fatalf("expected round %v, got %v", round, proposalEvent.Round)
}
}
@@ -625,38 +644,28 @@ func ensureNewValidBlock(t *testing.T, validBlockCh <-chan tmpubsub.Message, hei
func ensureNewBlock(t *testing.T, blockCh <-chan tmpubsub.Message, height int64) {
t.Helper()
select {
case <-time.After(ensureTimeout):
t.Fatalf("Timeout expired while waiting for NewBlock event")
case msg := <-blockCh:
blockEvent, ok := msg.Data().(types.EventDataNewBlock)
if !ok {
t.Fatalf("expected a EventDataNewBlock, got %T. Wrong subscription channel?",
msg.Data())
}
if blockEvent.Block.Height != height {
t.Fatalf("expected height %v, got %v", height, blockEvent.Block.Height)
}
msg := ensureMessageBeforeTimeout(t, blockCh, ensureTimeout)
blockEvent, ok := msg.Data().(types.EventDataNewBlock)
if !ok {
t.Fatalf("expected a EventDataNewBlock, got %T. Wrong subscription channel?", msg.Data())
}
if blockEvent.Block.Height != height {
t.Fatalf("expected height %v, got %v", height, blockEvent.Block.Height)
}
}
func ensureNewBlockHeader(t *testing.T, blockCh <-chan tmpubsub.Message, height int64, blockHash tmbytes.HexBytes) {
t.Helper()
select {
case <-time.After(ensureTimeout):
t.Fatalf("Timeout expired while waiting for NewBlockHeader event")
case msg := <-blockCh:
blockHeaderEvent, ok := msg.Data().(types.EventDataNewBlockHeader)
if !ok {
t.Fatalf("expected a EventDataNewBlockHeader, got %T. Wrong subscription channel?",
msg.Data())
}
if blockHeaderEvent.Header.Height != height {
t.Fatalf("expected height %v, got %v", height, blockHeaderEvent.Header.Height)
}
if !bytes.Equal(blockHeaderEvent.Header.Hash(), blockHash) {
t.Fatalf("expected header %X, got %X", blockHash, blockHeaderEvent.Header.Hash())
}
msg := ensureMessageBeforeTimeout(t, blockCh, ensureTimeout)
blockHeaderEvent, ok := msg.Data().(types.EventDataNewBlockHeader)
if !ok {
t.Fatalf("expected a EventDataNewBlockHeader, got %T. Wrong subscription channel?", msg.Data())
}
if blockHeaderEvent.Header.Height != height {
t.Fatalf("expected height %v, got %v", height, blockHeaderEvent.Header.Height)
}
if !bytes.Equal(blockHeaderEvent.Header.Hash(), blockHash) {
t.Fatalf("expected header %X, got %X", blockHash, blockHeaderEvent.Header.Hash())
}
}
@@ -673,25 +682,26 @@ func ensureRelock(t *testing.T, relockCh <-chan tmpubsub.Message, height int64,
}
func ensureProposal(t *testing.T, proposalCh <-chan tmpubsub.Message, height int64, round int32, propID types.BlockID) {
ensureProposalWithTimeout(t, proposalCh, height, round, &propID, ensureTimeout)
}
// nolint: lll
func ensureProposalWithTimeout(t *testing.T, proposalCh <-chan tmpubsub.Message, height int64, round int32, propID *types.BlockID, timeout time.Duration) {
t.Helper()
select {
case <-time.After(ensureTimeout):
t.Fatalf("Timeout expired while waiting for NewProposal event")
case msg := <-proposalCh:
proposalEvent, ok := msg.Data().(types.EventDataCompleteProposal)
if !ok {
t.Fatalf("expected a EventDataCompleteProposal, got %T. Wrong subscription channel?",
msg.Data())
}
if proposalEvent.Height != height {
t.Fatalf("expected height %v, got %v", height, proposalEvent.Height)
}
if proposalEvent.Round != round {
t.Fatalf("expected round %v, got %v", round, proposalEvent.Round)
}
if !proposalEvent.BlockID.Equals(propID) {
t.Fatalf("Proposed block does not match expected block (%v != %v)", proposalEvent.BlockID, propID)
}
msg := ensureMessageBeforeTimeout(t, proposalCh, timeout)
proposalEvent, ok := msg.Data().(types.EventDataCompleteProposal)
if !ok {
t.Fatalf("expected a EventDataCompleteProposal, got %T. Wrong subscription channel?",
msg.Data())
}
if proposalEvent.Height != height {
t.Fatalf("expected height %v, got %v", height, proposalEvent.Height)
}
if proposalEvent.Round != round {
t.Fatalf("expected round %v, got %v", round, proposalEvent.Round)
}
if propID != nil && !proposalEvent.BlockID.Equals(*propID) {
t.Fatalf("Proposed block does not match expected block (%v != %v)", proposalEvent.BlockID, *propID)
}
}
@@ -708,44 +718,37 @@ func ensurePrevote(t *testing.T, voteCh <-chan tmpubsub.Message, height int64, r
func ensureVote(t *testing.T, voteCh <-chan tmpubsub.Message, height int64, round int32,
voteType tmproto.SignedMsgType) {
t.Helper()
select {
case <-time.After(ensureTimeout):
t.Fatalf("Timeout expired while waiting for NewVote event")
case msg := <-voteCh:
voteEvent, ok := msg.Data().(types.EventDataVote)
if !ok {
t.Fatalf("expected a EventDataVote, got %T. Wrong subscription channel?",
msg.Data())
}
vote := voteEvent.Vote
if vote.Height != height {
t.Fatalf("expected height %v, got %v", height, vote.Height)
}
if vote.Round != round {
t.Fatalf("expected round %v, got %v", round, vote.Round)
}
if vote.Type != voteType {
t.Fatalf("expected type %v, got %v", voteType, vote.Type)
}
msg := ensureMessageBeforeTimeout(t, voteCh, ensureTimeout)
voteEvent, ok := msg.Data().(types.EventDataVote)
if !ok {
t.Fatalf("expected a EventDataVote, got %T. Wrong subscription channel?", msg.Data())
}
}
func ensurePrecommitTimeout(t *testing.T, ch <-chan tmpubsub.Message) {
t.Helper()
select {
case <-time.After(ensureTimeout):
t.Fatalf("Timeout expired while waiting for the Precommit to Timeout")
case <-ch:
vote := voteEvent.Vote
if vote.Height != height {
t.Fatalf("expected height %v, got %v", height, vote.Height)
}
if vote.Round != round {
t.Fatalf("expected round %v, got %v", round, vote.Round)
}
if vote.Type != voteType {
t.Fatalf("expected type %v, got %v", voteType, vote.Type)
}
}
func ensureNewEventOnChannel(t *testing.T, ch <-chan tmpubsub.Message) {
t.Helper()
ensureMessageBeforeTimeout(t, ch, ensureTimeout)
}
func ensureMessageBeforeTimeout(t *testing.T, ch <-chan tmpubsub.Message, to time.Duration) tmpubsub.Message {
t.Helper()
select {
case <-time.After(ensureTimeout):
t.Fatalf("Timeout expired while waiting for new activity on the channel")
case <-ch:
case <-time.After(to):
t.Fatalf("Timeout expired while waiting for message")
case msg := <-ch:
return msg
}
panic("unreachable")
}
//-------------------------------------------------------------------------------

View File

@@ -1,115 +1,496 @@
package consensus
import (
"bytes"
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/abci/example/kvstore"
"github.com/tendermint/tendermint/internal/eventbus"
"github.com/tendermint/tendermint/libs/log"
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
tmtimemocks "github.com/tendermint/tendermint/libs/time/mocks"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/types"
)
const (
// blockTimeIota is used in the test harness as the time between
// blocks when not otherwise specified.
blockTimeIota = time.Millisecond
)
// pbtsTestHarness constructs a Tendermint network that can be used for testing the
// implementation of the Proposer-Based timestamps algorithm.
// It runs a series of consensus heights and captures timing of votes and events.
type pbtsTestHarness struct {
// configuration options set by the user of the test harness.
pbtsTestConfiguration
// The Tendermint consensus state machine being run during
// a run of the pbtsTestHarness.
observedState *State
// A stub for signing votes and messages using the key
// from the observedState.
observedValidator *validatorStub
// A list of simulated validators that interact with the observedState and are
// fully controlled by the test harness.
otherValidators []*validatorStub
// The mock time source used by all of the validator stubs in the test harness.
// This mock clock allows the test harness to produce votes and blocks with arbitrary
// timestamps.
validatorClock *tmtimemocks.Source
chainID string
// channels for verifying that the observed validator completes certain actions.
ensureProposalCh, roundCh, blockCh, ensureVoteCh <-chan tmpubsub.Message
// channel of events from the observed validator annotated with the timestamp
// the event was received.
eventCh <-chan timestampedEvent
currentHeight int64
currentRound int32
t *testing.T
ctx context.Context
}
type pbtsTestConfiguration struct {
// The timestamp consensus parameters to be used by the state machine under test.
timingParams types.TimingParams
// The setting to use for the TimeoutPropose configuration parameter.
timeoutPropose time.Duration
// The timestamp of the first block produced by the network.
genesisTime time.Time
// The time at which the proposal at height 2 should be delivered.
height2ProposalDeliverTime time.Time
// The timestamp of the block proposed at height 2.
height2ProposedBlockTime time.Time
// The timestamp of the block proposed at height 4.
// At height 4, the proposed block time and the deliver time are the same so
// that timely-ness does not affect height 4.
height4ProposedBlockTime time.Time
}
func newPBTSTestHarness(ctx context.Context, t *testing.T, tc pbtsTestConfiguration) pbtsTestHarness {
t.Helper()
const validators = 4
cfg := configSetup(t)
clock := new(tmtimemocks.Source)
if tc.height4ProposedBlockTime.IsZero() {
// Set a default height4ProposedBlockTime.
// Use a proposed block time that is greater than the time that the
// block at height 2 was delivered. Height 3 is not relevant for testing
// and always occurs blockTimeIota before height 4. If not otherwise specified,
// height 4 therefore occurs 2*blockTimeIota after height 2.
tc.height4ProposedBlockTime = tc.height2ProposalDeliverTime.Add(2 * blockTimeIota)
}
cfg.Consensus.TimeoutPropose = tc.timeoutPropose
consensusParams := types.DefaultConsensusParams()
consensusParams.Timing = tc.timingParams
state, privVals := makeGenesisState(cfg, genesisStateArgs{
Params: consensusParams,
Time: tc.genesisTime,
Validators: validators,
})
cs, err := newState(ctx, log.TestingLogger(), state, privVals[0], kvstore.NewApplication())
require.NoError(t, err)
vss := make([]*validatorStub, validators)
for i := 0; i < validators; i++ {
vss[i] = newValidatorStub(privVals[i], int32(i))
}
incrementHeight(vss[1:]...)
for _, vs := range vss {
vs.clock = clock
}
pubKey, err := vss[0].PrivValidator.GetPubKey(ctx)
require.NoError(t, err)
eventCh := timestampedCollector(ctx, t, cs.eventBus)
return pbtsTestHarness{
pbtsTestConfiguration: tc,
observedValidator: vss[0],
observedState: cs,
otherValidators: vss[1:],
validatorClock: clock,
currentHeight: 1,
chainID: cfg.ChainID(),
roundCh: subscribe(ctx, t, cs.eventBus, types.EventQueryNewRound),
ensureProposalCh: subscribe(ctx, t, cs.eventBus, types.EventQueryCompleteProposal),
blockCh: subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock),
ensureVoteCh: subscribeToVoterBuffered(ctx, t, cs, pubKey.Address()),
eventCh: eventCh,
t: t,
ctx: ctx,
}
}
func (p *pbtsTestHarness) observedValidatorProposerHeight(previousBlockTime time.Time) heightResult {
p.validatorClock.On("Now").Return(p.height2ProposedBlockTime).Times(6)
ensureNewRound(p.t, p.roundCh, p.currentHeight, p.currentRound)
timeout := time.Until(previousBlockTime.Add(ensureTimeout))
ensureProposalWithTimeout(p.t, p.ensureProposalCh, p.currentHeight, p.currentRound, nil, timeout)
rs := p.observedState.GetRoundState()
bid := types.BlockID{Hash: rs.ProposalBlock.Hash(), PartSetHeader: rs.ProposalBlockParts.Header()}
ensurePrevote(p.t, p.ensureVoteCh, p.currentHeight, p.currentRound)
signAddVotes(p.ctx, p.observedState, tmproto.PrevoteType, p.chainID, bid, p.otherValidators...)
signAddVotes(p.ctx, p.observedState, tmproto.PrecommitType, p.chainID, bid, p.otherValidators...)
ensurePrecommit(p.t, p.ensureVoteCh, p.currentHeight, p.currentRound)
ensureNewBlock(p.t, p.blockCh, p.currentHeight)
vk, err := p.observedValidator.GetPubKey(context.Background())
require.NoError(p.t, err)
res := collectHeightResults(p.ctx, p.t, p.eventCh, p.currentHeight, vk.Address())
p.currentHeight++
incrementHeight(p.otherValidators...)
return res
}
func (p *pbtsTestHarness) height2() heightResult {
signer := p.otherValidators[0].PrivValidator
height3BlockTime := p.height2ProposedBlockTime.Add(-blockTimeIota)
return p.nextHeight(signer, p.height2ProposalDeliverTime, p.height2ProposedBlockTime, height3BlockTime)
}
func (p *pbtsTestHarness) intermediateHeights() {
signer := p.otherValidators[1].PrivValidator
blockTimeHeight3 := p.height4ProposedBlockTime.Add(-blockTimeIota)
p.nextHeight(signer, blockTimeHeight3, blockTimeHeight3, p.height4ProposedBlockTime)
signer = p.otherValidators[2].PrivValidator
p.nextHeight(signer, p.height4ProposedBlockTime, p.height4ProposedBlockTime, time.Now())
}
func (p *pbtsTestHarness) height5() heightResult {
return p.observedValidatorProposerHeight(p.height4ProposedBlockTime)
}
// nolint: lll
func (p *pbtsTestHarness) nextHeight(proposer types.PrivValidator, deliverTime, proposedTime, nextProposedTime time.Time) heightResult {
p.validatorClock.On("Now").Return(nextProposedTime).Times(6)
ensureNewRound(p.t, p.roundCh, p.currentHeight, p.currentRound)
b, _ := p.observedState.createProposalBlock()
b.Height = p.currentHeight
b.Header.Height = p.currentHeight
b.Header.Time = proposedTime
k, err := proposer.GetPubKey(context.Background())
require.NoError(p.t, err)
b.Header.ProposerAddress = k.Address()
ps := b.MakePartSet(types.BlockPartSizeBytes)
bid := types.BlockID{Hash: b.Hash(), PartSetHeader: ps.Header()}
prop := types.NewProposal(p.currentHeight, 0, -1, bid, proposedTime)
tp := prop.ToProto()
if err := proposer.SignProposal(context.Background(), p.observedState.state.ChainID, tp); err != nil {
p.t.Fatalf("error signing proposal: %s", err)
}
time.Sleep(time.Until(deliverTime))
prop.Signature = tp.Signature
if err := p.observedState.SetProposalAndBlock(prop, b, ps, "peerID"); err != nil {
p.t.Fatal(err)
}
ensureProposal(p.t, p.ensureProposalCh, p.currentHeight, 0, bid)
ensurePrevote(p.t, p.ensureVoteCh, p.currentHeight, p.currentRound)
signAddVotes(p.ctx, p.observedState, tmproto.PrevoteType, p.chainID, bid, p.otherValidators...)
signAddVotes(p.ctx, p.observedState, tmproto.PrecommitType, p.chainID, bid, p.otherValidators...)
ensurePrecommit(p.t, p.ensureVoteCh, p.currentHeight, p.currentRound)
vk, err := p.observedValidator.GetPubKey(context.Background())
require.NoError(p.t, err)
res := collectHeightResults(p.ctx, p.t, p.eventCh, p.currentHeight, vk.Address())
ensureNewBlock(p.t, p.blockCh, p.currentHeight)
p.currentHeight++
incrementHeight(p.otherValidators...)
return res
}
func timestampedCollector(ctx context.Context, t *testing.T, eb *eventbus.EventBus) <-chan timestampedEvent {
t.Helper()
// Since eventCh is not read until the end of each height, it must be large
// enough to hold all of the events produced during a single height.
eventCh := make(chan timestampedEvent, 100)
if err := eb.Observe(ctx, func(msg tmpubsub.Message) error {
eventCh <- timestampedEvent{
ts: time.Now(),
m: msg,
}
return nil
}, types.EventQueryVote, types.EventQueryCompleteProposal); err != nil {
t.Fatalf("Failed to observe query %v: %v", types.EventQueryVote, err)
}
return eventCh
}
// nolint: lll
func collectHeightResults(ctx context.Context, t *testing.T, eventCh <-chan timestampedEvent, height int64, address []byte) heightResult {
t.Helper()
var res heightResult
for event := range eventCh {
switch v := event.m.Data().(type) {
case types.EventDataVote:
if v.Vote.Height > height {
t.Fatalf("received prevote from unexpected height, expected: %d, saw: %d", height, v.Vote.Height)
}
if !bytes.Equal(address, v.Vote.ValidatorAddress) {
continue
}
if v.Vote.Type != tmproto.PrevoteType {
continue
}
res.prevote = v.Vote
res.prevoteIssuedAt = event.ts
case types.EventDataCompleteProposal:
if v.Height > height {
t.Fatalf("received proposal from unexpected height, expected: %d, saw: %d", height, v.Height)
}
res.proposalIssuedAt = event.ts
}
if res.isComplete() {
return res
}
}
t.Fatalf("complete height result never seen for height %d", height)
panic("unreachable")
}
type timestampedEvent struct {
ts time.Time
m tmpubsub.Message
}
func (p *pbtsTestHarness) run() resultSet {
startTestRound(p.ctx, p.observedState, p.currentHeight, p.currentRound)
r1 := p.observedValidatorProposerHeight(p.genesisTime)
r2 := p.height2()
p.intermediateHeights()
r5 := p.height5()
return resultSet{
genesisHeight: r1,
height2: r2,
height5: r5,
}
}
type resultSet struct {
genesisHeight heightResult
height2 heightResult
height5 heightResult
}
type heightResult struct {
proposalIssuedAt time.Time
prevote *types.Vote
prevoteIssuedAt time.Time
}
func (hr heightResult) isComplete() bool {
return !hr.proposalIssuedAt.IsZero() && !hr.prevoteIssuedAt.IsZero() && hr.prevote != nil
}
// TestProposerWaitsForGenesisTime tests that a proposer will not propose a block
// until after the genesis time has passed. The test sets the genesis time in the
// future and then ensures that the observed validator waits to propose a block.
func TestProposerWaitsForGenesisTime(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// create a genesis time far (enough) in the future.
initialTime := time.Now().Add(800 * time.Millisecond)
cfg := pbtsTestConfiguration{
timingParams: types.TimingParams{
Precision: 10 * time.Millisecond,
MessageDelay: 10 * time.Millisecond,
},
timeoutPropose: 10 * time.Millisecond,
genesisTime: initialTime,
height2ProposalDeliverTime: initialTime.Add(10 * time.Millisecond),
height2ProposedBlockTime: initialTime.Add(10 * time.Millisecond),
}
pbtsTest := newPBTSTestHarness(ctx, t, cfg)
results := pbtsTest.run()
// ensure that the proposal was issued after the genesis time.
assert.True(t, results.genesisHeight.proposalIssuedAt.After(cfg.genesisTime))
}
// TestProposerWaitsForPreviousBlock tests that the proposer of a block waits until
// the block time of the previous height has passed to propose the next block.
// The test harness ensures that the observed validator will be the proposer at
// height 1 and height 5. The test sets the block time of height 4 in the future
// and then verifies that the observed validator waits until after the block time
// of height 4 to propose a block at height 5.
func TestProposerWaitsForPreviousBlock(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
initialTime := time.Now().Add(time.Millisecond * 50)
cfg := pbtsTestConfiguration{
timingParams: types.TimingParams{
Precision: 100 * time.Millisecond,
MessageDelay: 500 * time.Millisecond,
},
timeoutPropose: 50 * time.Millisecond,
genesisTime: initialTime,
height2ProposalDeliverTime: initialTime.Add(150 * time.Millisecond),
height2ProposedBlockTime: initialTime.Add(100 * time.Millisecond),
height4ProposedBlockTime: initialTime.Add(800 * time.Millisecond),
}
pbtsTest := newPBTSTestHarness(ctx, t, cfg)
results := pbtsTest.run()
// the observed validator is the proposer at height 5.
// ensure that the observed validator did not propose a block until after
// the time configured for height 4.
assert.True(t, results.height5.proposalIssuedAt.After(cfg.height4ProposedBlockTime))
// Ensure that the validator issued a prevote for a non-nil block.
assert.NotNil(t, results.height5.prevote.BlockID.Hash)
}
func TestProposerWaitTime(t *testing.T) {
genesisTime, err := time.Parse(time.RFC3339, "2019-03-13T23:00:00Z")
require.NoError(t, err)
testCases := []struct {
name string
blockTime time.Time
localTime time.Time
expectedWait time.Duration
name string
previousBlockTime time.Time
localTime time.Time
expectedWait time.Duration
}{
{
name: "block time greater than local time",
blockTime: genesisTime.Add(5 * time.Nanosecond),
localTime: genesisTime.Add(1 * time.Nanosecond),
expectedWait: 4 * time.Nanosecond,
name: "block time greater than local time",
previousBlockTime: genesisTime.Add(5 * time.Nanosecond),
localTime: genesisTime.Add(1 * time.Nanosecond),
expectedWait: 4 * time.Nanosecond,
},
{
name: "local time greater than block time",
blockTime: genesisTime.Add(1 * time.Nanosecond),
localTime: genesisTime.Add(5 * time.Nanosecond),
expectedWait: 0,
name: "local time greater than block time",
previousBlockTime: genesisTime.Add(1 * time.Nanosecond),
localTime: genesisTime.Add(5 * time.Nanosecond),
expectedWait: 0,
},
{
name: "both times equal",
blockTime: genesisTime.Add(5 * time.Nanosecond),
localTime: genesisTime.Add(5 * time.Nanosecond),
expectedWait: 0,
name: "both times equal",
previousBlockTime: genesisTime.Add(5 * time.Nanosecond),
localTime: genesisTime.Add(5 * time.Nanosecond),
expectedWait: 0,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
b := types.Block{
Header: types.Header{
Time: testCase.blockTime,
},
}
mockSource := new(tmtimemocks.Source)
mockSource.On("Now").Return(testCase.localTime)
ti := proposerWaitTime(mockSource, b.Header)
ti := proposerWaitTime(mockSource, testCase.previousBlockTime)
assert.Equal(t, testCase.expectedWait, ti)
})
}
}
func TestProposalTimeout(t *testing.T) {
genesisTime, err := time.Parse(time.RFC3339, "2019-03-13T23:00:00Z")
require.NoError(t, err)
testCases := []struct {
name string
localTime time.Time
previousBlockTime time.Time
precision time.Duration
msgDelay time.Duration
expectedDuration time.Duration
}{
{
name: "MsgDelay + Precision has not quite elapsed",
localTime: genesisTime.Add(525 * time.Millisecond),
previousBlockTime: genesisTime.Add(6 * time.Millisecond),
precision: time.Millisecond * 20,
msgDelay: time.Millisecond * 500,
expectedDuration: 1 * time.Millisecond,
},
{
name: "MsgDelay + Precision equals current time",
localTime: genesisTime.Add(525 * time.Millisecond),
previousBlockTime: genesisTime.Add(5 * time.Millisecond),
precision: time.Millisecond * 20,
msgDelay: time.Millisecond * 500,
expectedDuration: 0,
},
{
name: "MsgDelay + Precision has elapsed",
localTime: genesisTime.Add(725 * time.Millisecond),
previousBlockTime: genesisTime.Add(5 * time.Millisecond),
precision: time.Millisecond * 20,
msgDelay: time.Millisecond * 500,
expectedDuration: 0,
func TestTimelyProposal(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
initialTime := time.Now()
cfg := pbtsTestConfiguration{
timingParams: types.TimingParams{
Precision: 10 * time.Millisecond,
MessageDelay: 140 * time.Millisecond,
},
timeoutPropose: 40 * time.Millisecond,
genesisTime: initialTime,
height2ProposedBlockTime: initialTime.Add(10 * time.Millisecond),
height2ProposalDeliverTime: initialTime.Add(30 * time.Millisecond),
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
b := types.Block{
Header: types.Header{
Time: testCase.previousBlockTime,
},
}
mockSource := new(tmtimemocks.Source)
mockSource.On("Now").Return(testCase.localTime)
tp := types.TimestampParams{
Precision: testCase.precision,
MsgDelay: testCase.msgDelay,
}
ti := proposalStepWaitingTime(mockSource, b.Header, tp)
assert.Equal(t, testCase.expectedDuration, ti)
})
}
pbtsTest := newPBTSTestHarness(ctx, t, cfg)
results := pbtsTest.run()
assert.True(t, results.height2.prevote.BlockID.Hash != nil)
}
func TestTooFarInThePastProposal(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
initialTime := time.Now()
// localtime > proposedBlockTime + MsgDelay + Precision
cfg := pbtsTestConfiguration{
timingParams: types.TimingParams{
Precision: 1 * time.Millisecond,
MessageDelay: 10 * time.Millisecond,
},
timeoutPropose: 50 * time.Millisecond,
genesisTime: initialTime,
height2ProposedBlockTime: initialTime.Add(10 * time.Millisecond),
height2ProposalDeliverTime: initialTime.Add(21 * time.Millisecond),
}
pbtsTest := newPBTSTestHarness(ctx, t, cfg)
results := pbtsTest.run()
time.Sleep(1 * time.Second)
assert.True(t, results.height2.prevote.BlockID.Hash == nil)
}
func TestTooFarInTheFutureProposal(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
initialTime := time.Now()
// localtime < proposedBlockTime - Precision
cfg := pbtsTestConfiguration{
timingParams: types.TimingParams{
Precision: 1 * time.Millisecond,
MessageDelay: 10 * time.Millisecond,
},
timeoutPropose: 50 * time.Millisecond,
genesisTime: initialTime,
height2ProposedBlockTime: initialTime.Add(100 * time.Millisecond),
height2ProposalDeliverTime: initialTime.Add(10 * time.Millisecond),
height4ProposedBlockTime: initialTime.Add(150 * time.Millisecond),
}
pbtsTest := newPBTSTestHarness(ctx, t, cfg)
results := pbtsTest.run()
assert.True(t, results.height2.prevote.BlockID.Hash == nil)
}

View File

@@ -384,7 +384,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
propBlockParts := propBlock.MakePartSet(partSize)
blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()}
proposal := types.NewProposal(vss[1].Height, round, -1, blockID)
proposal := types.NewProposal(vss[1].Height, round, -1, blockID, propBlock.Header.Time)
p := proposal.ToProto()
if err := vss[1].SignProposal(ctx, cfg.ChainID(), p); err != nil {
t.Fatal("failed to sign bad proposal", err)
@@ -416,7 +416,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
propBlockParts = propBlock.MakePartSet(partSize)
blockID = types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()}
proposal = types.NewProposal(vss[2].Height, round, -1, blockID)
proposal = types.NewProposal(vss[2].Height, round, -1, blockID, propBlock.Header.Time)
p = proposal.ToProto()
if err := vss[2].SignProposal(ctx, cfg.ChainID(), p); err != nil {
t.Fatal("failed to sign bad proposal", err)
@@ -475,7 +475,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
selfIndex := valIndexFn(0)
proposal = types.NewProposal(vss[3].Height, round, -1, blockID)
proposal = types.NewProposal(vss[3].Height, round, -1, blockID, propBlock.Header.Time)
p = proposal.ToProto()
if err := vss[3].SignProposal(ctx, cfg.ChainID(), p); err != nil {
t.Fatal("failed to sign bad proposal", err)
@@ -540,7 +540,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
sort.Sort(ValidatorStubsByPower(newVss))
selfIndex = valIndexFn(0)
proposal = types.NewProposal(vss[1].Height, round, -1, blockID)
proposal = types.NewProposal(vss[1].Height, round, -1, blockID, propBlock.Header.Time)
p = proposal.ToProto()
if err := vss[1].SignProposal(ctx, cfg.ChainID(), p); err != nil {
t.Fatal("failed to sign bad proposal", err)
@@ -816,7 +816,8 @@ func testHandshakeReplay(
}
// now start the app using the handshake - it should sync
genDoc, _ := sm.MakeGenesisDocFromFile(cfg.GenesisFile())
genDoc, err := sm.MakeGenesisDocFromFile(cfg.GenesisFile())
require.NoError(t, err)
handshaker := NewHandshaker(logger, stateStore, state, store, eventbus.NopEventBus{}, genDoc)
proxyApp := proxy.NewAppConns(clientCreator2, logger, proxy.NopMetrics())
if err := proxyApp.Start(ctx); err != nil {
@@ -825,7 +826,7 @@ func testHandshakeReplay(
t.Cleanup(func() { cancel(); proxyApp.Wait() })
err := handshaker.Handshake(ctx, proxyApp)
err = handshaker.Handshake(ctx, proxyApp)
if expectError {
require.Error(t, err)
return
@@ -1004,7 +1005,8 @@ func TestHandshakePanicsIfAppReturnsWrongAppHash(t *testing.T) {
require.NoError(t, err)
stateDB, state, store := stateAndStore(cfg, pubKey, appVersion)
stateStore := sm.NewStore(stateDB)
genDoc, _ := sm.MakeGenesisDocFromFile(cfg.GenesisFile())
genDoc, err := sm.MakeGenesisDocFromFile(cfg.GenesisFile())
require.NoError(t, err)
state.LastValidators = state.Validators.Copy()
// mode = 0 for committing all the blocks
blocks := sf.MakeBlocks(3, &state, privVal)

View File

@@ -713,6 +713,7 @@ func (cs *State) updateToState(state sm.State) {
cs.Validators = validators
cs.Proposal = nil
cs.ProposalReceiveTime = time.Time{}
cs.ProposalBlock = nil
cs.ProposalBlockParts = nil
cs.LockedRound = -1
@@ -1047,6 +1048,7 @@ func (cs *State) enterNewRound(height int64, round int32) {
} else {
logger.Debug("resetting proposal info")
cs.Proposal = nil
cs.ProposalReceiveTime = time.Time{}
cs.ProposalBlock = nil
cs.ProposalBlockParts = nil
}
@@ -1069,9 +1071,10 @@ func (cs *State) enterNewRound(height int64, round int32) {
cs.scheduleTimeout(cs.config.CreateEmptyBlocksInterval, height, round,
cstypes.RoundStepNewRound)
}
} else {
cs.enterPropose(height, round)
return
}
cs.enterPropose(height, round)
}
// needProofBlock returns true on the first height (so the genesis app hash is signed right away)
@@ -1104,6 +1107,16 @@ func (cs *State) enterPropose(height int64, round int32) {
return
}
// If this validator is the proposer of this round, and the previous block time is later than
// our local clock time, wait to propose until our local clock time has passed the block time.
if cs.privValidatorPubKey != nil && cs.isProposer(cs.privValidatorPubKey.Address()) {
proposerWaitTime := proposerWaitTime(tmtime.DefaultSource{}, cs.state.LastBlockTime)
if proposerWaitTime > 0 {
cs.scheduleTimeout(proposerWaitTime, height, round, cstypes.RoundStepNewRound)
return
}
}
logger.Debug("entering propose step", "current", fmt.Sprintf("%v/%v/%v", cs.Height, cs.Round, cs.Step))
defer func() {
@@ -1128,8 +1141,6 @@ func (cs *State) enterPropose(height int64, round int32) {
return
}
logger.Debug("node is a validator")
if cs.privValidatorPubKey == nil {
// If this node is a validator & proposer in the current round, it will
// miss the opportunity to create a block.
@@ -1137,18 +1148,20 @@ func (cs *State) enterPropose(height int64, round int32) {
return
}
address := cs.privValidatorPubKey.Address()
addr := cs.privValidatorPubKey.Address()
// if not a validator, we're done
if !cs.Validators.HasAddress(address) {
logger.Debug("node is not a validator", "addr", address, "vals", cs.Validators)
if !cs.Validators.HasAddress(addr) {
logger.Debug("node is not a validator", "addr", addr, "vals", cs.Validators)
return
}
if cs.isProposer(address) {
logger.Debug("node is a validator")
if cs.isProposer(addr) {
logger.Debug(
"propose step; our turn to propose",
"proposer", address,
"proposer", addr,
)
cs.decideProposal(height, round)
@@ -1188,7 +1201,7 @@ func (cs *State) defaultDecideProposal(height int64, round int32) {
// Make proposal
propBlockID := types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()}
proposal := types.NewProposal(height, round, cs.ValidRound, propBlockID)
proposal := types.NewProposal(height, round, cs.ValidRound, propBlockID, block.Header.Time)
p := proposal.ToProto()
// wait the max amount we would wait for a proposal
@@ -1300,12 +1313,47 @@ func (cs *State) enterPrevote(height int64, round int32) {
// (so we have more time to try and collect +2/3 prevotes for a single block)
}
func (cs *State) proposalIsTimely() bool {
tp := types.TimingParams{
Precision: cs.state.ConsensusParams.Timing.Precision,
MessageDelay: cs.state.ConsensusParams.Timing.MessageDelay,
}
return cs.Proposal.IsTimely(cs.ProposalReceiveTime, tp, cs.state.InitialHeight)
}
func (cs *State) defaultDoPrevote(height int64, round int32) {
logger := cs.Logger.With("height", height, "round", round)
// We did not receive a proposal within this round. (and thus executing this from a timeout)
// Check that a proposed block was not received within this round (and thus executing this from a timeout).
if cs.ProposalBlock == nil {
logger.Debug("prevote step: ProposalBlock is nil")
logger.Debug("prevote step: ProposalBlock is nil; prevoting nil")
cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
return
}
if cs.Proposal == nil {
logger.Debug("prevote step: did not receive proposal; prevoting nil")
cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
return
}
if !cs.Proposal.Timestamp.Equal(cs.ProposalBlock.Header.Time) {
logger.Debug("prevote step: proposal timestamp not equal; prevoting nil")
cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
return
}
if cs.Proposal.POLRound == -1 && cs.LockedRound == -1 && !cs.proposalIsTimely() {
logger.Debug("prevote step: Proposal is not timely; prevoting nil - ",
"proposed",
tmtime.Canonical(cs.Proposal.Timestamp).Format(time.RFC3339Nano),
"received",
tmtime.Canonical(cs.ProposalReceiveTime).Format(time.RFC3339Nano),
"msg_delay",
cs.state.ConsensusParams.Timing.MessageDelay,
"precision",
cs.state.ConsensusParams.Timing.Precision)
cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
return
}
@@ -1314,7 +1362,7 @@ func (cs *State) defaultDoPrevote(height int64, round int32) {
err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock)
if err != nil {
// ProposalBlock is invalid, prevote nil.
logger.Error("prevote step: ProposalBlock is invalid", "err", err)
logger.Error("prevote step: ProposalBlock is invalid; prevoting nil", "err", err)
cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
return
}
@@ -1465,20 +1513,33 @@ func (cs *State) enterPrecommit(height int64, round int32) {
// +2/3 prevoted nil. Precommit nil.
if blockID.IsNil() {
logger.Debug("precommit step; +2/3 prevoted for nil")
logger.Debug("precommit step: +2/3 prevoted for nil; precommitting nil")
cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{})
return
}
// At this point, +2/3 prevoted for a particular block.
// If we never received a proposal for this block, we must precommit nil
if cs.Proposal == nil || cs.ProposalBlock == nil {
logger.Debug("precommit step; did not receive proposal, precommitting nil")
cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{})
return
}
// At this point, +2/3 prevoted for a particular block.
// If the proposal time does not match the block time, precommit nil.
if !cs.Proposal.Timestamp.Equal(cs.ProposalBlock.Header.Time) {
logger.Debug("precommit step: proposal timestamp not equal; precommitting nil")
cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{})
return
}
// If we're already locked on that block, precommit it, and update the LockedRound
if cs.LockedBlock.HashesTo(blockID.Hash) {
logger.Debug("precommit step; +2/3 prevoted locked block; relocking")
logger.Debug("precommit step: +2/3 prevoted locked block; relocking")
cs.LockedRound = round
if err := cs.eventBus.PublishEventRelock(cs.RoundStateEvent()); err != nil {
logger.Error("failed publishing event relock", "err", err)
logger.Error("precommit step: failed publishing event relock", "err", err)
}
cs.signAddVote(tmproto.PrecommitType, blockID.Hash, blockID.PartSetHeader)
@@ -1489,11 +1550,11 @@ func (cs *State) enterPrecommit(height int64, round int32) {
// the proposed block, update our locked block to this block and issue a
// precommit vote for it.
if cs.ProposalBlock.HashesTo(blockID.Hash) {
logger.Debug("precommit step; +2/3 prevoted proposal block; locking", "hash", blockID.Hash)
logger.Debug("precommit step: +2/3 prevoted proposal block; locking", "hash", blockID.Hash)
// Validate the block.
if err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock); err != nil {
panic(fmt.Sprintf("precommit step; +2/3 prevoted for an invalid block: %v", err))
panic(fmt.Sprintf("precommit step: +2/3 prevoted for an invalid block %v; relocking", err))
}
cs.LockedRound = round
@@ -1501,7 +1562,7 @@ func (cs *State) enterPrecommit(height int64, round int32) {
cs.LockedBlockParts = cs.ProposalBlockParts
if err := cs.eventBus.PublishEventLock(cs.RoundStateEvent()); err != nil {
logger.Error("failed publishing event lock", "err", err)
logger.Error("precommit step: failed publishing event lock", "err", err)
}
cs.signAddVote(tmproto.PrecommitType, blockID.Hash, blockID.PartSetHeader)
@@ -1510,7 +1571,7 @@ func (cs *State) enterPrecommit(height int64, round int32) {
// There was a polka in this round for a block we don't have.
// Fetch that block, and precommit nil.
logger.Debug("precommit step; +2/3 prevotes for a block we do not have; voting nil", "block_id", blockID)
logger.Debug("precommit step: +2/3 prevotes for a block we do not have; voting nil", "block_id", blockID)
if !cs.ProposalBlockParts.HasHeader(blockID.PartSetHeader) {
cs.ProposalBlock = nil
@@ -1855,9 +1916,11 @@ func (cs *State) RecordMetrics(height int64, block *types.Block) {
//-----------------------------------------------------------------------------
func (cs *State) defaultSetProposal(proposal *types.Proposal) error {
recvTime := tmtime.Now()
// Already have one
// TODO: possibly catch double proposals
if cs.Proposal != nil {
if cs.Proposal != nil || proposal == nil {
return nil
}
@@ -1882,6 +1945,7 @@ func (cs *State) defaultSetProposal(proposal *types.Proposal) error {
proposal.Signature = p.Signature
cs.Proposal = proposal
cs.ProposalReceiveTime = recvTime
// We don't update cs.ProposalBlockParts if it is already set.
// This happens if we're already in cstypes.RoundStepCommit or if there is a valid block in the current round.
// TODO: We can check if Proposal is for a different block as this is a sign of misbehavior!
@@ -2423,32 +2487,10 @@ func repairWalFile(src, dst string) error {
// Block times must be monotonically increasing, so if the block time of the previous
// block is larger than the proposer's current time, then the proposer will sleep
// until its local clock exceeds the previous block time.
func proposerWaitTime(lt tmtime.Source, h types.Header) time.Duration {
func proposerWaitTime(lt tmtime.Source, bt time.Time) time.Duration {
t := lt.Now()
if h.Time.After(t) {
return h.Time.Sub(t)
if bt.After(t) {
return bt.Sub(t)
}
return 0
}
// proposalStepWaitingTime is used along with the `timeout-propose` configuration
// parameter to determines how long a validator will wait for a block to be sent from a proposer.
// proposalStepWaitingTime ensures that the validator waits long enough for the proposer to
// deliver a block with a monotically increasing timestamp.
//
// To ensure that the validator waits long enough, it must wait until the previous
// block's timestamp. It also must account for the difference between its own clock and
// the proposer's clock, i.e. the 'Precision', and the amount of time for the message to be transmitted,
// i.e. the MsgDelay.
//
// The result of proposalStepWaitingTime is compared with the configured `timeout-propose` duration,
// and the validator waits for whichever duration is larger before advancing to the next step
// and prevoting nil.
func proposalStepWaitingTime(lt tmtime.Source, h types.Header, tp types.TimestampParams) time.Duration {
t := lt.Now()
wt := h.Time.Add(tp.Precision).Add(tp.MsgDelay)
if t.After(wt) {
return 0
}
return wt.Sub(t)
}

View File

@@ -246,7 +246,7 @@ func TestStateBadProposal(t *testing.T) {
propBlock.AppHash = stateHash
propBlockParts := propBlock.MakePartSet(partSize)
blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()}
proposal := types.NewProposal(vs2.Height, round, -1, blockID)
proposal := types.NewProposal(vs2.Height, round, -1, blockID, propBlock.Header.Time)
p := proposal.ToProto()
if err := vs2.SignProposal(ctx, config.ChainID(), p); err != nil {
t.Fatal("failed to sign bad proposal", err)
@@ -306,7 +306,7 @@ func TestStateOversizedBlock(t *testing.T) {
propBlockParts := propBlock.MakePartSet(partSize)
blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()}
proposal := types.NewProposal(height, round, -1, blockID)
proposal := types.NewProposal(height, round, -1, blockID, propBlock.Header.Time)
p := proposal.ToProto()
if err := vs2.SignProposal(ctx, config.ChainID(), p); err != nil {
t.Fatal("failed to sign bad proposal", err)
@@ -856,7 +856,7 @@ func TestStateLock_POLRelock(t *testing.T) {
t.Log("### Starting Round 1")
incrementRound(vs2, vs3, vs4)
round++
propR1 := types.NewProposal(height, round, cs1.ValidRound, blockID)
propR1 := types.NewProposal(height, round, cs1.ValidRound, blockID, theBlock.Header.Time)
p := propR1.ToProto()
if err := vs2.SignProposal(ctx, cs1.state.ChainID, p); err != nil {
t.Fatalf("error signing proposal: %s", err)
@@ -1588,7 +1588,7 @@ func TestStateLock_POLSafety2(t *testing.T) {
round++ // moving to the next round
// in round 2 we see the polkad block from round 0
newProp := types.NewProposal(height, round, 0, propBlockID0)
newProp := types.NewProposal(height, round, 0, propBlockID0, propBlock0.Header.Time)
p := newProp.ToProto()
if err := vs3.SignProposal(ctx, config.ChainID(), p); err != nil {
t.Fatal(err)
@@ -1730,7 +1730,7 @@ func TestState_PrevotePOLFromPreviousRound(t *testing.T) {
t.Log("### Starting Round 2")
incrementRound(vs2, vs3, vs4)
round++
propR2 := types.NewProposal(height, round, 1, r1BlockID)
propR2 := types.NewProposal(height, round, 1, r1BlockID, propBlockR1.Header.Time)
p := propR2.ToProto()
if err := vs3.SignProposal(ctx, cs1.state.ChainID, p); err != nil {
t.Fatalf("error signing proposal: %s", err)
@@ -2291,7 +2291,7 @@ func TestStartNextHeightCorrectlyAfterTimeout(t *testing.T) {
signAddVotes(ctx, cs1, tmproto.PrecommitType, config.ChainID(), blockID, vs3)
// wait till timeout occurs
ensurePrecommitTimeout(t, precommitTimeoutCh)
ensureNewTimeout(t, precommitTimeoutCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds())
ensureNewRound(t, newRoundCh, height, round+1)
@@ -2529,6 +2529,7 @@ func TestStateOutputsBlockPartsStats(t *testing.T) {
cs, _, err := makeState(ctx, config, logger, 1)
require.NoError(t, err)
peerID, err := types.NewNodeID("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
require.NoError(t, err)
// 1) new block part
parts := types.NewPartSetFromData(tmrand.Bytes(100), 10)
@@ -2648,6 +2649,104 @@ func TestSignSameVoteTwice(t *testing.T) {
require.Equal(t, vote, vote2)
}
// TestStateTimestamp_ProposalNotMatch tests that a validator does not prevote a
// proposed block if the timestamp in the block does not matche the timestamp in the
// corresponding proposal message.
func TestStateTimestamp_ProposalNotMatch(t *testing.T) {
config := configSetup(t)
logger := log.TestingLogger()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cs1, vss, err := makeState(ctx, config, logger, 4)
require.NoError(t, err)
height, round := cs1.Height, cs1.Round
vs2, vs3, vs4 := vss[1], vss[2], vss[3]
proposalCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryCompleteProposal)
pv1, err := cs1.privValidator.GetPubKey(ctx)
require.NoError(t, err)
addr := pv1.Address()
voteCh := subscribeToVoter(ctx, t, cs1, addr)
propBlock, _ := cs1.createProposalBlock()
round++
incrementRound(vss[1:]...)
propBlockParts := propBlock.MakePartSet(types.BlockPartSizeBytes)
blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()}
// Create a proposal with a timestamp that does not match the timestamp of the block.
proposal := types.NewProposal(vs2.Height, round, -1, blockID, propBlock.Header.Time.Add(time.Millisecond))
p := proposal.ToProto()
if err := vs2.SignProposal(ctx, config.ChainID(), p); err != nil {
t.Fatal("failed to sign bad proposal", err)
}
proposal.Signature = p.Signature
require.NoError(t, cs1.SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"))
startTestRound(ctx, cs1, height, round)
ensureProposal(t, proposalCh, height, round, blockID)
signAddVotes(ctx, cs1, tmproto.PrevoteType, config.ChainID(), blockID, vs2, vs3, vs4)
// ensure that the validator prevotes nil.
ensurePrevote(t, voteCh, height, round)
validatePrevote(ctx, t, cs1, round, vss[0], nil)
ensurePrecommit(t, voteCh, height, round)
validatePrecommit(ctx, t, cs1, round, -1, vss[0], nil, nil)
}
// TestStateTimestamp_ProposalMatch tests that a validator prevotes a
// proposed block if the timestamp in the block matches the timestamp in the
// corresponding proposal message.
func TestStateTimestamp_ProposalMatch(t *testing.T) {
config := configSetup(t)
logger := log.TestingLogger()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cs1, vss, err := makeState(ctx, config, logger, 4)
require.NoError(t, err)
height, round := cs1.Height, cs1.Round
vs2, vs3, vs4 := vss[1], vss[2], vss[3]
proposalCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryCompleteProposal)
pv1, err := cs1.privValidator.GetPubKey(ctx)
require.NoError(t, err)
addr := pv1.Address()
voteCh := subscribeToVoter(ctx, t, cs1, addr)
propBlock, _ := cs1.createProposalBlock()
round++
incrementRound(vss[1:]...)
propBlockParts := propBlock.MakePartSet(types.BlockPartSizeBytes)
blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()}
// Create a proposal with a timestamp that matches the timestamp of the block.
proposal := types.NewProposal(vs2.Height, round, -1, blockID, propBlock.Header.Time)
p := proposal.ToProto()
if err := vs2.SignProposal(ctx, config.ChainID(), p); err != nil {
t.Fatal("failed to sign bad proposal", err)
}
proposal.Signature = p.Signature
require.NoError(t, cs1.SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"))
startTestRound(ctx, cs1, height, round)
ensureProposal(t, proposalCh, height, round, blockID)
signAddVotes(ctx, cs1, tmproto.PrevoteType, config.ChainID(), blockID, vs2, vs3, vs4)
// ensure that the validator prevotes the block.
ensurePrevote(t, voteCh, height, round)
validatePrevote(ctx, t, cs1, round, vss[0], propBlock.Hash())
ensurePrecommit(t, voteCh, height, round)
validatePrecommit(ctx, t, cs1, round, 1, vss[0], propBlock.Hash(), propBlock.Hash())
}
// subscribe subscribes test client to the given query and returns a channel with cap = 1.
func subscribe(
ctx context.Context,

View File

@@ -71,14 +71,15 @@ type RoundState struct {
StartTime time.Time `json:"start_time"`
// Subjective time when +2/3 precommits for Block at Round were found
CommitTime time.Time `json:"commit_time"`
Validators *types.ValidatorSet `json:"validators"`
Proposal *types.Proposal `json:"proposal"`
ProposalBlock *types.Block `json:"proposal_block"`
ProposalBlockParts *types.PartSet `json:"proposal_block_parts"`
LockedRound int32 `json:"locked_round"`
LockedBlock *types.Block `json:"locked_block"`
LockedBlockParts *types.PartSet `json:"locked_block_parts"`
CommitTime time.Time `json:"commit_time"`
Validators *types.ValidatorSet `json:"validators"`
Proposal *types.Proposal `json:"proposal"`
ProposalReceiveTime time.Time `json:"proposal_receive_time"`
ProposalBlock *types.Block `json:"proposal_block"`
ProposalBlockParts *types.PartSet `json:"proposal_block_parts"`
LockedRound int32 `json:"locked_round"`
LockedBlock *types.Block `json:"locked_block"`
LockedBlockParts *types.PartSet `json:"locked_block_parts"`
// Last known round with POL for non-nil valid block.
ValidRound int32 `json:"valid_round"`

View File

@@ -268,7 +268,7 @@ func (state State) MakeBlock(
if height == state.InitialHeight {
timestamp = state.LastBlockTime // genesis time
} else {
timestamp = MedianTime(commit, state.LastValidators)
timestamp = time.Now()
}
// Fill rest of header with state data.
@@ -283,29 +283,6 @@ func (state State) MakeBlock(
return block, block.MakePartSet(types.BlockPartSizeBytes)
}
// MedianTime computes a median time for a given Commit (based on Timestamp field of votes messages) and the
// corresponding validator set. The computed time is always between timestamps of
// the votes sent by honest processes, i.e., a faulty processes can not arbitrarily increase or decrease the
// computed value.
func MedianTime(commit *types.Commit, validators *types.ValidatorSet) time.Time {
weightedTimes := make([]*weightedTime, len(commit.Signatures))
totalVotingPower := int64(0)
for i, commitSig := range commit.Signatures {
if commitSig.Absent() {
continue
}
_, validator := validators.GetByAddress(commitSig.ValidatorAddress)
// If there's no condition, TestValidateBlockCommit panics; not needed normally.
if validator != nil {
totalVotingPower += validator.VotingPower
weightedTimes[i] = newWeightedTime(commitSig.Timestamp, validator.VotingPower)
}
}
return weightedMedian(weightedTimes, totalVotingPower)
}
//------------------------------------------------------------------------
// Genesis

View File

@@ -114,13 +114,6 @@ func validateBlock(state State, block *types.Block) error {
state.LastBlockTime,
)
}
medianTime := MedianTime(block.LastCommit, state.LastValidators)
if !block.Time.Equal(medianTime) {
return fmt.Errorf("invalid block time. Expected %v, got %v",
medianTime,
block.Time,
)
}
case block.Height == state.InitialHeight:
genesisTime := state.LastBlockTime

View File

@@ -64,7 +64,6 @@ func TestValidateBlockHeader(t *testing.T) {
{"ChainID wrong", func(block *types.Block) { block.ChainID = "not-the-real-one" }},
{"Height wrong", func(block *types.Block) { block.Height += 10 }},
{"Time wrong", func(block *types.Block) { block.Time = block.Time.Add(-time.Second * 1) }},
{"Time wrong 2", func(block *types.Block) { block.Time = block.Time.Add(time.Second * 1) }},
{"LastBlockID wrong", func(block *types.Block) { block.LastBlockID.PartSetHeader.Total += 10 }},
{"LastCommitHash wrong", func(block *types.Block) { block.LastCommitHash = wrongHash }},

View File

@@ -7,9 +7,9 @@ import (
"github.com/gogo/protobuf/proto"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto/ed25519"
ssproto "github.com/tendermint/tendermint/proto/tendermint/statesync"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/types"
)
func TestValidateMsg(t *testing.T) {
@@ -186,10 +186,30 @@ func TestStateSyncVectors(t *testing.T) {
{
"ParamsResponse",
&ssproto.ParamsResponse{
Height: 9001,
ConsensusParams: types.DefaultConsensusParams().ToProto(),
Height: 9001,
ConsensusParams: tmproto.ConsensusParams{
Block: &tmproto.BlockParams{
MaxBytes: 10,
MaxGas: 20,
},
Evidence: &tmproto.EvidenceParams{
MaxAgeNumBlocks: 10,
MaxAgeDuration: 300,
MaxBytes: 100,
},
Validator: &tmproto.ValidatorParams{
PubKeyTypes: []string{ed25519.KeyType},
},
Version: &tmproto.VersionParams{
AppVersion: 11,
},
Timing: &tmproto.TimingParams{
MessageDelay: 550,
Precision: 90,
},
},
},
"423408a946122f0a10088080c00a10ffffffffffffffffff01120e08a08d0612040880c60a188080401a090a07656432353531392200",
"423008a946122b0a04080a10141209080a120310ac0218641a090a07656432353531392202080b2a090a0310a6041202105a",
},
}

View File

@@ -34,6 +34,7 @@ type ConsensusParams struct {
Evidence *EvidenceParams `protobuf:"bytes,2,opt,name=evidence,proto3" json:"evidence,omitempty"`
Validator *ValidatorParams `protobuf:"bytes,3,opt,name=validator,proto3" json:"validator,omitempty"`
Version *VersionParams `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty"`
Timing *TimingParams `protobuf:"bytes,5,opt,name=timing,proto3" json:"timing,omitempty"`
}
func (m *ConsensusParams) Reset() { *m = ConsensusParams{} }
@@ -97,6 +98,13 @@ func (m *ConsensusParams) GetVersion() *VersionParams {
return nil
}
func (m *ConsensusParams) GetTiming() *TimingParams {
if m != nil {
return m.Timing
}
return nil
}
// BlockParams contains limits on the block size.
type BlockParams struct {
// Max block size, in bytes.
@@ -373,6 +381,58 @@ func (m *HashedParams) GetBlockMaxGas() int64 {
return 0
}
type TimingParams struct {
MessageDelay time.Duration `protobuf:"bytes,1,opt,name=message_delay,json=messageDelay,proto3,stdduration" json:"message_delay"`
Precision time.Duration `protobuf:"bytes,2,opt,name=precision,proto3,stdduration" json:"precision"`
}
func (m *TimingParams) Reset() { *m = TimingParams{} }
func (m *TimingParams) String() string { return proto.CompactTextString(m) }
func (*TimingParams) ProtoMessage() {}
func (*TimingParams) Descriptor() ([]byte, []int) {
return fileDescriptor_e12598271a686f57, []int{6}
}
func (m *TimingParams) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *TimingParams) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_TimingParams.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *TimingParams) XXX_Merge(src proto.Message) {
xxx_messageInfo_TimingParams.Merge(m, src)
}
func (m *TimingParams) XXX_Size() int {
return m.Size()
}
func (m *TimingParams) XXX_DiscardUnknown() {
xxx_messageInfo_TimingParams.DiscardUnknown(m)
}
var xxx_messageInfo_TimingParams proto.InternalMessageInfo
func (m *TimingParams) GetMessageDelay() time.Duration {
if m != nil {
return m.MessageDelay
}
return 0
}
func (m *TimingParams) GetPrecision() time.Duration {
if m != nil {
return m.Precision
}
return 0
}
func init() {
proto.RegisterType((*ConsensusParams)(nil), "tendermint.types.ConsensusParams")
proto.RegisterType((*BlockParams)(nil), "tendermint.types.BlockParams")
@@ -380,44 +440,48 @@ func init() {
proto.RegisterType((*ValidatorParams)(nil), "tendermint.types.ValidatorParams")
proto.RegisterType((*VersionParams)(nil), "tendermint.types.VersionParams")
proto.RegisterType((*HashedParams)(nil), "tendermint.types.HashedParams")
proto.RegisterType((*TimingParams)(nil), "tendermint.types.TimingParams")
}
func init() { proto.RegisterFile("tendermint/types/params.proto", fileDescriptor_e12598271a686f57) }
var fileDescriptor_e12598271a686f57 = []byte{
// 498 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x93, 0xc1, 0x6a, 0xd4, 0x40,
0x1c, 0xc6, 0x77, 0x9a, 0xda, 0xee, 0xfe, 0xe3, 0x76, 0xcb, 0x20, 0x18, 0x2b, 0xcd, 0xae, 0x39,
0x48, 0x41, 0x48, 0xc4, 0x22, 0x22, 0x08, 0xe2, 0x56, 0xa9, 0x20, 0x15, 0x09, 0xea, 0xa1, 0x97,
0x30, 0xd9, 0x8c, 0x69, 0xe8, 0x4e, 0x66, 0xc8, 0x24, 0xcb, 0xee, 0xcd, 0x47, 0xf0, 0xe8, 0x23,
0xe8, 0x9b, 0xf4, 0xd8, 0xa3, 0x27, 0x95, 0xdd, 0x17, 0x91, 0x4c, 0x32, 0xa6, 0x9b, 0xf6, 0x36,
0x33, 0xdf, 0xef, 0x9b, 0xe1, 0xfb, 0x86, 0x3f, 0xec, 0xe7, 0x34, 0x8d, 0x68, 0xc6, 0x92, 0x34,
0xf7, 0xf2, 0x85, 0xa0, 0xd2, 0x13, 0x24, 0x23, 0x4c, 0xba, 0x22, 0xe3, 0x39, 0xc7, 0xbb, 0x8d,
0xec, 0x2a, 0x79, 0xef, 0x4e, 0xcc, 0x63, 0xae, 0x44, 0xaf, 0x5c, 0x55, 0xdc, 0x9e, 0x1d, 0x73,
0x1e, 0x4f, 0xa9, 0xa7, 0x76, 0x61, 0xf1, 0xc5, 0x8b, 0x8a, 0x8c, 0xe4, 0x09, 0x4f, 0x2b, 0xdd,
0xf9, 0xba, 0x01, 0x83, 0x23, 0x9e, 0x4a, 0x9a, 0xca, 0x42, 0x7e, 0x50, 0x2f, 0xe0, 0x43, 0xb8,
0x15, 0x4e, 0xf9, 0xe4, 0xdc, 0x42, 0x23, 0x74, 0x60, 0x3e, 0xd9, 0x77, 0xdb, 0x6f, 0xb9, 0xe3,
0x52, 0xae, 0x68, 0xbf, 0x62, 0xf1, 0x0b, 0xe8, 0xd2, 0x59, 0x12, 0xd1, 0x74, 0x42, 0xad, 0x0d,
0xe5, 0x1b, 0x5d, 0xf7, 0xbd, 0xa9, 0x89, 0xda, 0xfa, 0xdf, 0x81, 0x5f, 0x42, 0x6f, 0x46, 0xa6,
0x49, 0x44, 0x72, 0x9e, 0x59, 0x86, 0xb2, 0x3f, 0xb8, 0x6e, 0xff, 0xac, 0x91, 0xda, 0xdf, 0x78,
0xf0, 0x73, 0xd8, 0x9e, 0xd1, 0x4c, 0x26, 0x3c, 0xb5, 0x36, 0x95, 0x7d, 0x78, 0x83, 0xbd, 0x02,
0x6a, 0xb3, 0xe6, 0x9d, 0x23, 0x30, 0xaf, 0xe4, 0xc1, 0xf7, 0xa1, 0xc7, 0xc8, 0x3c, 0x08, 0x17,
0x39, 0x95, 0xaa, 0x01, 0xc3, 0xef, 0x32, 0x32, 0x1f, 0x97, 0x7b, 0x7c, 0x17, 0xb6, 0x4b, 0x31,
0x26, 0x52, 0x85, 0x34, 0xfc, 0x2d, 0x46, 0xe6, 0xc7, 0x44, 0x3a, 0x3f, 0x11, 0xec, 0xac, 0xa7,
0xc3, 0x8f, 0x00, 0x97, 0x2c, 0x89, 0x69, 0x90, 0x16, 0x2c, 0x50, 0x35, 0xe9, 0x1b, 0x07, 0x8c,
0xcc, 0x5f, 0xc5, 0xf4, 0x7d, 0xc1, 0xd4, 0xd3, 0x12, 0x9f, 0xc0, 0xae, 0x86, 0xf5, 0x0f, 0xd5,
0x35, 0xde, 0x73, 0xab, 0x2f, 0x74, 0xf5, 0x17, 0xba, 0xaf, 0x6b, 0x60, 0xdc, 0xbd, 0xf8, 0x3d,
0xec, 0x7c, 0xff, 0x33, 0x44, 0xfe, 0x4e, 0x75, 0x9f, 0x56, 0xd6, 0x43, 0x18, 0xeb, 0x21, 0x9c,
0xa7, 0x30, 0x68, 0x35, 0x89, 0x1d, 0xe8, 0x8b, 0x22, 0x0c, 0xce, 0xe9, 0x22, 0x50, 0x5d, 0x59,
0x68, 0x64, 0x1c, 0xf4, 0x7c, 0x53, 0x14, 0xe1, 0x3b, 0xba, 0xf8, 0x58, 0x1e, 0x39, 0x8f, 0xa1,
0xbf, 0xd6, 0x20, 0x1e, 0x82, 0x49, 0x84, 0x08, 0x74, 0xef, 0x65, 0xb2, 0x4d, 0x1f, 0x88, 0x10,
0x35, 0xe6, 0x9c, 0xc2, 0xed, 0xb7, 0x44, 0x9e, 0xd1, 0xa8, 0x36, 0x3c, 0x84, 0x81, 0x6a, 0x21,
0x68, 0x17, 0xdc, 0x57, 0xc7, 0x27, 0xba, 0x65, 0x07, 0xfa, 0x0d, 0xd7, 0x74, 0x6d, 0x6a, 0xea,
0x98, 0xc8, 0xf1, 0xa7, 0x1f, 0x4b, 0x1b, 0x5d, 0x2c, 0x6d, 0x74, 0xb9, 0xb4, 0xd1, 0xdf, 0xa5,
0x8d, 0xbe, 0xad, 0xec, 0xce, 0xe5, 0xca, 0xee, 0xfc, 0x5a, 0xd9, 0x9d, 0xd3, 0x67, 0x71, 0x92,
0x9f, 0x15, 0xa1, 0x3b, 0xe1, 0xcc, 0xbb, 0x3a, 0x48, 0xcd, 0xb2, 0x9a, 0x94, 0xf6, 0x90, 0x85,
0x5b, 0xea, 0xfc, 0xf0, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x18, 0x54, 0x4f, 0xe1, 0x7f, 0x03,
0x00, 0x00,
// 559 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x94, 0xcf, 0x6e, 0xd3, 0x30,
0x1c, 0xc7, 0x9b, 0x75, 0xeb, 0xda, 0x5f, 0xdb, 0x75, 0xb2, 0x90, 0x08, 0x43, 0x4b, 0x4b, 0x0e,
0x68, 0x12, 0x52, 0x82, 0x98, 0x00, 0x21, 0x21, 0xa1, 0x75, 0x43, 0x4c, 0x42, 0x43, 0x28, 0x1a,
0x1c, 0x76, 0x89, 0x9c, 0xd6, 0x64, 0xd1, 0xea, 0xd8, 0x8a, 0x93, 0xaa, 0x7d, 0x0b, 0x8e, 0x48,
0xbc, 0x00, 0xbc, 0x01, 0x8f, 0xb0, 0xe3, 0x8e, 0x9c, 0x00, 0xb5, 0x2f, 0x82, 0xe2, 0xd8, 0xa4,
0x7f, 0x38, 0xc0, 0x2d, 0xf1, 0xf7, 0xfb, 0x89, 0xe5, 0xcf, 0xaf, 0x35, 0xec, 0xa7, 0x24, 0x1e,
0x92, 0x84, 0x46, 0x71, 0xea, 0xa6, 0x53, 0x4e, 0x84, 0xcb, 0x71, 0x82, 0xa9, 0x70, 0x78, 0xc2,
0x52, 0x86, 0x76, 0xcb, 0xd8, 0x91, 0xf1, 0xde, 0xad, 0x90, 0x85, 0x4c, 0x86, 0x6e, 0xfe, 0x54,
0xf4, 0xf6, 0xac, 0x90, 0xb1, 0x70, 0x44, 0x5c, 0xf9, 0x16, 0x64, 0x1f, 0xdc, 0x61, 0x96, 0xe0,
0x34, 0x62, 0x71, 0x91, 0xdb, 0xdf, 0x36, 0xa0, 0x73, 0xcc, 0x62, 0x41, 0x62, 0x91, 0x89, 0xb7,
0x72, 0x07, 0x74, 0x08, 0x5b, 0xc1, 0x88, 0x0d, 0xae, 0x4c, 0xa3, 0x67, 0x1c, 0x34, 0x1f, 0xed,
0x3b, 0xab, 0x7b, 0x39, 0xfd, 0x3c, 0x2e, 0xda, 0x5e, 0xd1, 0x45, 0xcf, 0xa1, 0x4e, 0xc6, 0xd1,
0x90, 0xc4, 0x03, 0x62, 0x6e, 0x48, 0xae, 0xb7, 0xce, 0xbd, 0x54, 0x0d, 0x85, 0xfe, 0x21, 0xd0,
0x0b, 0x68, 0x8c, 0xf1, 0x28, 0x1a, 0xe2, 0x94, 0x25, 0x66, 0x55, 0xe2, 0xf7, 0xd6, 0xf1, 0xf7,
0xba, 0xa2, 0xf8, 0x92, 0x41, 0xcf, 0x60, 0x7b, 0x4c, 0x12, 0x11, 0xb1, 0xd8, 0xdc, 0x94, 0x78,
0xf7, 0x2f, 0x78, 0x51, 0x50, 0xb0, 0xee, 0xa3, 0x27, 0x50, 0x4b, 0x23, 0x1a, 0xc5, 0xa1, 0xb9,
0x25, 0x49, 0x6b, 0x9d, 0x3c, 0x97, 0xb9, 0x02, 0x55, 0xdb, 0x3e, 0x86, 0xe6, 0x82, 0x07, 0x74,
0x17, 0x1a, 0x14, 0x4f, 0xfc, 0x60, 0x9a, 0x12, 0x21, 0xcd, 0x55, 0xbd, 0x3a, 0xc5, 0x93, 0x7e,
0xfe, 0x8e, 0x6e, 0xc3, 0x76, 0x1e, 0x86, 0x58, 0x48, 0x39, 0x55, 0xaf, 0x46, 0xf1, 0xe4, 0x15,
0x16, 0xf6, 0x57, 0x03, 0x76, 0x96, 0xad, 0xa0, 0x07, 0x80, 0xf2, 0x2e, 0x0e, 0x89, 0x1f, 0x67,
0xd4, 0x97, 0x7a, 0xf5, 0x17, 0x3b, 0x14, 0x4f, 0x8e, 0x42, 0xf2, 0x26, 0xa3, 0x72, 0x6b, 0x81,
0xce, 0x60, 0x57, 0x97, 0xf5, 0x64, 0x95, 0xfe, 0x3b, 0x4e, 0x31, 0x7a, 0x47, 0x8f, 0xde, 0x39,
0x51, 0x85, 0x7e, 0xfd, 0xfa, 0x47, 0xb7, 0xf2, 0xe9, 0x67, 0xd7, 0xf0, 0x76, 0x8a, 0xef, 0xe9,
0x64, 0xf9, 0x10, 0xd5, 0xe5, 0x43, 0xd8, 0x8f, 0xa1, 0xb3, 0x32, 0x01, 0x64, 0x43, 0x9b, 0x67,
0x81, 0x7f, 0x45, 0xa6, 0xbe, 0x34, 0x65, 0x1a, 0xbd, 0xea, 0x41, 0xc3, 0x6b, 0xf2, 0x2c, 0x78,
0x4d, 0xa6, 0xe7, 0xf9, 0x92, 0xfd, 0x10, 0xda, 0x4b, 0xe6, 0x51, 0x17, 0x9a, 0x98, 0x73, 0x5f,
0xcf, 0x2b, 0x3f, 0xd9, 0xa6, 0x07, 0x98, 0x73, 0x55, 0xb3, 0x2f, 0xa0, 0x75, 0x8a, 0xc5, 0x25,
0x19, 0x2a, 0xe0, 0x3e, 0x74, 0xa4, 0x05, 0x7f, 0x55, 0x70, 0x5b, 0x2e, 0x9f, 0x69, 0xcb, 0x36,
0xb4, 0xcb, 0x5e, 0xe9, 0xba, 0xa9, 0x5b, 0xb9, 0xf0, 0xcf, 0x06, 0xb4, 0x16, 0xc7, 0x89, 0x4e,
0xa1, 0x4d, 0x89, 0x10, 0xd2, 0x20, 0x19, 0xe1, 0xa9, 0xfa, 0xd5, 0xff, 0x93, 0xbe, 0x96, 0x22,
0x4f, 0x72, 0x10, 0x1d, 0x41, 0x83, 0x27, 0x64, 0x10, 0x89, 0xff, 0x1c, 0x42, 0x49, 0xf5, 0xdf,
0x7d, 0x99, 0x59, 0xc6, 0xf5, 0xcc, 0x32, 0x6e, 0x66, 0x96, 0xf1, 0x6b, 0x66, 0x19, 0x1f, 0xe7,
0x56, 0xe5, 0x66, 0x6e, 0x55, 0xbe, 0xcf, 0xad, 0xca, 0xc5, 0xd3, 0x30, 0x4a, 0x2f, 0xb3, 0xc0,
0x19, 0x30, 0xea, 0x2e, 0x5e, 0x0f, 0xe5, 0x63, 0xf1, 0xff, 0x5f, 0xbd, 0x3a, 0x82, 0x9a, 0x5c,
0x3f, 0xfc, 0x1d, 0x00, 0x00, 0xff, 0xff, 0xf4, 0x57, 0x2d, 0xa2, 0x55, 0x04, 0x00, 0x00,
}
func (this *ConsensusParams) Equal(that interface{}) bool {
@@ -451,6 +515,9 @@ func (this *ConsensusParams) Equal(that interface{}) bool {
if !this.Version.Equal(that1.Version) {
return false
}
if !this.Timing.Equal(that1.Timing) {
return false
}
return true
}
func (this *BlockParams) Equal(that interface{}) bool {
@@ -590,6 +657,33 @@ func (this *HashedParams) Equal(that interface{}) bool {
}
return true
}
func (this *TimingParams) Equal(that interface{}) bool {
if that == nil {
return this == nil
}
that1, ok := that.(*TimingParams)
if !ok {
that2, ok := that.(TimingParams)
if ok {
that1 = &that2
} else {
return false
}
}
if that1 == nil {
return this == nil
} else if this == nil {
return false
}
if this.MessageDelay != that1.MessageDelay {
return false
}
if this.Precision != that1.Precision {
return false
}
return true
}
func (m *ConsensusParams) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
@@ -610,6 +704,18 @@ func (m *ConsensusParams) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if m.Timing != nil {
{
size, err := m.Timing.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintParams(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x2a
}
if m.Version != nil {
{
size, err := m.Version.MarshalToSizedBuffer(dAtA[:i])
@@ -719,12 +825,12 @@ func (m *EvidenceParams) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i--
dAtA[i] = 0x18
}
n5, err5 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.MaxAgeDuration, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.MaxAgeDuration):])
if err5 != nil {
return 0, err5
n6, err6 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.MaxAgeDuration, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.MaxAgeDuration):])
if err6 != nil {
return 0, err6
}
i -= n5
i = encodeVarintParams(dAtA, i, uint64(n5))
i -= n6
i = encodeVarintParams(dAtA, i, uint64(n6))
i--
dAtA[i] = 0x12
if m.MaxAgeNumBlocks != 0 {
@@ -828,6 +934,45 @@ func (m *HashedParams) MarshalToSizedBuffer(dAtA []byte) (int, error) {
return len(dAtA) - i, nil
}
func (m *TimingParams) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *TimingParams) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *TimingParams) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
n7, err7 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.Precision, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.Precision):])
if err7 != nil {
return 0, err7
}
i -= n7
i = encodeVarintParams(dAtA, i, uint64(n7))
i--
dAtA[i] = 0x12
n8, err8 := github_com_gogo_protobuf_types.StdDurationMarshalTo(m.MessageDelay, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdDuration(m.MessageDelay):])
if err8 != nil {
return 0, err8
}
i -= n8
i = encodeVarintParams(dAtA, i, uint64(n8))
i--
dAtA[i] = 0xa
return len(dAtA) - i, nil
}
func encodeVarintParams(dAtA []byte, offset int, v uint64) int {
offset -= sovParams(v)
base := offset
@@ -861,6 +1006,10 @@ func (m *ConsensusParams) Size() (n int) {
l = m.Version.Size()
n += 1 + l + sovParams(uint64(l))
}
if m.Timing != nil {
l = m.Timing.Size()
n += 1 + l + sovParams(uint64(l))
}
return n
}
@@ -938,6 +1087,19 @@ func (m *HashedParams) Size() (n int) {
return n
}
func (m *TimingParams) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.MessageDelay)
n += 1 + l + sovParams(uint64(l))
l = github_com_gogo_protobuf_types.SizeOfStdDuration(m.Precision)
n += 1 + l + sovParams(uint64(l))
return n
}
func sovParams(x uint64) (n int) {
return (math_bits.Len64(x|1) + 6) / 7
}
@@ -1117,6 +1279,42 @@ func (m *ConsensusParams) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 5:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Timing", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowParams
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthParams
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthParams
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Timing == nil {
m.Timing = &TimingParams{}
}
if err := m.Timing.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipParams(dAtA[iNdEx:])
@@ -1586,6 +1784,122 @@ func (m *HashedParams) Unmarshal(dAtA []byte) error {
}
return nil
}
func (m *TimingParams) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowParams
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: TimingParams: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: TimingParams: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field MessageDelay", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowParams
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthParams
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthParams
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.MessageDelay, dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Precision", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowParams
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthParams
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthParams
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if err := github_com_gogo_protobuf_types.StdDurationUnmarshal(&m.Precision, dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipParams(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthParams
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipParams(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0

View File

@@ -1,4 +0,0 @@
ARG TENDERMINT_VERSION=latest
FROM tendermint/tendermint:${TENDERMINT_VERSION}
COPY tm-signer-harness /usr/bin/tm-signer-harness

View File

@@ -1,21 +0,0 @@
.PHONY: build install docker-image
TENDERMINT_VERSION?=latest
BUILD_TAGS?='tendermint'
VERSION := $(shell git describe --always)
BUILD_FLAGS = -ldflags "-X github.com/tendermint/tendermint/version.TMCoreSemVer=$(VERSION)"
.DEFAULT_GOAL := build
build:
CGO_ENABLED=0 go build $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o ../../build/tm-signer-harness main.go
install:
CGO_ENABLED=0 go install $(BUILD_FLAGS) -tags $(BUILD_TAGS) .
docker-image:
GOOS=linux GOARCH=amd64 go build $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o tm-signer-harness main.go
docker build \
--build-arg TENDERMINT_VERSION=$(TENDERMINT_VERSION) \
-t tendermint/tm-signer-harness:$(TENDERMINT_VERSION) .
rm -rf tm-signer-harness

View File

@@ -1,5 +0,0 @@
# tm-signer-harness
See the [`tm-signer-harness`
documentation](https://tendermint.com/docs/tools/remote-signer-validation.html)
for more details.

View File

@@ -1,427 +0,0 @@
package internal
import (
"bytes"
"context"
"fmt"
"net"
"os"
"os/signal"
"time"
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/internal/state"
"github.com/tendermint/tendermint/privval"
"github.com/tendermint/tendermint/libs/log"
tmnet "github.com/tendermint/tendermint/libs/net"
tmos "github.com/tendermint/tendermint/libs/os"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/types"
)
// Test harness error codes (which act as exit codes when the test harness fails).
const (
NoError int = iota // 0
ErrInvalidParameters // 1
ErrMaxAcceptRetriesReached // 2
ErrFailedToLoadGenesisFile // 3
ErrFailedToCreateListener // 4
ErrFailedToStartListener // 5
ErrInterrupted // 6
ErrOther // 7
ErrTestPublicKeyFailed // 8
ErrTestSignProposalFailed // 9
ErrTestSignVoteFailed // 10
)
var voteTypes = []tmproto.SignedMsgType{tmproto.PrevoteType, tmproto.PrecommitType}
// TestHarnessError allows us to keep track of which exit code should be used
// when exiting the main program.
type TestHarnessError struct {
Code int // The exit code to return
Err error // The original error
Info string // Any additional information
}
var _ error = (*TestHarnessError)(nil)
// TestHarness allows for testing of a remote signer to ensure compatibility
// with this version of Tendermint.
type TestHarness struct {
addr string
signerClient *privval.SignerClient
fpv *privval.FilePV
chainID string
acceptRetries int
logger log.Logger
exitWhenComplete bool
exitCode int
}
// TestHarnessConfig provides configuration to set up a remote signer test
// harness.
type TestHarnessConfig struct {
BindAddr string
KeyFile string
StateFile string
GenesisFile string
AcceptDeadline time.Duration
ConnDeadline time.Duration
AcceptRetries int
SecretConnKey ed25519.PrivKey
ExitWhenComplete bool // Whether or not to call os.Exit when the harness has completed.
}
// timeoutError can be used to check if an error returned from the netp package
// was due to a timeout.
type timeoutError interface {
Timeout() bool
}
// NewTestHarness will load Tendermint data from the given files (including
// validator public/private keypairs and chain details) and create a new
// harness.
func NewTestHarness(ctx context.Context, logger log.Logger, cfg TestHarnessConfig) (*TestHarness, error) {
keyFile := ExpandPath(cfg.KeyFile)
stateFile := ExpandPath(cfg.StateFile)
logger.Info("Loading private validator configuration", "keyFile", keyFile, "stateFile", stateFile)
// NOTE: LoadFilePV ultimately calls os.Exit on failure. No error will be
// returned if this call fails.
fpv, err := privval.LoadFilePV(keyFile, stateFile)
if err != nil {
return nil, err
}
genesisFile := ExpandPath(cfg.GenesisFile)
logger.Info("Loading chain ID from genesis file", "genesisFile", genesisFile)
st, err := state.MakeGenesisDocFromFile(genesisFile)
if err != nil {
return nil, newTestHarnessError(ErrFailedToLoadGenesisFile, err, genesisFile)
}
logger.Info("Loaded genesis file", "chainID", st.ChainID)
spv, err := newTestHarnessListener(logger, cfg)
if err != nil {
return nil, newTestHarnessError(ErrFailedToCreateListener, err, "")
}
signerClient, err := privval.NewSignerClient(ctx, spv, st.ChainID)
if err != nil {
return nil, newTestHarnessError(ErrFailedToCreateListener, err, "")
}
return &TestHarness{
addr: cfg.BindAddr,
signerClient: signerClient,
fpv: fpv,
chainID: st.ChainID,
acceptRetries: cfg.AcceptRetries,
logger: logger,
exitWhenComplete: cfg.ExitWhenComplete,
exitCode: 0,
}, nil
}
// Run will execute the tests associated with this test harness. The intention
// here is to call this from one's `main` function, as the way it succeeds or
// fails at present is to call os.Exit() with an exit code related to the error
// that caused the tests to fail, or exit code 0 on success.
func (th *TestHarness) Run() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for sig := range c {
th.logger.Info("Caught interrupt, terminating...", "sig", sig)
th.Shutdown(newTestHarnessError(ErrInterrupted, nil, ""))
}
}()
th.logger.Info("Starting test harness")
accepted := false
var startErr error
for acceptRetries := th.acceptRetries; acceptRetries > 0; acceptRetries-- {
th.logger.Info("Attempting to accept incoming connection", "acceptRetries", acceptRetries)
if err := th.signerClient.WaitForConnection(10 * time.Millisecond); err != nil {
// if it wasn't a timeout error
if _, ok := err.(timeoutError); !ok {
th.logger.Error("Failed to start listener", "err", err)
th.Shutdown(newTestHarnessError(ErrFailedToStartListener, err, ""))
// we need the return statements in case this is being run
// from a unit test - otherwise this function will just die
// when os.Exit is called
return
}
startErr = err
} else {
th.logger.Info("Accepted external connection")
accepted = true
break
}
}
if !accepted {
th.logger.Error("Maximum accept retries reached", "acceptRetries", th.acceptRetries)
th.Shutdown(newTestHarnessError(ErrMaxAcceptRetriesReached, startErr, ""))
return
}
// Run the tests
if err := th.TestPublicKey(); err != nil {
th.Shutdown(err)
return
}
if err := th.TestSignProposal(); err != nil {
th.Shutdown(err)
return
}
if err := th.TestSignVote(); err != nil {
th.Shutdown(err)
return
}
th.logger.Info("SUCCESS! All tests passed.")
th.Shutdown(nil)
}
// TestPublicKey just validates that we can (1) fetch the public key from the
// remote signer, and (2) it matches the public key we've configured for our
// local Tendermint version.
func (th *TestHarness) TestPublicKey() error {
th.logger.Info("TEST: Public key of remote signer")
fpvk, err := th.fpv.GetPubKey(context.Background())
if err != nil {
return err
}
th.logger.Info("Local", "pubKey", fpvk)
sck, err := th.signerClient.GetPubKey(context.Background())
if err != nil {
return err
}
th.logger.Info("Remote", "pubKey", sck)
if !bytes.Equal(fpvk.Bytes(), sck.Bytes()) {
th.logger.Error("FAILED: Local and remote public keys do not match")
return newTestHarnessError(ErrTestPublicKeyFailed, nil, "")
}
return nil
}
// TestSignProposal makes sure the remote signer can successfully sign
// proposals.
func (th *TestHarness) TestSignProposal() error {
th.logger.Info("TEST: Signing of proposals")
// sha256 hash of "hash"
hash := tmhash.Sum([]byte("hash"))
prop := &types.Proposal{
Type: tmproto.ProposalType,
Height: 100,
Round: 0,
POLRound: -1,
BlockID: types.BlockID{
Hash: hash,
PartSetHeader: types.PartSetHeader{
Hash: hash,
Total: 1000000,
},
},
Timestamp: time.Now(),
}
p := prop.ToProto()
propBytes := types.ProposalSignBytes(th.chainID, p)
if err := th.signerClient.SignProposal(context.Background(), th.chainID, p); err != nil {
th.logger.Error("FAILED: Signing of proposal", "err", err)
return newTestHarnessError(ErrTestSignProposalFailed, err, "")
}
prop.Signature = p.Signature
th.logger.Debug("Signed proposal", "prop", prop)
// first check that it's a basically valid proposal
if err := prop.ValidateBasic(); err != nil {
th.logger.Error("FAILED: Signed proposal is invalid", "err", err)
return newTestHarnessError(ErrTestSignProposalFailed, err, "")
}
sck, err := th.signerClient.GetPubKey(context.Background())
if err != nil {
return err
}
// now validate the signature on the proposal
if sck.VerifySignature(propBytes, prop.Signature) {
th.logger.Info("Successfully validated proposal signature")
} else {
th.logger.Error("FAILED: Proposal signature validation failed")
return newTestHarnessError(ErrTestSignProposalFailed, nil, "signature validation failed")
}
return nil
}
// TestSignVote makes sure the remote signer can successfully sign all kinds of
// votes.
func (th *TestHarness) TestSignVote() error {
th.logger.Info("TEST: Signing of votes")
for _, voteType := range voteTypes {
th.logger.Info("Testing vote type", "type", voteType)
hash := tmhash.Sum([]byte("hash"))
vote := &types.Vote{
Type: voteType,
Height: 101,
Round: 0,
BlockID: types.BlockID{
Hash: hash,
PartSetHeader: types.PartSetHeader{
Hash: hash,
Total: 1000000,
},
},
ValidatorIndex: 0,
ValidatorAddress: tmhash.SumTruncated([]byte("addr")),
Timestamp: time.Now(),
}
v := vote.ToProto()
voteBytes := types.VoteSignBytes(th.chainID, v)
// sign the vote
if err := th.signerClient.SignVote(context.Background(), th.chainID, v); err != nil {
th.logger.Error("FAILED: Signing of vote", "err", err)
return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType))
}
vote.Signature = v.Signature
th.logger.Debug("Signed vote", "vote", vote)
// validate the contents of the vote
if err := vote.ValidateBasic(); err != nil {
th.logger.Error("FAILED: Signed vote is invalid", "err", err)
return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType))
}
sck, err := th.signerClient.GetPubKey(context.Background())
if err != nil {
return err
}
// now validate the signature on the proposal
if sck.VerifySignature(voteBytes, vote.Signature) {
th.logger.Info("Successfully validated vote signature", "type", voteType)
} else {
th.logger.Error("FAILED: Vote signature validation failed", "type", voteType)
return newTestHarnessError(ErrTestSignVoteFailed, nil, "signature validation failed")
}
}
return nil
}
// Shutdown will kill the test harness and attempt to close all open sockets
// gracefully. If the supplied error is nil, it is assumed that the exit code
// should be 0. If err is not nil, it will exit with an exit code related to the
// error.
func (th *TestHarness) Shutdown(err error) {
var exitCode int
if err == nil {
exitCode = NoError
} else if therr, ok := err.(*TestHarnessError); ok {
exitCode = therr.Code
} else {
exitCode = ErrOther
}
th.exitCode = exitCode
// in case sc.Stop() takes too long
if th.exitWhenComplete {
go func() {
time.Sleep(time.Duration(5) * time.Second)
th.logger.Error("Forcibly exiting program after timeout")
os.Exit(exitCode)
}()
}
err = th.signerClient.Close()
if err != nil {
th.logger.Error("Failed to cleanly stop listener: %s", err.Error())
}
if th.exitWhenComplete {
os.Exit(exitCode)
}
}
// newTestHarnessListener creates our client instance which we will use for testing.
func newTestHarnessListener(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerListenerEndpoint, error) {
proto, addr := tmnet.ProtocolAndAddress(cfg.BindAddr)
if proto == "unix" {
// make sure the socket doesn't exist - if so, try to delete it
if tmos.FileExists(addr) {
if err := os.Remove(addr); err != nil {
logger.Error("Failed to remove existing Unix domain socket", "addr", addr)
return nil, err
}
}
}
ln, err := net.Listen(proto, addr)
if err != nil {
return nil, err
}
logger.Info("Listening", "proto", proto, "addr", addr)
var svln net.Listener
switch proto {
case "unix":
unixLn := privval.NewUnixListener(ln)
privval.UnixListenerTimeoutAccept(cfg.AcceptDeadline)(unixLn)
privval.UnixListenerTimeoutReadWrite(cfg.ConnDeadline)(unixLn)
svln = unixLn
case "tcp":
tcpLn := privval.NewTCPListener(ln, cfg.SecretConnKey)
privval.TCPListenerTimeoutAccept(cfg.AcceptDeadline)(tcpLn)
privval.TCPListenerTimeoutReadWrite(cfg.ConnDeadline)(tcpLn)
logger.Info("Resolved TCP address for listener", "addr", tcpLn.Addr())
svln = tcpLn
default:
_ = ln.Close()
logger.Error("Unsupported protocol (must be unix:// or tcp://)", "proto", proto)
return nil, newTestHarnessError(ErrInvalidParameters, nil, fmt.Sprintf("Unsupported protocol: %s", proto))
}
return privval.NewSignerListenerEndpoint(logger, svln), nil
}
func newTestHarnessError(code int, err error, info string) *TestHarnessError {
return &TestHarnessError{
Code: code,
Err: err,
Info: info,
}
}
func (e *TestHarnessError) Error() string {
var msg string
switch e.Code {
case ErrInvalidParameters:
msg = "Invalid parameters supplied to application"
case ErrMaxAcceptRetriesReached:
msg = "Maximum accept retries reached"
case ErrFailedToLoadGenesisFile:
msg = "Failed to load genesis file"
case ErrFailedToCreateListener:
msg = "Failed to create listener"
case ErrFailedToStartListener:
msg = "Failed to start listener"
case ErrInterrupted:
msg = "Interrupted"
case ErrTestPublicKeyFailed:
msg = "Public key validation test failed"
case ErrTestSignProposalFailed:
msg = "Proposal signing validation test failed"
case ErrTestSignVoteFailed:
msg = "Vote signing validation test failed"
default:
msg = "Unknown error"
}
if len(e.Info) > 0 {
msg = fmt.Sprintf("%s: %s", msg, e.Info)
}
if e.Err != nil {
msg = fmt.Sprintf("%s (original error: %s)", msg, e.Err.Error())
}
return msg
}

View File

@@ -1,226 +0,0 @@
package internal
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/privval"
"github.com/tendermint/tendermint/types"
)
const (
keyFileContents = `{
"address": "D08FCA3BA74CF17CBFC15E64F9505302BB0E2748",
"pub_key": {
"type": "tendermint/PubKeyEd25519",
"value": "ZCsuTjaczEyon70nmKxwvwu+jqrbq5OH3yQjcK0SFxc="
},
"priv_key": {
"type": "tendermint/PrivKeyEd25519",
"value": "8O39AkQsoe1sBQwud/Kdul8lg8K9SFsql9aZvwXQSt1kKy5ONpzMTKifvSeYrHC/C76Oqturk4ffJCNwrRIXFw=="
}
}`
stateFileContents = `{
"height": "0",
"round": 0,
"step": 0
}`
genesisFileContents = `{
"genesis_time": "2019-01-15T11:56:34.8963Z",
"chain_id": "test-chain-0XwP5E",
"consensus_params": {
"block": {
"max_bytes": "22020096",
"max_gas": "-1",
"time_iota_ms": "1000"
},
"evidence": {
"max_age_num_blocks": "100000",
"max_age_duration": "172800000000000",
"max_num": 50
},
"validator": {
"pub_key_types": [
"ed25519"
]
}
},
"validators": [
{
"address": "D08FCA3BA74CF17CBFC15E64F9505302BB0E2748",
"pub_key": {
"type": "tendermint/PubKeyEd25519",
"value": "ZCsuTjaczEyon70nmKxwvwu+jqrbq5OH3yQjcK0SFxc="
},
"power": "10",
"name": ""
}
],
"app_hash": ""
}`
defaultConnDeadline = 100
)
func TestRemoteSignerTestHarnessMaxAcceptRetriesReached(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cfg := makeConfig(t, 1, 2)
defer cleanup(cfg)
th, err := NewTestHarness(ctx, log.TestingLogger(), cfg)
require.NoError(t, err)
th.Run()
assert.Equal(t, ErrMaxAcceptRetriesReached, th.exitCode)
}
func TestRemoteSignerTestHarnessSuccessfulRun(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
harnessTest(
ctx,
t,
func(th *TestHarness) *privval.SignerServer {
return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, false)
},
NoError,
)
}
func TestRemoteSignerPublicKeyCheckFailed(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
harnessTest(
ctx,
t,
func(th *TestHarness) *privval.SignerServer {
return newMockSignerServer(t, th, ed25519.GenPrivKey(), false, false)
},
ErrTestPublicKeyFailed,
)
}
func TestRemoteSignerProposalSigningFailed(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
harnessTest(
ctx,
t,
func(th *TestHarness) *privval.SignerServer {
return newMockSignerServer(t, th, th.fpv.Key.PrivKey, true, false)
},
ErrTestSignProposalFailed,
)
}
func TestRemoteSignerVoteSigningFailed(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
harnessTest(
ctx,
t,
func(th *TestHarness) *privval.SignerServer {
return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, true)
},
ErrTestSignVoteFailed,
)
}
func newMockSignerServer(
t *testing.T,
th *TestHarness,
privKey crypto.PrivKey,
breakProposalSigning bool,
breakVoteSigning bool,
) *privval.SignerServer {
mockPV := types.NewMockPVWithParams(privKey, breakProposalSigning, breakVoteSigning)
dialerEndpoint := privval.NewSignerDialerEndpoint(
th.logger,
privval.DialTCPFn(
th.addr,
time.Duration(defaultConnDeadline)*time.Millisecond,
ed25519.GenPrivKey(),
),
)
return privval.NewSignerServer(dialerEndpoint, th.chainID, mockPV)
}
// For running relatively standard tests.
func harnessTest(
ctx context.Context,
t *testing.T,
signerServerMaker func(th *TestHarness) *privval.SignerServer,
expectedExitCode int,
) {
cfg := makeConfig(t, 100, 3)
defer cleanup(cfg)
th, err := NewTestHarness(ctx, log.TestingLogger(), cfg)
require.NoError(t, err)
donec := make(chan struct{})
go func() {
defer close(donec)
th.Run()
}()
ss := signerServerMaker(th)
require.NoError(t, ss.Start(ctx))
assert.True(t, ss.IsRunning())
defer ss.Stop() //nolint:errcheck // ignore for tests
<-donec
assert.Equal(t, expectedExitCode, th.exitCode)
}
func makeConfig(t *testing.T, acceptDeadline, acceptRetries int) TestHarnessConfig {
return TestHarnessConfig{
BindAddr: privval.GetFreeLocalhostAddrPort(),
KeyFile: makeTempFile("tm-testharness-keyfile", keyFileContents),
StateFile: makeTempFile("tm-testharness-statefile", stateFileContents),
GenesisFile: makeTempFile("tm-testharness-genesisfile", genesisFileContents),
AcceptDeadline: time.Duration(acceptDeadline) * time.Millisecond,
ConnDeadline: time.Duration(defaultConnDeadline) * time.Millisecond,
AcceptRetries: acceptRetries,
SecretConnKey: ed25519.GenPrivKey(),
ExitWhenComplete: false,
}
}
func cleanup(cfg TestHarnessConfig) {
os.Remove(cfg.KeyFile)
os.Remove(cfg.StateFile)
os.Remove(cfg.GenesisFile)
}
func makeTempFile(name, content string) string {
tempFile, err := os.CreateTemp("", fmt.Sprintf("%s-*", name))
if err != nil {
panic(err)
}
if _, err := tempFile.Write([]byte(content)); err != nil {
tempFile.Close()
panic(err)
}
if err := tempFile.Close(); err != nil {
panic(err)
}
return tempFile.Name()
}

View File

@@ -1,25 +0,0 @@
package internal
import (
"os/user"
"path/filepath"
"strings"
)
// ExpandPath will check if the given path begins with a "~" symbol, and if so,
// will expand it to become the user's home directory. If it fails to expand the
// path it will automatically return the original path itself.
func ExpandPath(path string) string {
usr, err := user.Current()
if err != nil {
return path
}
if path == "~" {
return usr.HomeDir
} else if strings.HasPrefix(path, "~/") {
return filepath.Join(usr.HomeDir, path[2:])
}
return path
}

View File

@@ -1,203 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"time"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/privval"
"github.com/tendermint/tendermint/tools/tm-signer-harness/internal"
"github.com/tendermint/tendermint/version"
)
const (
defaultAcceptRetries = 100
defaultBindAddr = "tcp://127.0.0.1:0"
defaultAcceptDeadline = 1
defaultConnDeadline = 3
defaultExtractKeyOutput = "./signing.key"
)
var defaultTMHome string
var logger = log.MustNewDefaultLogger(log.LogFormatPlain, log.LogLevelInfo, false)
// Command line flags
var (
flagAcceptRetries int
flagBindAddr string
flagTMHome string
flagKeyOutputPath string
)
// Command line commands
var (
rootCmd *flag.FlagSet
runCmd *flag.FlagSet
extractKeyCmd *flag.FlagSet
versionCmd *flag.FlagSet
)
func init() {
rootCmd = flag.NewFlagSet("root", flag.ExitOnError)
rootCmd.Usage = func() {
fmt.Println(`Remote signer test harness for Tendermint.
Usage:
tm-signer-harness <command> [flags]
Available Commands:
extract_key Extracts a signing key from a local Tendermint instance
help Help on the available commands
run Runs the test harness
version Display version information and exit
Use "tm-signer-harness help <command>" for more information about that command.`)
fmt.Println("")
}
hd, err := os.UserHomeDir()
if err != nil {
fmt.Println("The UserHomeDir is not defined, setting the default TM Home PATH to \"~/.tendermint\"")
defaultTMHome = "~/.tendermint"
} else {
defaultTMHome = fmt.Sprintf("%s/.tendermint", hd)
}
runCmd = flag.NewFlagSet("run", flag.ExitOnError)
runCmd.IntVar(&flagAcceptRetries,
"accept-retries",
defaultAcceptRetries,
"The number of attempts to listen for incoming connections")
runCmd.StringVar(&flagBindAddr, "addr", defaultBindAddr, "Bind to this address for the testing")
runCmd.StringVar(&flagTMHome, "tmhome", defaultTMHome, "Path to the Tendermint home directory")
runCmd.Usage = func() {
fmt.Println(`Runs the remote signer test harness for Tendermint.
Usage:
tm-signer-harness run [flags]
Flags:`)
runCmd.PrintDefaults()
fmt.Println("")
}
extractKeyCmd = flag.NewFlagSet("extract_key", flag.ExitOnError)
extractKeyCmd.StringVar(&flagKeyOutputPath,
"output",
defaultExtractKeyOutput,
"Path to which signing key should be written")
extractKeyCmd.StringVar(&flagTMHome, "tmhome", defaultTMHome, "Path to the Tendermint home directory")
extractKeyCmd.Usage = func() {
fmt.Println(`Extracts a signing key from a local Tendermint instance for use in the remote
signer under test.
Usage:
tm-signer-harness extract_key [flags]
Flags:`)
extractKeyCmd.PrintDefaults()
fmt.Println("")
}
versionCmd = flag.NewFlagSet("version", flag.ExitOnError)
versionCmd.Usage = func() {
fmt.Println(`
Prints the Tendermint version for which this remote signer harness was built.
Usage:
tm-signer-harness version`)
fmt.Println("")
}
}
func runTestHarness(ctx context.Context, acceptRetries int, bindAddr, tmhome string) {
tmhome = internal.ExpandPath(tmhome)
cfg := internal.TestHarnessConfig{
BindAddr: bindAddr,
KeyFile: filepath.Join(tmhome, "config", "priv_validator_key.json"),
StateFile: filepath.Join(tmhome, "data", "priv_validator_state.json"),
GenesisFile: filepath.Join(tmhome, "config", "genesis.json"),
AcceptDeadline: time.Duration(defaultAcceptDeadline) * time.Second,
AcceptRetries: acceptRetries,
ConnDeadline: time.Duration(defaultConnDeadline) * time.Second,
SecretConnKey: ed25519.GenPrivKey(),
ExitWhenComplete: true,
}
harness, err := internal.NewTestHarness(ctx, logger, cfg)
if err != nil {
logger.Error(err.Error())
if therr, ok := err.(*internal.TestHarnessError); ok {
os.Exit(therr.Code)
}
os.Exit(internal.ErrOther)
}
harness.Run()
}
func extractKey(tmhome, outputPath string) {
keyFile := filepath.Join(internal.ExpandPath(tmhome), "config", "priv_validator_key.json")
stateFile := filepath.Join(internal.ExpandPath(tmhome), "data", "priv_validator_state.json")
fpv, err := privval.LoadFilePV(keyFile, stateFile)
if err != nil {
logger.Error("Can't load file pv", "err", err)
os.Exit(1)
}
pkb := []byte(fpv.Key.PrivKey.(ed25519.PrivKey))
if err := os.WriteFile(internal.ExpandPath(outputPath), pkb[:32], 0600); err != nil {
logger.Info("Failed to write private key", "output", outputPath, "err", err)
os.Exit(1)
}
logger.Info("Successfully wrote private key", "output", outputPath)
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := rootCmd.Parse(os.Args[1:]); err != nil {
fmt.Printf("Error parsing flags: %v\n", err)
os.Exit(1)
}
if rootCmd.NArg() == 0 || (rootCmd.NArg() == 1 && rootCmd.Arg(0) == "help") {
rootCmd.Usage()
os.Exit(0)
}
switch rootCmd.Arg(0) {
case "help":
switch rootCmd.Arg(1) {
case "run":
runCmd.Usage()
case "extract_key":
extractKeyCmd.Usage()
case "version":
versionCmd.Usage()
default:
fmt.Printf("Unrecognized command: %s\n", rootCmd.Arg(1))
os.Exit(1)
}
case "run":
if err := runCmd.Parse(os.Args[2:]); err != nil {
fmt.Printf("Error parsing flags: %v\n", err)
os.Exit(1)
}
runTestHarness(ctx, flagAcceptRetries, flagBindAddr, flagTMHome)
case "extract_key":
if err := extractKeyCmd.Parse(os.Args[2:]); err != nil {
fmt.Printf("Error parsing flags: %v\n", err)
os.Exit(1)
}
extractKey(flagTMHome, flagKeyOutputPath)
case "version":
fmt.Println(version.TMVersion)
default:
fmt.Printf("Unrecognized command: %s\n", flag.Arg(0))
os.Exit(1)
}
}

View File

@@ -56,21 +56,26 @@ func TestGenesisBad(t *testing.T) {
}
}
func TestGenesisGood(t *testing.T) {
func TestBasicGenesisDoc(t *testing.T) {
// test a good one by raw json
genDocBytes := []byte(
`{
"genesis_time": "0001-01-01T00:00:00Z",
"chain_id": "test-chain-QDKdJr",
"initial_height": "1000",
"consensus_params": null,
"validators": [{
"pub_key":{"type":"tendermint/PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="},
"power":"10",
"name":""
}],
"app_hash":"",
"app_state":{"account_owner": "Bob"}
"app_state":{"account_owner": "Bob"},
"consensus_params": {
"timing": {"precision": "1", "message_delay": "10"},
"validator": {"pub_key_types":["ed25519"]},
"block": {"max_bytes": "100"},
"evidence": {"max_age_num_blocks": "100", "max_age_duration": "10"}
}
}`,
)
_, err := GenesisDocFromJSON(genDocBytes)
@@ -97,12 +102,12 @@ func TestGenesisGood(t *testing.T) {
genDocBytes, err = tmjson.Marshal(genDoc)
assert.NoError(t, err, "error marshaling genDoc")
genDoc, err = GenesisDocFromJSON(genDocBytes)
assert.NoError(t, err, "expected no error for valid genDoc json")
require.NoError(t, err, "expected no error for valid genDoc json")
// test with invalid consensus params
genDoc.ConsensusParams.Block.MaxBytes = 0
genDocBytes, err = tmjson.Marshal(genDoc)
assert.NoError(t, err, "error marshaling genDoc")
require.NoError(t, err, "error marshaling genDoc")
_, err = GenesisDocFromJSON(genDocBytes)
assert.Error(t, err, "expected error for genDoc json with block size of 0")

View File

@@ -41,7 +41,7 @@ type ConsensusParams struct {
Evidence EvidenceParams `json:"evidence"`
Validator ValidatorParams `json:"validator"`
Version VersionParams `json:"version"`
Timestamp TimestampParams `json:"timestamp"`
Timing TimingParams `json:"timing"`
}
// HashedParams is a subset of ConsensusParams.
@@ -76,12 +76,11 @@ type VersionParams struct {
AppVersion uint64 `json:"app_version"`
}
// TimestampParams influence the validity of block timestamps.
// TimingParams influence the validity of block timestamps.
// TODO (@wbanfield): add link to proposer-based timestamp spec when completed.
type TimestampParams struct {
Precision time.Duration `json:"precision"`
Accuracy time.Duration `json:"accuracy"`
MsgDelay time.Duration `json:"msg_delay"`
type TimingParams struct {
Precision time.Duration `json:"precision"`
MessageDelay time.Duration `json:"message_delay"`
}
// DefaultConsensusParams returns a default ConsensusParams.
@@ -91,6 +90,7 @@ func DefaultConsensusParams() *ConsensusParams {
Evidence: DefaultEvidenceParams(),
Validator: DefaultValidatorParams(),
Version: DefaultVersionParams(),
Timing: DefaultTimingParams(),
}
}
@@ -125,13 +125,12 @@ func DefaultVersionParams() VersionParams {
}
}
func DefaultTimestampParams() TimestampParams {
func DefaultTimingParams() TimingParams {
// TODO(@wbanfield): Determine experimental values for these defaults
// https://github.com/tendermint/tendermint/issues/7202
return TimestampParams{
Precision: 2 * time.Second,
Accuracy: 500 * time.Millisecond,
MsgDelay: 3 * time.Second,
return TimingParams{
Precision: 10 * time.Millisecond,
MessageDelay: 500 * time.Millisecond,
}
}
@@ -167,7 +166,7 @@ func (params ConsensusParams) ValidateConsensusParams() error {
}
if params.Evidence.MaxAgeDuration <= 0 {
return fmt.Errorf("evidence.MaxAgeDuration must be grater than 0 if provided, Got %v",
return fmt.Errorf("evidence.MaxAgeDuration must be greater than 0 if provided, Got %v",
params.Evidence.MaxAgeDuration)
}
@@ -181,6 +180,16 @@ func (params ConsensusParams) ValidateConsensusParams() error {
params.Evidence.MaxBytes)
}
if params.Timing.MessageDelay <= 0 {
return fmt.Errorf("timing.MessageDelay must be greater than 0. Got: %d",
params.Timing.MessageDelay)
}
if params.Timing.Precision <= 0 {
return fmt.Errorf("timing.Precision must be greater than 0. Got: %d",
params.Timing.Precision)
}
if len(params.Validator.PubKeyTypes) == 0 {
return errors.New("len(Validator.PubKeyTypes) must be greater than 0")
}
@@ -224,6 +233,8 @@ func (params ConsensusParams) HashConsensusParams() []byte {
func (params *ConsensusParams) Equals(params2 *ConsensusParams) bool {
return params.Block == params2.Block &&
params.Evidence == params2.Evidence &&
params.Version == params2.Version &&
params.Timing == params2.Timing &&
tmstrings.StringSliceEqual(params.Validator.PubKeyTypes, params2.Validator.PubKeyTypes)
}
@@ -254,6 +265,10 @@ func (params ConsensusParams) UpdateConsensusParams(params2 *tmproto.ConsensusPa
if params2.Version != nil {
res.Version.AppVersion = params2.Version.AppVersion
}
if params2.Timing != nil {
res.Timing.Precision = params2.Timing.Precision
res.Timing.MessageDelay = params2.Timing.MessageDelay
}
return res
}
@@ -274,6 +289,10 @@ func (params *ConsensusParams) ToProto() tmproto.ConsensusParams {
Version: &tmproto.VersionParams{
AppVersion: params.Version.AppVersion,
},
Timing: &tmproto.TimingParams{
MessageDelay: params.Timing.MessageDelay,
Precision: params.Timing.Precision,
},
}
}
@@ -294,5 +313,9 @@ func ConsensusParamsFromProto(pbParams tmproto.ConsensusParams) ConsensusParams
Version: VersionParams{
AppVersion: pbParams.Version.AppVersion,
},
Timing: TimingParams{
MessageDelay: pbParams.Timing.MessageDelay,
Precision: pbParams.Timing.Precision,
},
}
}

View File

@@ -23,23 +23,140 @@ func TestConsensusParamsValidation(t *testing.T) {
valid bool
}{
// test block params
0: {makeParams(1, 0, 2, 0, valEd25519), true},
1: {makeParams(0, 0, 2, 0, valEd25519), false},
2: {makeParams(47*1024*1024, 0, 2, 0, valEd25519), true},
3: {makeParams(10, 0, 2, 0, valEd25519), true},
4: {makeParams(100*1024*1024, 0, 2, 0, valEd25519), true},
5: {makeParams(101*1024*1024, 0, 2, 0, valEd25519), false},
6: {makeParams(1024*1024*1024, 0, 2, 0, valEd25519), false},
7: {makeParams(1024*1024*1024, 0, -1, 0, valEd25519), false},
{
params: makeParams(makeParamsArgs{
blockBytes: 1,
evidenceAge: 2,
precision: 1,
messageDelay: 1}),
valid: true,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 0,
evidenceAge: 2,
precision: 1,
messageDelay: 1}),
valid: false,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 47 * 1024 * 1024,
evidenceAge: 2,
precision: 1,
messageDelay: 1}),
valid: true,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 10,
evidenceAge: 2,
precision: 1,
messageDelay: 1}),
valid: true,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 100 * 1024 * 1024,
evidenceAge: 2,
precision: 1,
messageDelay: 1}),
valid: true,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 101 * 1024 * 1024,
evidenceAge: 2,
precision: 1,
messageDelay: 1}),
valid: false,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 1024 * 1024 * 1024,
evidenceAge: 2,
precision: 1,
messageDelay: 1}),
valid: false,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 1024 * 1024 * 1024,
evidenceAge: 2,
precision: 1,
messageDelay: 1}),
valid: false,
},
// test evidence params
8: {makeParams(1, 0, 0, 0, valEd25519), false},
9: {makeParams(1, 0, 2, 2, valEd25519), false},
10: {makeParams(1000, 0, 2, 1, valEd25519), true},
11: {makeParams(1, 0, -1, 0, valEd25519), false},
{
params: makeParams(makeParamsArgs{
blockBytes: 1,
evidenceAge: 0,
maxEvidenceBytes: 0,
precision: 1,
messageDelay: 1}),
valid: false,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 1,
evidenceAge: 2,
maxEvidenceBytes: 2,
precision: 1,
messageDelay: 1}),
valid: false,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 1000,
evidenceAge: 2,
maxEvidenceBytes: 1,
precision: 1,
messageDelay: 1}),
valid: true,
},
{
params: makeParams(makeParamsArgs{
blockBytes: 1,
evidenceAge: -1,
maxEvidenceBytes: 0,
precision: 1,
messageDelay: 1}),
valid: false,
},
// test no pubkey type provided
12: {makeParams(1, 0, 2, 0, []string{}), false},
{
params: makeParams(makeParamsArgs{
evidenceAge: 2,
pubkeyTypes: []string{},
precision: 1,
messageDelay: 1}),
valid: false,
},
// test invalid pubkey type provided
13: {makeParams(1, 0, 2, 0, []string{"potatoes make good pubkeys"}), false},
{
params: makeParams(makeParamsArgs{
evidenceAge: 2,
pubkeyTypes: []string{"potatoes make good pubkeys"},
precision: 1,
messageDelay: 1}),
valid: false,
},
// test invalid pubkey type provided
{
params: makeParams(makeParamsArgs{
evidenceAge: 2,
precision: 1,
messageDelay: -1}),
valid: false,
},
{
params: makeParams(makeParamsArgs{
evidenceAge: 2,
precision: -1,
messageDelay: 1}),
valid: false,
},
}
for i, tc := range testCases {
if tc.valid {
@@ -50,38 +167,51 @@ func TestConsensusParamsValidation(t *testing.T) {
}
}
func makeParams(
blockBytes, blockGas int64,
evidenceAge int64,
maxEvidenceBytes int64,
pubkeyTypes []string,
) ConsensusParams {
type makeParamsArgs struct {
blockBytes int64
blockGas int64
evidenceAge int64
maxEvidenceBytes int64
pubkeyTypes []string
precision time.Duration
messageDelay time.Duration
}
func makeParams(args makeParamsArgs) ConsensusParams {
if args.pubkeyTypes == nil {
args.pubkeyTypes = valEd25519
}
return ConsensusParams{
Block: BlockParams{
MaxBytes: blockBytes,
MaxGas: blockGas,
MaxBytes: args.blockBytes,
MaxGas: args.blockGas,
},
Evidence: EvidenceParams{
MaxAgeNumBlocks: evidenceAge,
MaxAgeDuration: time.Duration(evidenceAge),
MaxBytes: maxEvidenceBytes,
MaxAgeNumBlocks: args.evidenceAge,
MaxAgeDuration: time.Duration(args.evidenceAge),
MaxBytes: args.maxEvidenceBytes,
},
Validator: ValidatorParams{
PubKeyTypes: pubkeyTypes,
PubKeyTypes: args.pubkeyTypes,
},
Timing: TimingParams{
Precision: args.precision,
MessageDelay: args.messageDelay,
},
}
}
func TestConsensusParamsHash(t *testing.T) {
params := []ConsensusParams{
makeParams(4, 2, 3, 1, valEd25519),
makeParams(1, 4, 3, 1, valEd25519),
makeParams(1, 2, 4, 1, valEd25519),
makeParams(2, 5, 7, 1, valEd25519),
makeParams(1, 7, 6, 1, valEd25519),
makeParams(9, 5, 4, 1, valEd25519),
makeParams(7, 8, 9, 1, valEd25519),
makeParams(4, 6, 5, 1, valEd25519),
makeParams(makeParamsArgs{blockBytes: 4, blockGas: 2, evidenceAge: 3, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 1, blockGas: 4, evidenceAge: 3, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 1, blockGas: 2, evidenceAge: 4, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 2, blockGas: 5, evidenceAge: 7, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 1, blockGas: 7, evidenceAge: 6, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 9, blockGas: 5, evidenceAge: 4, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 7, blockGas: 8, evidenceAge: 9, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 4, blockGas: 6, evidenceAge: 5, maxEvidenceBytes: 1}),
}
hashes := make([][]byte, len(params))
@@ -101,20 +231,31 @@ func TestConsensusParamsHash(t *testing.T) {
func TestConsensusParamsUpdate(t *testing.T) {
testCases := []struct {
params ConsensusParams
intialParams ConsensusParams
updates *tmproto.ConsensusParams
updatedParams ConsensusParams
}{
// empty updates
{
makeParams(1, 2, 3, 0, valEd25519),
&tmproto.ConsensusParams{},
makeParams(1, 2, 3, 0, valEd25519),
intialParams: makeParams(makeParamsArgs{blockBytes: 1, blockGas: 2, evidenceAge: 3}),
updates: &tmproto.ConsensusParams{},
updatedParams: makeParams(makeParamsArgs{blockBytes: 1, blockGas: 2, evidenceAge: 3}),
},
{
// update timing params
intialParams: makeParams(makeParamsArgs{evidenceAge: 3, precision: time.Second, messageDelay: 3 * time.Second}),
updates: &tmproto.ConsensusParams{
Timing: &tmproto.TimingParams{
Precision: time.Second * 2,
MessageDelay: time.Second * 4,
},
},
updatedParams: makeParams(makeParamsArgs{evidenceAge: 3, precision: 2 * time.Second, messageDelay: 4 * time.Second}),
},
// fine updates
{
makeParams(1, 2, 3, 0, valEd25519),
&tmproto.ConsensusParams{
intialParams: makeParams(makeParamsArgs{blockBytes: 1, blockGas: 2, evidenceAge: 3}),
updates: &tmproto.ConsensusParams{
Block: &tmproto.BlockParams{
MaxBytes: 100,
MaxGas: 200,
@@ -128,11 +269,15 @@ func TestConsensusParamsUpdate(t *testing.T) {
PubKeyTypes: valSecp256k1,
},
},
makeParams(100, 200, 300, 50, valSecp256k1),
updatedParams: makeParams(makeParamsArgs{
blockBytes: 100, blockGas: 200,
evidenceAge: 300,
maxEvidenceBytes: 50,
pubkeyTypes: valSecp256k1}),
},
{
makeParams(1, 2, 3, 0, valEd25519),
&tmproto.ConsensusParams{
intialParams: makeParams(makeParamsArgs{blockBytes: 1, blockGas: 2, evidenceAge: 3}),
updates: &tmproto.ConsensusParams{
Block: &tmproto.BlockParams{
MaxBytes: 100,
MaxGas: 200,
@@ -145,17 +290,23 @@ func TestConsensusParamsUpdate(t *testing.T) {
Validator: &tmproto.ValidatorParams{
PubKeyTypes: valSr25519,
},
}, makeParams(100, 200, 300, 50, valSr25519),
},
updatedParams: makeParams(makeParamsArgs{
blockBytes: 100,
blockGas: 200,
evidenceAge: 300,
maxEvidenceBytes: 50,
pubkeyTypes: valSr25519}),
},
}
for _, tc := range testCases {
assert.Equal(t, tc.updatedParams, tc.params.UpdateConsensusParams(tc.updates))
assert.Equal(t, tc.updatedParams, tc.intialParams.UpdateConsensusParams(tc.updates))
}
}
func TestConsensusParamsUpdate_AppVersion(t *testing.T) {
params := makeParams(1, 2, 3, 0, valEd25519)
params := makeParams(makeParamsArgs{blockBytes: 1, blockGas: 2, evidenceAge: 3})
assert.EqualValues(t, 0, params.Version.AppVersion)
@@ -167,14 +318,16 @@ func TestConsensusParamsUpdate_AppVersion(t *testing.T) {
func TestProto(t *testing.T) {
params := []ConsensusParams{
makeParams(4, 2, 3, 1, valEd25519),
makeParams(1, 4, 3, 1, valEd25519),
makeParams(1, 2, 4, 1, valEd25519),
makeParams(2, 5, 7, 1, valEd25519),
makeParams(1, 7, 6, 1, valEd25519),
makeParams(9, 5, 4, 1, valEd25519),
makeParams(7, 8, 9, 1, valEd25519),
makeParams(4, 6, 5, 1, valEd25519),
makeParams(makeParamsArgs{blockBytes: 4, blockGas: 2, evidenceAge: 3, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 1, blockGas: 4, evidenceAge: 3, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 1, blockGas: 2, evidenceAge: 4, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 2, blockGas: 5, evidenceAge: 7, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 1, blockGas: 7, evidenceAge: 6, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 9, blockGas: 5, evidenceAge: 4, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 7, blockGas: 8, evidenceAge: 9, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{blockBytes: 4, blockGas: 6, evidenceAge: 5, maxEvidenceBytes: 1}),
makeParams(makeParamsArgs{precision: time.Second, messageDelay: time.Minute}),
makeParams(makeParamsArgs{precision: time.Nanosecond, messageDelay: time.Millisecond}),
}
for i := range params {

View File

@@ -34,14 +34,14 @@ type Proposal struct {
// NewProposal returns a new Proposal.
// If there is no POLRound, polRound should be -1.
func NewProposal(height int64, round int32, polRound int32, blockID BlockID) *Proposal {
func NewProposal(height int64, round int32, polRound int32, blockID BlockID, ts time.Time) *Proposal {
return &Proposal{
Type: tmproto.ProposalType,
Height: height,
Round: round,
BlockID: blockID,
POLRound: polRound,
Timestamp: tmtime.Now(),
Timestamp: tmtime.Canonical(ts),
}
}
@@ -84,18 +84,24 @@ func (p *Proposal) ValidateBasic() error {
// configured Precision and MsgDelay parameters.
// Specifically, a proposed block timestamp is considered timely if it is satisfies the following inequalities:
//
// proposedBlockTime > validatorLocaltime - Precision && proposedBlockTime < validatorLocalTime + Precision + MsgDelay.
// localtime >= proposedBlockTime - Precision
// localtime <= proposedBlockTime + MsgDelay + Precision
//
// Note: If the proposal is for the `initialHeight` the second inequality is not checked. This is because
// the timestamp in this case is set to the preconfigured genesis time.
// For more information on the meaning of 'timely', see the proposer-based timestamp specification:
// https://github.com/tendermint/spec/tree/master/spec/consensus/proposer-based-timestamp
func (p *Proposal) IsTimely(clock tmtime.Source, tp TimestampParams) bool {
lt := clock.Now()
lhs := lt.Add(-tp.Precision)
rhs := lt.Add(tp.Precision).Add(tp.MsgDelay)
if lhs.Before(p.Timestamp) && rhs.After(p.Timestamp) {
return true
func (p *Proposal) IsTimely(recvTime time.Time, tp TimingParams, initialHeight int64) bool {
// lhs is `proposedBlockTime - Precision` in the first inequality
lhs := p.Timestamp.Add(-tp.Precision)
// rhs is `proposedBlockTime + MsgDelay + Precision` in the second inequality
rhs := p.Timestamp.Add(tp.MessageDelay).Add(tp.Precision)
if recvTime.Before(lhs) || (p.Height != initialHeight && recvTime.After(rhs)) {
return false
}
return false
return true
}
// String returns a string representation of the Proposal.

View File

@@ -13,7 +13,7 @@ import (
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/tendermint/tendermint/internal/libs/protoio"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmtimemocks "github.com/tendermint/tendermint/libs/time/mocks"
tmtime "github.com/tendermint/tendermint/libs/time"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
@@ -63,7 +63,7 @@ func TestProposalVerifySignature(t *testing.T) {
prop := NewProposal(
4, 2, 2,
BlockID{tmrand.Bytes(tmhash.Size), PartSetHeader{777, tmrand.Bytes(tmhash.Size)}})
BlockID{tmrand.Bytes(tmhash.Size), PartSetHeader{777, tmrand.Bytes(tmhash.Size)}}, tmtime.Now())
p := prop.ToProto()
signBytes := ProposalSignBytes("test_chain_id", p)
@@ -154,7 +154,7 @@ func TestProposalValidateBasic(t *testing.T) {
t.Run(tc.testName, func(t *testing.T) {
prop := NewProposal(
4, 2, 2,
blockID)
blockID, tmtime.Now())
p := prop.ToProto()
err := privVal.SignProposal(context.Background(), "test_chain_id", p)
prop.Signature = p.Signature
@@ -166,9 +166,9 @@ func TestProposalValidateBasic(t *testing.T) {
}
func TestProposalProtoBuf(t *testing.T) {
proposal := NewProposal(1, 2, 3, makeBlockID([]byte("hash"), 2, []byte("part_set_hash")))
proposal := NewProposal(1, 2, 3, makeBlockID([]byte("hash"), 2, []byte("part_set_hash")), tmtime.Now())
proposal.Signature = []byte("sig")
proposal2 := NewProposal(1, 2, 3, BlockID{})
proposal2 := NewProposal(1, 2, 3, BlockID{}, tmtime.Now())
testCases := []struct {
msg string
@@ -197,59 +197,93 @@ func TestIsTimely(t *testing.T) {
genesisTime, err := time.Parse(time.RFC3339, "2019-03-13T23:00:00Z")
require.NoError(t, err)
testCases := []struct {
name string
proposalTime time.Time
localTime time.Time
precision time.Duration
msgDelay time.Duration
expectTimely bool
name string
genesisHeight int64
proposalHeight int64
proposalTime time.Time
recvTime time.Time
precision time.Duration
msgDelay time.Duration
expectTimely bool
}{
// proposalTime - precision <= localTime <= proposalTime + msgDelay + precision
{
// Checking that the following inequality evaluates to true:
// 1 - 2 < 0 < 1 + 2 + 1
name: "basic timely",
proposalTime: genesisTime,
localTime: genesisTime.Add(1 * time.Nanosecond),
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: true,
// 0 - 2 <= 1 <= 0 + 1 + 2
name: "basic timely",
genesisHeight: 1,
proposalHeight: 2,
proposalTime: genesisTime,
recvTime: genesisTime.Add(1 * time.Nanosecond),
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: true,
},
{
// Checking that the following inequality evaluates to false:
// 3 - 2 < 0 < 3 + 2 + 1
name: "local time too large",
proposalTime: genesisTime,
localTime: genesisTime.Add(3 * time.Nanosecond),
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: false,
// 0 - 2 <= 4 <= 0 + 1 + 2
name: "local time too large",
genesisHeight: 1,
proposalHeight: 2,
proposalTime: genesisTime,
recvTime: genesisTime.Add(4 * time.Nanosecond),
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: false,
},
{
// Checking that the following inequality evaluates to false:
// 0 - 2 < 2 < 2 + 1
name: "proposal time too large",
proposalTime: genesisTime.Add(4 * time.Nanosecond),
localTime: genesisTime,
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: false,
// 4 - 2 <= 0 <= 4 + 2 + 1
name: "proposal time too large",
genesisHeight: 1,
proposalHeight: 2,
proposalTime: genesisTime.Add(4 * time.Nanosecond),
recvTime: genesisTime,
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: false,
},
{
// Checking that the following inequality evaluates to true:
// 0 - 2 <= 4
// and the following check is skipped
// 4 <= 0 + 1 + 2
name: "local time too large but proposal is for genesis",
genesisHeight: 1,
proposalHeight: 1,
proposalTime: genesisTime,
recvTime: genesisTime.Add(4 * time.Nanosecond),
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: true,
},
{
// Checking that the following inequality evaluates to false:
// 4 - 2 <= 0
name: "proposal time too large for genesis block proposal",
genesisHeight: 1,
proposalHeight: 1,
proposalTime: genesisTime.Add(4 * time.Nanosecond),
recvTime: genesisTime,
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
p := Proposal{
Height: testCase.proposalHeight,
Timestamp: testCase.proposalTime,
}
tp := TimestampParams{
Precision: testCase.precision,
MsgDelay: testCase.msgDelay,
tp := TimingParams{
Precision: testCase.precision,
MessageDelay: testCase.msgDelay,
}
mockSource := new(tmtimemocks.Source)
mockSource.On("Now").Return(testCase.localTime)
ti := p.IsTimely(mockSource, tp)
ti := p.IsTimely(testCase.recvTime, tp, testCase.genesisHeight)
assert.Equal(t, testCase.expectTimely, ti)
})
}