Compare commits

..

8 Commits

Author SHA1 Message Date
William Banfield
017d45f8e3 base synchrony on amount of voting power observed on the network from proposals 2021-12-16 16:12:48 -05: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
William Banfield
e4598b1de1 internal/consensus: remove proposal wait time (#7418) 2021-12-09 17:18:41 -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
30 changed files with 1293 additions and 2532 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": "200",
"precision": "50"
},
"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

@@ -248,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)
@@ -544,245 +544,210 @@ func makeState(ctx context.Context, cfg *config.Config, logger log.Logger, nVali
//-------------------------------------------------------------------------------
type eventChecker struct {
ctx context.Context
t *testing.T
ch <-chan tmpubsub.Message
timeout time.Duration
}
func (ec eventChecker) ensureMatchingProposal(height int64, round int32, propID types.BlockID) {
ec.t.Helper()
func ensureNoMessageBeforeTimeout(t *testing.T, ch <-chan tmpubsub.Message, timeout time.Duration,
errorMessage string) {
t.Helper()
select {
case <-time.After(ensureTimeout):
ec.t.Fatalf("Timeout expired while waiting for NewProposal event")
case msg := <-ec.ch:
proposalEvent, ok := msg.Data().(types.EventDataCompleteProposal)
if !ok {
ec.t.Fatalf("expected a EventDataCompleteProposal, got %T. Wrong subscription channel?",
msg.Data())
}
if proposalEvent.Height != height {
ec.t.Fatalf("expected height %v, got %v", height, proposalEvent.Height)
}
if proposalEvent.Round != round {
ec.t.Fatalf("expected round %v, got %v", round, proposalEvent.Round)
}
if !proposalEvent.BlockID.Equals(propID) {
ec.t.Fatalf("Proposed block does not match expected block (%v != %v)", proposalEvent.BlockID, propID)
}
}
}
func (ec eventChecker) ensureNewProposal(height int64, round int32) {
ec.t.Helper()
to := ec.timeout
if to == 0 {
to = ensureTimeout
}
select {
case <-time.After(to):
ec.t.Fatalf("Timeout expired while waiting for NewProposal event")
case msg := <-ec.ch:
proposalEvent, ok := msg.Data().(types.EventDataCompleteProposal)
if !ok {
ec.t.Fatalf("expected a EventDataCompleteProposal, got %T. Wrong subscription channel?",
msg.Data())
}
if proposalEvent.Height != height {
ec.t.Fatalf("expected height %v, got %v", height, proposalEvent.Height)
}
if proposalEvent.Round != round {
ec.t.Fatalf("expected round %v, got %v", round, proposalEvent.Round)
}
}
}
func (ec eventChecker) ensureNewRound(height int64, round int32) {
ec.t.Helper()
to := ec.timeout
if to == 0 {
to = ensureTimeout
}
select {
case <-time.After(to):
ec.t.Fatal("Timeout expired while waiting for NewRound event")
case msg := <-ec.ch:
newRoundEvent, ok := msg.Data().(types.EventDataNewRound)
if !ok {
ec.t.Fatalf("expected a EventDataNewRound, got %T. Wrong subscription channel?", msg.Data())
}
if newRoundEvent.Height != height {
ec.t.Fatalf("expected height %v, got %v", height, newRoundEvent.Height)
}
if newRoundEvent.Round != round {
ec.t.Fatalf("expected round %v, got %v", round, newRoundEvent.Round)
}
}
}
func (ec eventChecker) ensureNewEvent() {
ec.t.Helper()
to := ec.timeout
if to == 0 {
to = ensureTimeout
}
select {
case <-time.After(to):
ec.t.Fatal("timeout occured while waiting for event")
case <-ec.ch:
}
}
func (ec eventChecker) ensureNoNewEvent(errorMessage string) {
ec.t.Helper()
to := ec.timeout
if to == 0 {
to = ensureTimeout
}
select {
case <-time.After(to):
case <-time.After(timeout):
break
case <-ec.ch:
ec.t.Fatalf("unexpected event: %s", errorMessage)
case <-ch:
t.Fatalf("unexpected event: %s", errorMessage)
}
}
func (ec eventChecker) ensureNoNewRoundStep() {
ec.t.Helper()
ec.ensureNoNewEvent("We should be stuck waiting, not receiving NewRoundStep event")
func ensureNoNewEventOnChannel(t *testing.T, ch <-chan tmpubsub.Message) {
t.Helper()
ensureNoMessageBeforeTimeout(
t,
ch,
ensureTimeout,
"We should be stuck waiting, not receiving new event on the channel")
}
func (ec eventChecker) ensureNoNewTimeout() {
ec.t.Helper()
ec.ensureNoNewEvent("We should be stuck waiting, not receiving NewTimeout event")
func ensureNoNewRoundStep(t *testing.T, stepCh <-chan tmpubsub.Message) {
t.Helper()
ensureNoMessageBeforeTimeout(
t,
stepCh,
ensureTimeout,
"We should be stuck waiting, not receiving NewRoundStep event")
}
func (ec eventChecker) ensureNewRoundState(height int64, round int32, errorMessage string) { // nolint: lll
ec.t.Helper()
to := ec.timeout
if to == 0 {
to = ensureTimeout
func ensureNoNewTimeout(t *testing.T, stepCh <-chan tmpubsub.Message, timeout int64) {
t.Helper()
timeoutDuration := time.Duration(timeout*10) * time.Nanosecond
ensureNoMessageBeforeTimeout(
t,
stepCh,
timeoutDuration,
"We should be stuck waiting, not receiving NewTimeout event")
}
func ensureNewEvent(t *testing.T, ch <-chan tmpubsub.Message, height int64, round int32, timeout time.Duration, errorMessage string) { // nolint: lll
t.Helper()
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()
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)
}
}
func ensureNewTimeout(t *testing.T, timeoutCh <-chan tmpubsub.Message, height int64, round int32, timeout int64) {
t.Helper()
timeoutDuration := time.Duration(timeout*10) * time.Nanosecond
ensureNewEvent(t, timeoutCh, height, round, timeoutDuration,
"Timeout expired while waiting for NewTimeout event")
}
func ensureNewProposal(t *testing.T, proposalCh <-chan tmpubsub.Message, height int64, round int32) {
t.Helper()
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)
}
}
func ensureNewValidBlock(t *testing.T, validBlockCh <-chan tmpubsub.Message, height int64, round int32) {
t.Helper()
ensureNewEvent(t, validBlockCh, height, round, ensureTimeout,
"Timeout expired while waiting for NewValidBlock event")
}
func ensureNewBlock(t *testing.T, blockCh <-chan tmpubsub.Message, height int64) {
t.Helper()
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()
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())
}
}
func ensureLock(t *testing.T, lockCh <-chan tmpubsub.Message, height int64, round int32) {
t.Helper()
ensureNewEvent(t, lockCh, height, round, ensureTimeout,
"Timeout expired while waiting for LockValue event")
}
func ensureRelock(t *testing.T, relockCh <-chan tmpubsub.Message, height int64, round int32) {
t.Helper()
ensureNewEvent(t, relockCh, height, round, ensureTimeout,
"Timeout expired while waiting for RelockValue event")
}
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()
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 !proposalEvent.BlockID.Equals(propID) {
t.Fatalf("Proposed block does not match expected block (%v != %v)", proposalEvent.BlockID, propID)
}
}
func ensurePrecommit(t *testing.T, voteCh <-chan tmpubsub.Message, height int64, round int32) {
t.Helper()
ensureVote(t, voteCh, height, round, tmproto.PrecommitType)
}
func ensurePrevote(t *testing.T, voteCh <-chan tmpubsub.Message, height int64, round int32) {
t.Helper()
ensureVote(t, voteCh, height, round, tmproto.PrevoteType)
}
func ensureVote(t *testing.T, voteCh <-chan tmpubsub.Message, height int64, round int32,
voteType tmproto.SignedMsgType) {
t.Helper()
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())
}
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(to):
ec.t.Fatalf("timed out waiting for new event: %s", errorMessage)
case msg := <-ec.ch:
roundStateEvent, ok := msg.Data().(types.EventDataRoundState)
if !ok {
ec.t.Fatalf("expected a EventDataRoundState, got %T. Wrong subscription channel?", msg.Data())
}
if roundStateEvent.Height != height {
ec.t.Fatalf("expected height %v, got %v", height, roundStateEvent.Height)
}
if roundStateEvent.Round != round {
ec.t.Fatalf("expected round %v, got %v", round, roundStateEvent.Round)
}
// TODO: We could check also for a step at this point!
}
}
func (ec eventChecker) ensureNewTimeout(height int64, round int32) {
ec.t.Helper()
ec.ensureNewRoundState(height, round, "Timeout expired while waiting for NewTimeout event")
}
func (ec eventChecker) ensureNewValidBlock(height int64, round int32) {
ec.t.Helper()
ec.ensureNewRoundState(height, round, "Timeout expired while waiting for NewValidBlock event")
}
func (ec eventChecker) ensureNewBlock(height int64) {
ec.t.Helper()
to := ec.timeout
if to == 0 {
to = ensureTimeout
}
select {
case <-time.After(to):
ec.t.Fatalf("Timeout expired while waiting for NewBlock event")
case msg := <-ec.ch:
blockEvent, ok := msg.Data().(types.EventDataNewBlock)
if !ok {
ec.t.Fatalf("expected a EventDataNewBlock, got %T. Wrong subscription channel?",
msg.Data())
}
if blockEvent.Block.Height != height {
ec.t.Fatalf("expected height %v, got %v", height, blockEvent.Block.Height)
}
}
}
func (ec eventChecker) ensureNewBlockHeader(height int64, blockHash tmbytes.HexBytes) {
ec.t.Helper()
to := ec.timeout
if to == 0 {
to = ensureTimeout
}
select {
case <-time.After(to):
ec.t.Fatalf("Timeout expired while waiting for NewBlockHeader event")
case msg := <-ec.ch:
blockHeaderEvent, ok := msg.Data().(types.EventDataNewBlockHeader)
if !ok {
ec.t.Fatalf("expected a EventDataNewBlockHeader, got %T. Wrong subscription channel?",
msg.Data())
}
if blockHeaderEvent.Header.Height != height {
ec.t.Fatalf("expected height %v, got %v", height, blockHeaderEvent.Header.Height)
}
if !bytes.Equal(blockHeaderEvent.Header.Hash(), blockHash) {
ec.t.Fatalf("expected header %X, got %X", blockHash, blockHeaderEvent.Header.Hash())
}
}
}
func (ec eventChecker) ensureLock(height int64, round int32) {
ec.t.Helper()
ec.ensureNewRoundState(height, round, "Timeout expired while waiting for LockValue event")
}
func (ec eventChecker) ensureRelock(height int64, round int32) {
ec.t.Helper()
ec.ensureNewRoundState(height, round, "Timeout expired while waiting for RelockValue event")
}
func (ec eventChecker) ensurePrecommit(height int64, round int32) {
ec.t.Helper()
ec.ensureVote(height, round, tmproto.PrecommitType)
}
func (ec eventChecker) ensurePrevote(height int64, round int32) {
ec.t.Helper()
ec.ensureVote(height, round, tmproto.PrevoteType)
}
func (ec eventChecker) ensureVote(height int64, round int32, voteType tmproto.SignedMsgType) {
ec.t.Helper()
to := ec.timeout
if to == 0 {
to = ensureTimeout
}
select {
case <-time.After(to):
ec.t.Fatalf("Timeout expired while waiting for NewVote event")
case msg := <-ec.ch:
voteEvent, ok := msg.Data().(types.EventDataVote)
if !ok {
ec.t.Fatalf("expected a EventDataVote, got %T. Wrong subscription channel?",
msg.Data())
}
vote := voteEvent.Vote
if vote.Height != height {
ec.t.Fatalf("expected height %v, got %v", height, vote.Height)
}
if vote.Round != round {
ec.t.Fatalf("expected round %v, got %v", round, vote.Round)
}
if vote.Type != voteType {
ec.t.Fatalf("expected type %v, got %v", voteType, vote.Type)
}
t.Fatalf("Timeout expired while waiting for message")
case msg := <-ch:
return msg
}
panic("unreachable")
}
//-------------------------------------------------------------------------------

View File

@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
dbm "github.com/tendermint/tm-db"
"github.com/tendermint/tendermint/abci/example/code"
abci "github.com/tendermint/tendermint/abci/types"
@@ -18,7 +19,6 @@ import (
"github.com/tendermint/tendermint/internal/store"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/types"
dbm "github.com/tendermint/tm-db"
)
// for testing
@@ -43,19 +43,15 @@ func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) {
cs := newStateWithConfig(ctx, log.TestingLogger(), config, state, privVals[0], NewCounterApplication())
assertMempool(cs.txNotifier).EnableTxsAvailable()
height, round := cs.Height, cs.Round
blockChecker := eventChecker{
ctx: ctx,
t: t,
ch: subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock),
}
newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock)
startTestRound(ctx, cs, height, round)
blockChecker.ensureNewBlock(height) // first block gets committed
blockChecker.ensureNoNewEvent("unexpected block event")
ensureNewEventOnChannel(t, newBlockCh) // first block gets committed
ensureNoNewEventOnChannel(t, newBlockCh)
deliverTxsRange(ctx, cs, 0, 1)
blockChecker.ensureNewEvent() // commit txs
blockChecker.ensureNewEvent() // commit updated app hash
blockChecker.ensureNoNewEvent("unexpected block event")
ensureNewEventOnChannel(t, newBlockCh) // commit txs
ensureNewEventOnChannel(t, newBlockCh) // commit updated app hash
ensureNoNewEventOnChannel(t, newBlockCh)
}
func TestMempoolProgressAfterCreateEmptyBlocksInterval(t *testing.T) {
@@ -75,16 +71,12 @@ func TestMempoolProgressAfterCreateEmptyBlocksInterval(t *testing.T) {
assertMempool(cs.txNotifier).EnableTxsAvailable()
blockChecker := eventChecker{
ctx: ctx,
t: t,
ch: subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock),
}
newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock)
startTestRound(ctx, cs, cs.Height, cs.Round)
blockChecker.ensureNewEvent() // first block gets committed
blockChecker.ensureNoNewEvent("unexpected block") // then we dont make a block ...
blockChecker.ensureNewEvent() // until the CreateEmptyBlocksInterval has passed
ensureNewEventOnChannel(t, newBlockCh) // first block gets committed
ensureNoNewEventOnChannel(t, newBlockCh) // then we dont make a block ...
ensureNewEventOnChannel(t, newBlockCh) // until the CreateEmptyBlocksInterval has passed
}
func TestMempoolProgressInHigherRound(t *testing.T) {
@@ -103,21 +95,9 @@ func TestMempoolProgressInHigherRound(t *testing.T) {
cs := newStateWithConfig(ctx, log.TestingLogger(), config, state, privVals[0], NewCounterApplication())
assertMempool(cs.txNotifier).EnableTxsAvailable()
height, round := cs.Height, cs.Round
blockChecker := eventChecker{
ctx: ctx,
t: t,
ch: subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock),
}
roundChecker := eventChecker{
ctx: ctx,
t: t,
ch: subscribe(ctx, t, cs.eventBus, types.EventQueryNewRound),
}
proposalTimeoutChecker := eventChecker{
ctx: ctx,
t: t,
ch: subscribe(ctx, t, cs.eventBus, types.EventQueryTimeoutPropose),
}
newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock)
newRoundCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewRound)
timeoutCh := subscribe(ctx, t, cs.eventBus, types.EventQueryTimeoutPropose)
cs.setProposal = func(proposal *types.Proposal) error {
if cs.Height == 2 && cs.Round == 0 {
// dont set the proposal in round 0 so we timeout and
@@ -129,19 +109,19 @@ func TestMempoolProgressInHigherRound(t *testing.T) {
}
startTestRound(ctx, cs, height, round)
roundChecker.ensureNewRound(height, round) // first round at first height
blockChecker.ensureNewEvent() // first block gets committed
ensureNewRound(t, newRoundCh, height, round) // first round at first height
ensureNewEventOnChannel(t, newBlockCh) // first block gets committed
height++ // moving to the next height
round = 0
roundChecker.ensureNewRound(height, round) // first round at next height
deliverTxsRange(ctx, cs, 0, 1) // we deliver txs, but dont set a proposal so we get the next round
proposalTimeoutChecker.ensureNewTimeout(height, round)
ensureNewRound(t, newRoundCh, height, round) // first round at next height
deliverTxsRange(ctx, cs, 0, 1) // we deliver txs, but dont set a proposal so we get the next round
ensureNewTimeout(t, timeoutCh, height, round, cs.config.TimeoutPropose.Nanoseconds())
round++ // moving to the next round
roundChecker.ensureNewRound(height, round) // wait for the next round
blockChecker.ensureNewEvent() // now we can commit the block
round++ // moving to the next round
ensureNewRound(t, newRoundCh, height, round) // wait for the next round
ensureNewEventOnChannel(t, newBlockCh) // now we can commit the block
}
func deliverTxsRange(ctx context.Context, cs *State, start, end int) {

View File

@@ -18,6 +18,12 @@ import (
"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.
@@ -45,9 +51,11 @@ type pbtsTestHarness struct {
chainID string
// channels for verifying that the observed validator completes certain actions.
proposalChecker, roundChecker, blockChecker, voteChecker eventChecker
ensureProposalCh, roundCh, blockCh, ensureVoteCh <-chan tmpubsub.Message
resultCh <-chan heightResult
// channel of events from the observed validator annotated with the timestamp
// the event was received.
eventCh <-chan timestampedEvent
currentHeight int64
currentRound int32
@@ -71,6 +79,11 @@ type pbtsTestConfiguration struct {
// 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 {
@@ -78,6 +91,15 @@ func newPBTSTestHarness(ctx context.Context, t *testing.T, tc pbtsTestConfigurat
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
@@ -101,7 +123,7 @@ func newPBTSTestHarness(ctx context.Context, t *testing.T, tc pbtsTestConfigurat
pubKey, err := vss[0].PrivValidator.GetPubKey(ctx)
require.NoError(t, err)
resultCh := registerResultCollector(ctx, t, cs.eventBus, pubKey.Address())
eventCh := timestampedCollector(ctx, t, cs.eventBus)
return pbtsTestHarness{
pbtsTestConfiguration: tc,
@@ -111,46 +133,66 @@ func newPBTSTestHarness(ctx context.Context, t *testing.T, tc pbtsTestConfigurat
validatorClock: clock,
currentHeight: 1,
chainID: cfg.ChainID(),
roundChecker: eventChecker{ctx: ctx, t: t, ch: subscribe(ctx, t, cs.eventBus, types.EventQueryNewRound)},
proposalChecker: eventChecker{ctx: ctx, t: t, ch: subscribe(ctx, t, cs.eventBus, types.EventQueryCompleteProposal)},
blockChecker: eventChecker{ctx: ctx, t: t, ch: subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock)},
voteChecker: eventChecker{ctx: ctx, t: t, ch: subscribeToVoterBuffered(ctx, t, cs, pubKey.Address())},
resultCh: resultCh,
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) genesisHeight() heightResult {
p.validatorClock.On("Now").Return(p.height2ProposedBlockTime).Times(8)
func (p *pbtsTestHarness) observedValidatorProposerHeight(previousBlockTime time.Time) heightResult {
p.validatorClock.On("Now").Return(p.height2ProposedBlockTime).Times(6)
startTestRound(p.ctx, p.observedState, p.currentHeight, p.currentRound)
p.roundChecker.ensureNewRound(p.currentHeight, p.currentRound)
ensureNewRound(p.t, p.roundCh, p.currentHeight, p.currentRound)
propBlock, partSet := p.observedState.createProposalBlock()
bid := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: partSet.Header()}
p.proposalChecker.ensureMatchingProposal(p.currentHeight, p.currentRound, bid)
p.voteChecker.ensurePrevote(p.currentHeight, p.currentRound)
timeout := time.Until(previousBlockTime.Add(ensureTimeout))
ensureProposalWithTimeout(p.t, p.ensureProposalCh, p.currentHeight, p.currentRound, bid, timeout)
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...)
p.voteChecker.ensurePrecommit(p.currentHeight, p.currentRound)
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.blockChecker.ensureNewBlock(p.currentHeight)
p.currentHeight++
incrementHeight(p.otherValidators...)
return <-p.resultCh
return res
}
func (p *pbtsTestHarness) height2() heightResult {
signer := p.otherValidators[0].PrivValidator
return p.nextHeight(signer, p.height2ProposalDeliverTime, p.height2ProposedBlockTime, time.Now())
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(8)
p.validatorClock.On("Now").Return(nextProposedTime).Times(6)
p.roundChecker.ensureNewRound(p.currentHeight, p.currentRound)
ensureNewRound(p.t, p.roundCh, p.currentHeight, p.currentRound)
b, _ := p.observedState.createProposalBlock()
b.Height = p.currentHeight
@@ -162,7 +204,7 @@ func (p *pbtsTestHarness) nextHeight(proposer types.PrivValidator, deliverTime,
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)
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 {
@@ -174,127 +216,173 @@ func (p *pbtsTestHarness) nextHeight(proposer types.PrivValidator, deliverTime,
if err := p.observedState.SetProposalAndBlock(prop, b, ps, "peerID"); err != nil {
p.t.Fatal(err)
}
p.proposalChecker.ensureMatchingProposal(p.currentHeight, 0, bid)
ensureProposal(p.t, p.ensureProposalCh, p.currentHeight, 0, bid)
p.voteChecker.ensurePrevote(p.currentHeight, p.currentRound)
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...)
p.voteChecker.ensurePrecommit(p.currentHeight, p.currentRound)
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 <-p.resultCh
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 registerResultCollector(ctx context.Context, t *testing.T, eb *eventbus.EventBus, address []byte) <-chan heightResult {
func collectHeightResults(ctx context.Context, t *testing.T, eventCh <-chan timestampedEvent, height int64, address []byte) heightResult {
t.Helper()
resultCh := make(chan heightResult, 2)
var res heightResult
if err := eb.Observe(ctx, func(msg tmpubsub.Message) error {
ts := time.Now()
vote := msg.Data().(types.EventDataVote)
// we only fire for our own votes
if !bytes.Equal(address, vote.Vote.ValidatorAddress) {
return nil
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 vote.Vote.Type != tmproto.PrevoteType {
return nil
if res.isComplete() {
return res
}
res.prevoteIssuedAt = ts
res.prevote = vote.Vote
resultCh <- res
return nil
}, types.EventQueryVote); err != nil {
t.Fatalf("Failed to observe query %v: %v", types.EventQueryVote, err)
}
return resultCh
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 {
p.genesisHeight()
startTestRound(p.ctx, p.observedState, p.currentHeight, p.currentRound)
r1 := p.observedValidatorProposerHeight(p.genesisTime)
r2 := p.height2()
p.intermediateHeights()
r5 := p.height5()
err := p.observedState.Stop()
require.NoError(p.t, err)
return resultSet{
height2: r2,
genesisHeight: r1,
height2: r2,
height5: r5,
}
}
type resultSet struct {
height2 heightResult
genesisHeight heightResult
height2 heightResult
height5 heightResult
}
type heightResult struct {
prevote *types.Vote
prevoteIssuedAt time.Time
proposalIssuedAt time.Time
prevote *types.Vote
prevoteIssuedAt time.Time
}
// TestReceiveProposalWaitsForPreviousBlockTime tests that a validator receiving
// a proposal waits until the previous block time passes before issuing a prevote.
// The test delivers the block to the validator after the configured `timeout-propose`,
// but before the proposer-based timestamp bound on block delivery and checks that
// the consensus algorithm correctly waits for the new block to be delivered
// and issues a prevote for it.
func TestReceiveProposalWaitsForPreviousBlockTime(t *testing.T) {
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()
initialTime := time.Now().Add(50 * time.Millisecond)
// create a genesis time far (enough) in the future.
initialTime := time.Now().Add(800 * time.Millisecond)
cfg := pbtsTestConfiguration{
timingParams: types.TimingParams{
Precision: 100 * time.Millisecond,
MessageDelay: 500 * time.Millisecond,
timestampParams: types.TimestampParams{
Precision: 10 * time.Millisecond,
MsgDelay: 10 * time.Millisecond,
},
timeoutPropose: 50 * time.Millisecond,
timeoutPropose: 10 * time.Millisecond,
genesisTime: initialTime,
height2ProposalDeliverTime: initialTime.Add(450 * time.Millisecond),
height2ProposedBlockTime: initialTime.Add(350 * time.Millisecond),
height2ProposalDeliverTime: initialTime.Add(10 * time.Millisecond),
height2ProposedBlockTime: initialTime.Add(10 * time.Millisecond),
}
pbtsTest := newPBTSTestHarness(ctx, t, cfg)
results := pbtsTest.run()
// Check that the validator waited until after the proposer-based timestamp
// waitingTime bound.
assert.True(t, results.height2.prevoteIssuedAt.After(cfg.height2ProposalDeliverTime))
maxWaitingTime := cfg.genesisTime.Add(cfg.timingParams.Precision).Add(cfg.timingParams.MessageDelay)
assert.True(t, results.height2.prevoteIssuedAt.Before(maxWaitingTime))
// Check that the validator did not prevote for nil.
assert.NotNil(t, results.height2.prevote.BlockID.Hash)
// ensure that the proposal was issued after the genesis time.
assert.True(t, results.genesisHeight.proposalIssuedAt.After(cfg.genesisTime))
}
// TestReceiveProposalTimesOutOnSlowDelivery tests that a validator receiving
// a proposal times out and prevotes nil if the block is not delivered by the
// within the proposer-based timestamp algorithm's waitingTime bound.
// The test delivers the block to the validator after the previous block's time
// and after the proposer-based timestamp bound on block delivery.
// The test then checks that the validator correctly waited for the new block
// and prevoted nil after timing out.
func TestReceiveProposalTimesOutOnSlowDelivery(t *testing.T) {
// 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()
initialTime := time.Now().Add(time.Millisecond * 50)
cfg := pbtsTestConfiguration{
timingParams: types.TimingParams{
Precision: 100 * time.Millisecond,
MessageDelay: 500 * time.Millisecond,
timestampParams: types.TimestampParams{
Precision: 100 * time.Millisecond,
MsgDelay: 500 * time.Millisecond,
},
timeoutPropose: 50 * time.Millisecond,
genesisTime: initialTime,
height2ProposalDeliverTime: initialTime.Add(610 * time.Millisecond),
height2ProposedBlockTime: initialTime.Add(350 * time.Millisecond),
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()
// Check that the validator waited until after the proposer-based timestamp
// waitinTime bound.
maxWaitingTime := initialTime.Add(cfg.timingParams.Precision).Add(cfg.timingParams.MessageDelay)
assert.True(t, results.height2.prevoteIssuedAt.After(maxWaitingTime))
// 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 nil.
assert.Nil(t, results.height2.prevote.BlockID.Hash)
// 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) {
@@ -335,56 +423,3 @@ func TestProposerWaitTime(t *testing.T) {
})
}
}
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,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
mockSource := new(tmtimemocks.Source)
mockSource.On("Now").Return(testCase.localTime)
tp := types.TimingParams{
Precision: testCase.precision,
MessageDelay: testCase.msgDelay,
}
ti := proposalStepWaitingTime(mockSource, testCase.previousBlockTime, tp)
assert.Equal(t, testCase.expectedDuration, ti)
})
}
}

View File

@@ -348,16 +348,8 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
partSize := types.BlockPartSizeBytes
roundChecker := eventChecker{
ctx: ctx,
t: t,
ch: subscribe(ctx, t, css[0].eventBus, types.EventQueryNewRound),
}
proposalChecker := eventChecker{
ctx: ctx,
t: t,
ch: subscribe(ctx, t, css[0].eventBus, types.EventQueryCompleteProposal),
}
newRoundCh := subscribe(ctx, t, css[0].eventBus, types.EventQueryNewRound)
proposalCh := subscribe(ctx, t, css[0].eventBus, types.EventQueryCompleteProposal)
vss := make([]*validatorStub, nPeers)
for i := 0; i < nPeers; i++ {
@@ -368,15 +360,15 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
// start the machine
startTestRound(ctx, css[0], height, round)
incrementHeight(vss...)
roundChecker.ensureNewRound(height, 0)
proposalChecker.ensureNewProposal(height, round)
ensureNewRound(t, newRoundCh, height, 0)
ensureNewProposal(t, proposalCh, height, round)
rs := css[0].GetRoundState()
signAddVotes(ctx, css[0], tmproto.PrecommitType, sim.Config.ChainID(),
types.BlockID{Hash: rs.ProposalBlock.Hash(), PartSetHeader: rs.ProposalBlockParts.Header()},
vss[1:nVals]...)
roundChecker.ensureNewRound(height+1, 0)
ensureNewRound(t, newRoundCh, height+1, 0)
// HEIGHT 2
height++
@@ -392,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)
@@ -403,12 +395,12 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
if err := css[0].SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"); err != nil {
t.Fatal(err)
}
proposalChecker.ensureNewProposal(height, round)
ensureNewProposal(t, proposalCh, height, round)
rs = css[0].GetRoundState()
signAddVotes(ctx, css[0], tmproto.PrecommitType, sim.Config.ChainID(),
types.BlockID{Hash: rs.ProposalBlock.Hash(), PartSetHeader: rs.ProposalBlockParts.Header()},
vss[1:nVals]...)
roundChecker.ensureNewRound(height+1, 0)
ensureNewRound(t, newRoundCh, height+1, 0)
// HEIGHT 3
height++
@@ -424,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)
@@ -435,12 +427,12 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
if err := css[0].SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"); err != nil {
t.Fatal(err)
}
proposalChecker.ensureNewProposal(height, round)
ensureNewProposal(t, proposalCh, height, round)
rs = css[0].GetRoundState()
signAddVotes(ctx, css[0], tmproto.PrecommitType, sim.Config.ChainID(),
types.BlockID{Hash: rs.ProposalBlock.Hash(), PartSetHeader: rs.ProposalBlockParts.Header()},
vss[1:nVals]...)
roundChecker.ensureNewRound(height+1, 0)
ensureNewRound(t, newRoundCh, height+1, 0)
// HEIGHT 4
height++
@@ -483,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)
@@ -494,7 +486,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
if err := css[0].SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"); err != nil {
t.Fatal(err)
}
proposalChecker.ensureNewProposal(height, round)
ensureNewProposal(t, proposalCh, height, round)
removeValidatorTx2 := kvstore.MakeValSetChangeTx(newVal2ABCI, 0)
err = assertMempool(css[0].txNotifier).CheckTx(ctx, removeValidatorTx2, nil, mempool.TxInfo{})
@@ -511,7 +503,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
newVss[i])
}
roundChecker.ensureNewRound(height+1, 0)
ensureNewRound(t, newRoundCh, height+1, 0)
// HEIGHT 5
height++
@@ -521,7 +513,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
newVss[newVssIdx].VotingPower = 25
sort.Sort(ValidatorStubsByPower(newVss))
selfIndex = valIndexFn(0)
proposalChecker.ensureNewProposal(height, round)
ensureNewProposal(t, proposalCh, height, round)
rs = css[0].GetRoundState()
for i := 0; i < nVals+1; i++ {
if i == selfIndex {
@@ -532,7 +524,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
types.BlockID{Hash: rs.ProposalBlock.Hash(), PartSetHeader: rs.ProposalBlockParts.Header()},
newVss[i])
}
roundChecker.ensureNewRound(height+1, 0)
ensureNewRound(t, newRoundCh, height+1, 0)
// HEIGHT 6
height++
@@ -548,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)
@@ -559,7 +551,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
if err := css[0].SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"); err != nil {
t.Fatal(err)
}
proposalChecker.ensureNewProposal(height, round)
ensureNewProposal(t, proposalCh, height, round)
rs = css[0].GetRoundState()
for i := 0; i < nVals+3; i++ {
if i == selfIndex {
@@ -570,7 +562,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite {
types.BlockID{Hash: rs.ProposalBlock.Hash(), PartSetHeader: rs.ProposalBlockParts.Header()},
newVss[i])
}
roundChecker.ensureNewRound(height+1, 0)
ensureNewRound(t, newRoundCh, height+1, 0)
sim.Chain = make([]*types.Block, 0)
sim.Commits = make([]*types.Commit, 0)
@@ -824,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 {
@@ -833,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
@@ -1012,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

@@ -1069,9 +1069,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 +1105,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() {
@@ -1119,12 +1130,8 @@ func (cs *State) enterPropose(height int64, round int32) {
}
}()
//nolint: lll
waitingTime := proposalStepWaitingTime(tmtime.DefaultSource{}, cs.state.LastBlockTime, cs.state.ConsensusParams.Timing)
proposalTimeout := maxDuration(cs.config.Propose(round), waitingTime)
// If we don't get the proposal and all block parts quick enough, enterPrevote
cs.scheduleTimeout(proposalTimeout, height, round, cstypes.RoundStepPropose)
cs.scheduleTimeout(cs.config.Propose(round), height, round, cstypes.RoundStepPropose)
// Nothing more to do if we're not a validator
if cs.privValidator == nil {
@@ -1132,8 +1139,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.
@@ -1141,18 +1146,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)
@@ -1192,7 +1199,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
@@ -1314,6 +1321,12 @@ func (cs *State) defaultDoPrevote(height int64, round int32) {
return
}
if !cs.Proposal.Timestamp.Equal(cs.ProposalBlock.Header.Time) {
logger.Debug("proposal timestamp not equal, prevoting nil")
cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
return
}
// Validate proposal block
err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock)
if err != nil {
@@ -1338,6 +1351,7 @@ func (cs *State) defaultDoPrevote(height int64, round int32) {
*/
if cs.Proposal.POLRound == -1 {
if cs.LockedRound == -1 {
// TODO(@wbanfield) add check for timely here as well
logger.Debug("prevote step: ProposalBlock is valid and there is no locked block; prevoting the proposal")
cs.signAddVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header())
return
@@ -1473,9 +1487,15 @@ func (cs *State) enterPrecommit(height int64, round int32) {
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("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")
@@ -2434,32 +2454,3 @@ func proposerWaitTime(lt tmtime.Source, bt time.Time) time.Duration {
}
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, bt time.Time, tp types.TimingParams) time.Duration {
t := lt.Now()
wt := bt.Add(tp.Precision).Add(tp.MessageDelay)
if t.After(wt) {
return 0
}
return wt.Sub(t)
}
func maxDuration(d1, d2 time.Duration) time.Duration {
if d1 >= d2 {
return d1
}
return d2
}

File diff suppressed because it is too large Load Diff

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

@@ -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

@@ -4,7 +4,9 @@ import (
"bytes"
"errors"
"fmt"
"time"
"github.com/tendermint/tendermint/proto/tendermint/types"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
@@ -43,6 +45,21 @@ func (lb LightBlock) ValidateBasic(chainID string) error {
return nil
}
func (lb LightBlock) CalculateSynchronyVotingPower(t types.TimingParams) time.Duration {
// Synchrony calculated as a function of the amount of voting power that has been seen since
// since the beginning of the height exclusive of the round in which the proposal succeeded.
votingPowerSeen := lb.ValidatorSet.VotingPowerBeforeRound(lb.SignedHeader.Commit.Round)
nonFaultyThreshold := lb.ValidatorSet.TotalVotingPower()*1/3 + 1
thresholdsObserved := (votingPowerSeen / nonFaultyThreshold)
return t.Precision + t.MessageDelay + t.MessageDelay*time.Duration(thresholdsObserved)
}
func (lb LightBlock) CalculateSynchronyRounds(t types.TimingParams) time.Duration {
// hard coded value of '10' used in this example
return t.Precision + t.MessageDelay + t.MessageDelay*time.Duration(lb.Commit.Round/10)
}
// String returns a string representation of the LightBlock
func (lb LightBlock) String() string {
return lb.StringIndented("")

View File

@@ -6,8 +6,10 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/version"
)
@@ -163,3 +165,45 @@ func TestSignedHeaderValidateBasic(t *testing.T) {
})
}
}
func TestCalculateSynchronyVotingPower(t *testing.T) {
vals, _ := randValidatorPrivValSet(5, 10)
sh := &SignedHeader{
Commit: &Commit{
Round: 4,
},
}
lb := LightBlock{
ValidatorSet: vals,
SignedHeader: sh,
}
tp := types.TimingParams{
MessageDelay: 100 * time.Millisecond,
Precision: 10 * time.Millisecond,
}
s := lb.CalculateSynchronyVotingPower(tp)
require.Equal(t, s, tp.Precision+tp.MessageDelay+tp.MessageDelay*2)
}
func TestCalculateSynchronyRound(t *testing.T) {
vals, _ := randValidatorPrivValSet(5, 10)
sh := &SignedHeader{
Commit: &Commit{
Round: 11,
},
}
lb := LightBlock{
ValidatorSet: vals,
SignedHeader: sh,
}
tp := types.TimingParams{
MessageDelay: 100 * time.Millisecond,
Precision: 10 * time.Millisecond,
}
s := lb.CalculateSynchronyRounds(tp)
require.Equal(t, s, tp.Precision+tp.MessageDelay+tp.MessageDelay*1)
}

View File

@@ -41,7 +41,7 @@ type ConsensusParams struct {
Evidence EvidenceParams `json:"evidence"`
Validator ValidatorParams `json:"validator"`
Version VersionParams `json:"version"`
Timing TimingParams `json:"timestamp"`
Timing TimingParams `json:"timing"`
}
// HashedParams is a subset of ConsensusParams.
@@ -90,6 +90,7 @@ func DefaultConsensusParams() *ConsensusParams {
Evidence: DefaultEvidenceParams(),
Validator: DefaultValidatorParams(),
Version: DefaultVersionParams(),
Timing: DefaultTimingParams(),
}
}
@@ -128,8 +129,8 @@ func DefaultTimingParams() TimingParams {
// TODO(@wbanfield): Determine experimental values for these defaults
// https://github.com/tendermint/tendermint/issues/7202
return TimingParams{
Precision: 2 * time.Second,
MessageDelay: 3 * time.Second,
Precision: 1 * time.Nanosecond,
MessageDelay: 1 * time.Nanosecond,
}
}
@@ -165,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)
}
@@ -179,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")
}
@@ -222,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)
}
@@ -252,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
}

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),
}
}
@@ -88,10 +88,11 @@ func (p *Proposal) ValidateBasic() error {
//
// 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 TimingParams) bool {
func (p *Proposal) IsTimely(clock tmtime.Source, tp TimingParams, vs *ValidatorSet) bool {
thirdsSeen := vs.VotingPowerBeforeRound(p.Round) / (vs.TotalVotingPower()*1/3 + 1)
lt := clock.Now()
lhs := lt.Add(-tp.Precision)
rhs := lt.Add(tp.Precision).Add(tp.MessageDelay)
rhs := lt.Add(tp.Precision)
lhs := lt.Add(-tp.Precision).Add(-tp.MessageDelay - tp.MessageDelay*time.Duration(thirdsSeen))
if lhs.Before(p.Timestamp) && rhs.After(p.Timestamp) {
return true
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/tendermint/tendermint/internal/libs/protoio"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmtime "github.com/tendermint/tendermint/libs/time"
tmtimemocks "github.com/tendermint/tendermint/libs/time/mocks"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
@@ -63,7 +64,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 +155,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 +167,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
@@ -199,6 +200,7 @@ func TestIsTimely(t *testing.T) {
testCases := []struct {
name string
proposalTime time.Time
round int32
localTime time.Time
precision time.Duration
msgDelay time.Duration
@@ -234,11 +236,30 @@ func TestIsTimely(t *testing.T) {
msgDelay: time.Nanosecond,
expectTimely: false,
},
{
name: "long proposal in later round",
proposalTime: genesisTime,
round: 2, // should see 1/3 +1 of the network power
localTime: genesisTime.Add(3 * time.Nanosecond),
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: true,
},
{
name: "long proposal in earlier round",
proposalTime: genesisTime,
round: 1, // should see 1/3 +1 of the network power
localTime: genesisTime.Add(3 * time.Nanosecond),
precision: time.Nanosecond * 2,
msgDelay: time.Nanosecond,
expectTimely: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
p := Proposal{
Timestamp: testCase.proposalTime,
Round: testCase.round,
}
tp := TimingParams{
@@ -249,7 +270,8 @@ func TestIsTimely(t *testing.T) {
mockSource := new(tmtimemocks.Source)
mockSource.On("Now").Return(testCase.localTime)
ti := p.IsTimely(mockSource, tp)
vals, _ := randValidatorPrivValSet(5, 15)
ti := p.IsTimely(mockSource, tp, vals)
assert.Equal(t, testCase.expectTimely, ti)
})
}

View File

@@ -342,6 +342,17 @@ func (vals *ValidatorSet) findProposer() *Validator {
return proposer
}
// VotingPowerSeenBeforeRound calculates the amount of voting power seen on the network
// from proposals before round r.
func (vals *ValidatorSet) VotingPowerBeforeRound(r int32) int64 {
var votingPowerSeen int64
for i := int32(0); i < r; i++ {
vals.IncrementProposerPriority(1)
votingPowerSeen += vals.GetProposer().VotingPower
}
return votingPowerSeen
}
// Hash returns the Merkle root hash build using validators (as leaves) in the
// set.
func (vals *ValidatorSet) Hash() []byte {