@ -0,0 +1,101 @@ | |||||
package certifiers | |||||
import ( | |||||
"bytes" | |||||
"github.com/pkg/errors" | |||||
rtypes "github.com/tendermint/tendermint/rpc/core/types" | |||||
"github.com/tendermint/tendermint/types" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
// Certifier checks the votes to make sure the block really is signed properly. | |||||
// Certifier must know the current set of validitors by some other means. | |||||
type Certifier interface { | |||||
Certify(check *Commit) error | |||||
ChainID() string | |||||
} | |||||
// *Commit is basically the rpc /commit response, but extended | |||||
// | |||||
// This is the basepoint for proving anything on the blockchain. It contains | |||||
// a signed header. If the signatures are valid and > 2/3 of the known set, | |||||
// we can store this checkpoint and use it to prove any number of aspects of | |||||
// the system: such as txs, abci state, validator sets, etc... | |||||
type Commit rtypes.ResultCommit | |||||
// FullCommit is a commit and the actual validator set, | |||||
// the base info you need to update to a given point, | |||||
// assuming knowledge of some previous validator set | |||||
type FullCommit struct { | |||||
*Commit `json:"commit"` | |||||
Validators *types.ValidatorSet `json:"validator_set"` | |||||
} | |||||
func NewFullCommit(commit *Commit, vals *types.ValidatorSet) FullCommit { | |||||
return FullCommit{ | |||||
Commit: commit, | |||||
Validators: vals, | |||||
} | |||||
} | |||||
func CommitFromResult(commit *rtypes.ResultCommit) *Commit { | |||||
return (*Commit)(commit) | |||||
} | |||||
func (c *Commit) Height() int { | |||||
if c == nil || c.Header == nil { | |||||
return 0 | |||||
} | |||||
return c.Header.Height | |||||
} | |||||
func (c *Commit) ValidatorsHash() []byte { | |||||
if c == nil || c.Header == nil { | |||||
return nil | |||||
} | |||||
return c.Header.ValidatorsHash | |||||
} | |||||
// ValidateBasic does basic consistency checks and makes sure the headers | |||||
// and commits are all consistent and refer to our chain. | |||||
// | |||||
// Make sure to use a Verifier to validate the signatures actually provide | |||||
// a significantly strong proof for this header's validity. | |||||
func (c *Commit) ValidateBasic(chainID string) error { | |||||
// make sure the header is reasonable | |||||
if c.Header == nil { | |||||
return errors.New("Commit missing header") | |||||
} | |||||
if c.Header.ChainID != chainID { | |||||
return errors.Errorf("Header belongs to another chain '%s' not '%s'", | |||||
c.Header.ChainID, chainID) | |||||
} | |||||
if c.Commit == nil { | |||||
return errors.New("Commit missing signatures") | |||||
} | |||||
// make sure the header and commit match (height and hash) | |||||
if c.Commit.Height() != c.Header.Height { | |||||
return certerr.ErrHeightMismatch(c.Commit.Height(), c.Header.Height) | |||||
} | |||||
hhash := c.Header.Hash() | |||||
chash := c.Commit.BlockID.Hash | |||||
if !bytes.Equal(hhash, chash) { | |||||
return errors.Errorf("Commits sign block %X header is block %X", | |||||
chash, hhash) | |||||
} | |||||
// make sure the commit is reasonable | |||||
err := c.Commit.ValidateBasic() | |||||
if err != nil { | |||||
return errors.WithStack(err) | |||||
} | |||||
// looks good, we just need to make sure the signatures are really from | |||||
// empowered validators | |||||
return nil | |||||
} |
@ -0,0 +1,25 @@ | |||||
package client_test | |||||
import ( | |||||
"os" | |||||
"testing" | |||||
"github.com/tendermint/abci/example/dummy" | |||||
nm "github.com/tendermint/tendermint/node" | |||||
rpctest "github.com/tendermint/tendermint/rpc/test" | |||||
) | |||||
var node *nm.Node | |||||
func TestMain(m *testing.M) { | |||||
// start a tendermint node (and merkleeyes) in the background to test against | |||||
app := dummy.NewDummyApplication() | |||||
node = rpctest.StartTendermint(app) | |||||
code := m.Run() | |||||
// and shut down proper at the end | |||||
node.Stop() | |||||
node.Wait() | |||||
os.Exit(code) | |||||
} |
@ -0,0 +1,129 @@ | |||||
/* | |||||
Package client defines a provider that uses a rpcclient | |||||
to get information, which is used to get new headers | |||||
and validators directly from a node. | |||||
*/ | |||||
package client | |||||
import ( | |||||
"bytes" | |||||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||||
"github.com/tendermint/tendermint/types" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
type SignStatusClient interface { | |||||
rpcclient.SignClient | |||||
rpcclient.StatusClient | |||||
} | |||||
type provider struct { | |||||
node SignStatusClient | |||||
lastHeight int | |||||
} | |||||
// NewProvider can wrap any rpcclient to expose it as | |||||
// a read-only provider. | |||||
func NewProvider(node SignStatusClient) certifiers.Provider { | |||||
return &provider{node: node} | |||||
} | |||||
// NewProvider can connects to a tendermint json-rpc endpoint | |||||
// at the given url, and uses that as a read-only provider. | |||||
func NewHTTPProvider(remote string) certifiers.Provider { | |||||
return &provider{ | |||||
node: rpcclient.NewHTTP(remote, "/websocket"), | |||||
} | |||||
} | |||||
// StoreCommit is a noop, as clients can only read from the chain... | |||||
func (p *provider) StoreCommit(_ certifiers.FullCommit) error { return nil } | |||||
// GetHash gets the most recent validator and sees if it matches | |||||
// | |||||
// TODO: improve when the rpc interface supports more functionality | |||||
func (p *provider) GetByHash(hash []byte) (certifiers.FullCommit, error) { | |||||
var fc certifiers.FullCommit | |||||
vals, err := p.node.Validators(nil) | |||||
// if we get no validators, or a different height, return an error | |||||
if err != nil { | |||||
return fc, err | |||||
} | |||||
p.updateHeight(vals.BlockHeight) | |||||
vhash := types.NewValidatorSet(vals.Validators).Hash() | |||||
if !bytes.Equal(hash, vhash) { | |||||
return fc, certerr.ErrCommitNotFound() | |||||
} | |||||
return p.seedFromVals(vals) | |||||
} | |||||
// GetByHeight gets the validator set by height | |||||
func (p *provider) GetByHeight(h int) (fc certifiers.FullCommit, err error) { | |||||
commit, err := p.node.Commit(&h) | |||||
if err != nil { | |||||
return fc, err | |||||
} | |||||
return p.seedFromCommit(commit) | |||||
} | |||||
func (p *provider) LatestCommit() (fc certifiers.FullCommit, err error) { | |||||
commit, err := p.GetLatestCommit() | |||||
if err != nil { | |||||
return fc, err | |||||
} | |||||
return p.seedFromCommit(commit) | |||||
} | |||||
// GetLatestCommit should return the most recent commit there is, | |||||
// which handles queries for future heights as per the semantics | |||||
// of GetByHeight. | |||||
func (p *provider) GetLatestCommit() (*ctypes.ResultCommit, error) { | |||||
status, err := p.node.Status() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return p.node.Commit(&status.LatestBlockHeight) | |||||
} | |||||
func (p *provider) seedFromVals(vals *ctypes.ResultValidators) (certifiers.FullCommit, error) { | |||||
// now get the commits and build a full commit | |||||
commit, err := p.node.Commit(&vals.BlockHeight) | |||||
if err != nil { | |||||
return certifiers.FullCommit{}, err | |||||
} | |||||
fc := certifiers.NewFullCommit( | |||||
certifiers.CommitFromResult(commit), | |||||
types.NewValidatorSet(vals.Validators), | |||||
) | |||||
return fc, nil | |||||
} | |||||
func (p *provider) seedFromCommit(commit *ctypes.ResultCommit) (fc certifiers.FullCommit, err error) { | |||||
fc.Commit = certifiers.CommitFromResult(commit) | |||||
// now get the proper validators | |||||
vals, err := p.node.Validators(&commit.Header.Height) | |||||
if err != nil { | |||||
return fc, err | |||||
} | |||||
// make sure they match the commit (as we cannot enforce height) | |||||
vset := types.NewValidatorSet(vals.Validators) | |||||
if !bytes.Equal(vset.Hash(), commit.Header.ValidatorsHash) { | |||||
return fc, certerr.ErrValidatorsChanged() | |||||
} | |||||
p.updateHeight(commit.Header.Height) | |||||
fc.Validators = vset | |||||
return fc, nil | |||||
} | |||||
func (p *provider) updateHeight(h int) { | |||||
if h > p.lastHeight { | |||||
p.lastHeight = h | |||||
} | |||||
} |
@ -0,0 +1,62 @@ | |||||
package client_test | |||||
import ( | |||||
"testing" | |||||
"time" | |||||
"github.com/stretchr/testify/assert" | |||||
"github.com/stretchr/testify/require" | |||||
rpctest "github.com/tendermint/tendermint/rpc/test" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
"github.com/tendermint/tendermint/certifiers/client" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
func TestProvider(t *testing.T) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
cfg := rpctest.GetConfig() | |||||
rpcAddr := cfg.RPC.ListenAddress | |||||
chainID := cfg.ChainID | |||||
p := client.NewHTTPProvider(rpcAddr) | |||||
require.NotNil(t, p) | |||||
// let it produce some blocks | |||||
time.Sleep(500 * time.Millisecond) | |||||
// let's get the highest block | |||||
seed, err := p.LatestCommit() | |||||
require.Nil(err, "%+v", err) | |||||
sh := seed.Height() | |||||
vhash := seed.Header.ValidatorsHash | |||||
assert.True(sh < 5000) | |||||
// let's check this is valid somehow | |||||
assert.Nil(seed.ValidateBasic(chainID)) | |||||
cert := certifiers.NewStatic(chainID, seed.Validators) | |||||
// historical queries now work :) | |||||
lower := sh - 5 | |||||
seed, err = p.GetByHeight(lower) | |||||
assert.Nil(err, "%+v", err) | |||||
assert.Equal(lower, seed.Height()) | |||||
// also get by hash (given the match) | |||||
seed, err = p.GetByHash(vhash) | |||||
require.Nil(err, "%+v", err) | |||||
require.Equal(vhash, seed.Header.ValidatorsHash) | |||||
err = cert.Certify(seed.Commit) | |||||
assert.Nil(err, "%+v", err) | |||||
// get by hash fails without match | |||||
seed, err = p.GetByHash([]byte("foobar")) | |||||
assert.NotNil(err) | |||||
assert.True(certerr.IsCommitNotFoundErr(err)) | |||||
// storing the seed silently ignored | |||||
err = p.StoreCommit(seed) | |||||
assert.Nil(err, "%+v", err) | |||||
} |
@ -0,0 +1,133 @@ | |||||
/* | |||||
Package certifiers allows you to securely validate headers | |||||
without a full node. | |||||
This library pulls together all the crypto and algorithms, | |||||
so given a relatively recent (< unbonding period) known | |||||
validator set, one can get indisputable proof that data is in | |||||
the chain (current state) or detect if the node is lying to | |||||
the client. | |||||
Tendermint RPC exposes a lot of info, but a malicious node | |||||
could return any data it wants to queries, or even to block | |||||
headers, even making up fake signatures from non-existent | |||||
validators to justify it. This is a lot of logic to get | |||||
right, to be contained in a small, easy to use library, | |||||
that does this for you, so you can just build nice UI. | |||||
We design for clients who have no strong trust relationship | |||||
with any tendermint node, just the validator set as a whole. | |||||
Beyond building nice mobile or desktop applications, the | |||||
cosmos hub is another important example of a client, | |||||
that needs undeniable proof without syncing the full chain, | |||||
in order to efficiently implement IBC. | |||||
Commits | |||||
There are two main data structures that we pass around - Commit | |||||
and FullCommit. Both of them mirror what information is | |||||
exposed in tendermint rpc. | |||||
Commit is a block header along with enough validator signatures | |||||
to prove its validity (> 2/3 of the voting power). A FullCommit | |||||
is a Commit along with the full validator set. When the | |||||
validator set doesn't change, the Commit is enough, but since | |||||
the block header only has a hash, we need the FullCommit to | |||||
follow any changes to the validator set. | |||||
Certifiers | |||||
A Certifier validates a new Commit given the currently known | |||||
state. There are three different types of Certifiers exposed, | |||||
each one building on the last one, with additional complexity. | |||||
Static - given the validator set upon initialization. Verifies | |||||
all signatures against that set and if the validator set | |||||
changes, it will reject all headers. | |||||
Dynamic - This wraps Static and has the same Certify | |||||
method. However, it adds an Update method, which can be called | |||||
with a FullCommit when the validator set changes. If it can | |||||
prove this is a valid transition, it will update the validator | |||||
set. | |||||
Inquiring - this wraps Dynamic and implements an auto-update | |||||
strategy on top of the Dynamic update. If a call to | |||||
Certify fails as the validator set has changed, then it | |||||
attempts to find a FullCommit and Update to that header. | |||||
To get these FullCommits, it makes use of a Provider. | |||||
Providers | |||||
A Provider allows us to store and retrieve the FullCommits, | |||||
to provide memory to the Inquiring Certifier. | |||||
NewMemStoreProvider - in-memory cache. | |||||
files.NewProvider - disk backed storage. | |||||
client.NewHTTPProvider - query tendermint rpc. | |||||
NewCacheProvider - combine multiple providers. | |||||
The suggested use for local light clients is | |||||
client.NewHTTPProvider for getting new data (Source), | |||||
and NewCacheProvider(NewMemStoreProvider(), | |||||
files.NewProvider()) to store confirmed headers (Trusted) | |||||
How We Track Validators | |||||
Unless you want to blindly trust the node you talk with, you | |||||
need to trace every response back to a hash in a block header | |||||
and validate the commit signatures of that block header match | |||||
the proper validator set. If there is a contant validator | |||||
set, you store it locally upon initialization of the client, | |||||
and check against that every time. | |||||
Once there is a dynamic validator set, the issue of | |||||
verifying a block becomes a bit more tricky. There is | |||||
background information in a | |||||
github issue (https://github.com/tendermint/tendermint/issues/377). | |||||
In short, if there is a block at height H with a known | |||||
(trusted) validator set V, and another block at height H' | |||||
(H' > H) with validator set V' != V, then we want a way to | |||||
safely update it. | |||||
First, get the new (unconfirmed) validator set V' and | |||||
verify H' is internally consistent and properly signed by | |||||
this V'. Assuming it is a valid block, we check that at | |||||
least 2/3 of the validators in V also signed it, meaning | |||||
it would also be valid under our old assumptions. | |||||
That should be enough, but we can also check that the | |||||
V counts for at least 2/3 of the total votes in H' | |||||
for extra safety (we can have a discussion if this is | |||||
strictly required). If we can verify all this, | |||||
then we can accept H' and V' as valid and use that to | |||||
validate all blocks X > H'. | |||||
If we cannot update directly from H -> H' because there was | |||||
too much change to the validator set, then we can look for | |||||
some Hm (H < Hm < H') with a validator set Vm. Then we try | |||||
to update H -> Hm and Hm -> H' in two separate steps. | |||||
If one of these steps doesn't work, then we continue | |||||
bisecting, until we eventually have to externally | |||||
validate the valdiator set changes at every block. | |||||
Since we never trust any server in this protocol, only the | |||||
signatures themselves, it doesn't matter if the seed comes | |||||
from a (possibly malicious) node or a (possibly malicious) user. | |||||
We can accept it or reject it based only on our trusted | |||||
validator set and cryptographic proofs. This makes it | |||||
extremely important to verify that you have the proper | |||||
validator set when initializing the client, as that is the | |||||
root of all trust. | |||||
Or course, this assumes that the known block is within the | |||||
unbonding period to avoid the "nothing at stake" problem. | |||||
If you haven't seen the state in a few months, you will need | |||||
to manually verify the new validator set hash using off-chain | |||||
means (the same as getting the initial hash). | |||||
*/ | |||||
package certifiers |
@ -0,0 +1,173 @@ | |||||
package certifiers | |||||
import ( | |||||
"github.com/pkg/errors" | |||||
"github.com/tendermint/tendermint/types" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
var _ Certifier = &Dynamic{} | |||||
// Dynamic uses a Static for Certify, but adds an | |||||
// Update method to allow for a change of validators. | |||||
// | |||||
// You can pass in a FullCommit with another validator set, | |||||
// and if this is a provably secure transition (< 1/3 change, | |||||
// sufficient signatures), then it will update the | |||||
// validator set for the next Certify call. | |||||
// For security, it will only follow validator set changes | |||||
// going forward. | |||||
type Dynamic struct { | |||||
cert *Static | |||||
lastHeight int | |||||
} | |||||
func NewDynamic(chainID string, vals *types.ValidatorSet, height int) *Dynamic { | |||||
return &Dynamic{ | |||||
cert: NewStatic(chainID, vals), | |||||
lastHeight: height, | |||||
} | |||||
} | |||||
func (c *Dynamic) ChainID() string { | |||||
return c.cert.ChainID() | |||||
} | |||||
func (c *Dynamic) Validators() *types.ValidatorSet { | |||||
return c.cert.vSet | |||||
} | |||||
func (c *Dynamic) Hash() []byte { | |||||
return c.cert.Hash() | |||||
} | |||||
func (c *Dynamic) LastHeight() int { | |||||
return c.lastHeight | |||||
} | |||||
// Certify handles this with | |||||
func (c *Dynamic) Certify(check *Commit) error { | |||||
err := c.cert.Certify(check) | |||||
if err == nil { | |||||
// update last seen height if input is valid | |||||
c.lastHeight = check.Height() | |||||
} | |||||
return err | |||||
} | |||||
// Update will verify if this is a valid change and update | |||||
// the certifying validator set if safe to do so. | |||||
// | |||||
// Returns an error if update is impossible (invalid proof or IsTooMuchChangeErr) | |||||
func (c *Dynamic) Update(fc FullCommit) error { | |||||
// ignore all checkpoints in the past -> only to the future | |||||
h := fc.Height() | |||||
if h <= c.lastHeight { | |||||
return certerr.ErrPastTime() | |||||
} | |||||
// first, verify if the input is self-consistent.... | |||||
err := fc.ValidateBasic(c.ChainID()) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// now, make sure not too much change... meaning this commit | |||||
// would be approved by the currently known validator set | |||||
// as well as the new set | |||||
commit := fc.Commit.Commit | |||||
err = VerifyCommitAny(c.Validators(), fc.Validators, c.ChainID(), | |||||
commit.BlockID, h, commit) | |||||
if err != nil { | |||||
return certerr.ErrTooMuchChange() | |||||
} | |||||
// looks good, we can update | |||||
c.cert = NewStatic(c.ChainID(), fc.Validators) | |||||
c.lastHeight = h | |||||
return nil | |||||
} | |||||
// VerifyCommitAny will check to see if the set would | |||||
// be valid with a different validator set. | |||||
// | |||||
// old is the validator set that we know | |||||
// * over 2/3 of the power in old signed this block | |||||
// | |||||
// cur is the validator set that signed this block | |||||
// * only votes from old are sufficient for 2/3 majority | |||||
// in the new set as well | |||||
// | |||||
// That means that: | |||||
// * 10% of the valset can't just declare themselves kings | |||||
// * If the validator set is 3x old size, we need more proof to trust | |||||
// | |||||
// *** TODO: move this. | |||||
// It belongs in tendermint/types/validator_set.go: VerifyCommitAny | |||||
func VerifyCommitAny(old, cur *types.ValidatorSet, chainID string, | |||||
blockID types.BlockID, height int, commit *types.Commit) error { | |||||
if cur.Size() != len(commit.Precommits) { | |||||
return errors.Errorf("Invalid commit -- wrong set size: %v vs %v", cur.Size(), len(commit.Precommits)) | |||||
} | |||||
if height != commit.Height() { | |||||
return errors.Errorf("Invalid commit -- wrong height: %v vs %v", height, commit.Height()) | |||||
} | |||||
oldVotingPower := int64(0) | |||||
curVotingPower := int64(0) | |||||
seen := map[int]bool{} | |||||
round := commit.Round() | |||||
for idx, precommit := range commit.Precommits { | |||||
// first check as in VerifyCommit | |||||
if precommit == nil { | |||||
continue | |||||
} | |||||
if precommit.Height != height { | |||||
return certerr.ErrHeightMismatch(height, precommit.Height) | |||||
} | |||||
if precommit.Round != round { | |||||
return errors.Errorf("Invalid commit -- wrong round: %v vs %v", round, precommit.Round) | |||||
} | |||||
if precommit.Type != types.VoteTypePrecommit { | |||||
return errors.Errorf("Invalid commit -- not precommit @ index %v", idx) | |||||
} | |||||
if !blockID.Equals(precommit.BlockID) { | |||||
continue // Not an error, but doesn't count | |||||
} | |||||
// we only grab by address, ignoring unknown validators | |||||
vi, ov := old.GetByAddress(precommit.ValidatorAddress) | |||||
if ov == nil || seen[vi] { | |||||
continue // missing or double vote... | |||||
} | |||||
seen[vi] = true | |||||
// Validate signature old school | |||||
precommitSignBytes := types.SignBytes(chainID, precommit) | |||||
if !ov.PubKey.VerifyBytes(precommitSignBytes, precommit.Signature) { | |||||
return errors.Errorf("Invalid commit -- invalid signature: %v", precommit) | |||||
} | |||||
// Good precommit! | |||||
oldVotingPower += ov.VotingPower | |||||
// check new school | |||||
_, cv := cur.GetByIndex(idx) | |||||
if cv.PubKey.Equals(ov.PubKey) { | |||||
// make sure this is properly set in the current block as well | |||||
curVotingPower += cv.VotingPower | |||||
} | |||||
} | |||||
if oldVotingPower <= old.TotalVotingPower()*2/3 { | |||||
return errors.Errorf("Invalid commit -- insufficient old voting power: got %v, needed %v", | |||||
oldVotingPower, (old.TotalVotingPower()*2/3 + 1)) | |||||
} else if curVotingPower <= cur.TotalVotingPower()*2/3 { | |||||
return errors.Errorf("Invalid commit -- insufficient cur voting power: got %v, needed %v", | |||||
curVotingPower, (cur.TotalVotingPower()*2/3 + 1)) | |||||
} | |||||
return nil | |||||
} |
@ -0,0 +1,130 @@ | |||||
package certifiers_test | |||||
import ( | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
"github.com/stretchr/testify/require" | |||||
"github.com/tendermint/tendermint/types" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
"github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
// TestDynamicCert just makes sure it still works like StaticCert | |||||
func TestDynamicCert(t *testing.T) { | |||||
// assert, require := assert.New(t), require.New(t) | |||||
assert := assert.New(t) | |||||
// require := require.New(t) | |||||
keys := certifiers.GenValKeys(4) | |||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do! | |||||
vals := keys.ToValidators(20, 10) | |||||
// and a certifier based on our known set | |||||
chainID := "test-dyno" | |||||
cert := certifiers.NewDynamic(chainID, vals, 0) | |||||
cases := []struct { | |||||
keys certifiers.ValKeys | |||||
vals *types.ValidatorSet | |||||
height int | |||||
first, last int // who actually signs | |||||
proper bool // true -> expect no error | |||||
changed bool // true -> expect validator change error | |||||
}{ | |||||
// perfect, signed by everyone | |||||
{keys, vals, 1, 0, len(keys), true, false}, | |||||
// skip little guy is okay | |||||
{keys, vals, 2, 1, len(keys), true, false}, | |||||
// but not the big guy | |||||
{keys, vals, 3, 0, len(keys) - 1, false, false}, | |||||
// even changing the power a little bit breaks the static validator | |||||
// the sigs are enough, but the validator hash is unknown | |||||
{keys, keys.ToValidators(20, 11), 4, 0, len(keys), false, true}, | |||||
} | |||||
for _, tc := range cases { | |||||
check := tc.keys.GenCommit(chainID, tc.height, nil, tc.vals, | |||||
[]byte("bar"), tc.first, tc.last) | |||||
err := cert.Certify(check) | |||||
if tc.proper { | |||||
assert.Nil(err, "%+v", err) | |||||
assert.Equal(cert.LastHeight(), tc.height) | |||||
} else { | |||||
assert.NotNil(err) | |||||
if tc.changed { | |||||
assert.True(errors.IsValidatorsChangedErr(err), "%+v", err) | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// TestDynamicUpdate makes sure we update safely and sanely | |||||
func TestDynamicUpdate(t *testing.T) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
chainID := "test-dyno-up" | |||||
keys := certifiers.GenValKeys(5) | |||||
vals := keys.ToValidators(20, 0) | |||||
cert := certifiers.NewDynamic(chainID, vals, 40) | |||||
// one valid block to give us a sense of time | |||||
h := 100 | |||||
good := keys.GenCommit(chainID, h, nil, vals, []byte("foo"), 0, len(keys)) | |||||
err := cert.Certify(good) | |||||
require.Nil(err, "%+v", err) | |||||
// some new sets to try later | |||||
keys2 := keys.Extend(2) | |||||
keys3 := keys2.Extend(4) | |||||
// we try to update with some blocks | |||||
cases := []struct { | |||||
keys certifiers.ValKeys | |||||
vals *types.ValidatorSet | |||||
height int | |||||
first, last int // who actually signs | |||||
proper bool // true -> expect no error | |||||
changed bool // true -> expect too much change error | |||||
}{ | |||||
// same validator set, well signed, of course it is okay | |||||
{keys, vals, h + 10, 0, len(keys), true, false}, | |||||
// same validator set, poorly signed, fails | |||||
{keys, vals, h + 20, 2, len(keys), false, false}, | |||||
// shift the power a little, works if properly signed | |||||
{keys, keys.ToValidators(10, 0), h + 30, 1, len(keys), true, false}, | |||||
// but not on a poor signature | |||||
{keys, keys.ToValidators(10, 0), h + 40, 2, len(keys), false, false}, | |||||
// and not if it was in the past | |||||
{keys, keys.ToValidators(10, 0), h + 25, 0, len(keys), false, false}, | |||||
// let's try to adjust to a whole new validator set (we have 5/7 of the votes) | |||||
{keys2, keys2.ToValidators(10, 0), h + 33, 0, len(keys2), true, false}, | |||||
// properly signed but too much change, not allowed (only 7/11 validators known) | |||||
{keys3, keys3.ToValidators(10, 0), h + 50, 0, len(keys3), false, true}, | |||||
} | |||||
for _, tc := range cases { | |||||
fc := tc.keys.GenFullCommit(chainID, tc.height, nil, tc.vals, | |||||
[]byte("bar"), tc.first, tc.last) | |||||
err := cert.Update(fc) | |||||
if tc.proper { | |||||
assert.Nil(err, "%d: %+v", tc.height, err) | |||||
// we update last seen height | |||||
assert.Equal(cert.LastHeight(), tc.height) | |||||
// and we update the proper validators | |||||
assert.EqualValues(fc.Header.ValidatorsHash, cert.Hash()) | |||||
} else { | |||||
assert.NotNil(err, "%d", tc.height) | |||||
// we don't update the height | |||||
assert.NotEqual(cert.LastHeight(), tc.height) | |||||
if tc.changed { | |||||
assert.True(errors.IsTooMuchChangeErr(err), | |||||
"%d: %+v", tc.height, err) | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,86 @@ | |||||
package errors | |||||
import ( | |||||
"fmt" | |||||
"github.com/pkg/errors" | |||||
) | |||||
var ( | |||||
errValidatorsChanged = fmt.Errorf("Validators differ between header and certifier") | |||||
errCommitNotFound = fmt.Errorf("Commit not found by provider") | |||||
errTooMuchChange = fmt.Errorf("Validators change too much to safely update") | |||||
errPastTime = fmt.Errorf("Update older than certifier height") | |||||
errNoPathFound = fmt.Errorf("Cannot find a path of validators") | |||||
) | |||||
// IsCommitNotFoundErr checks whether an error is due to missing data | |||||
func IsCommitNotFoundErr(err error) bool { | |||||
return err != nil && (errors.Cause(err) == errCommitNotFound) | |||||
} | |||||
func ErrCommitNotFound() error { | |||||
return errors.WithStack(errCommitNotFound) | |||||
} | |||||
// IsValidatorsChangedErr checks whether an error is due | |||||
// to a differing validator set | |||||
func IsValidatorsChangedErr(err error) bool { | |||||
return err != nil && (errors.Cause(err) == errValidatorsChanged) | |||||
} | |||||
func ErrValidatorsChanged() error { | |||||
return errors.WithStack(errValidatorsChanged) | |||||
} | |||||
// IsTooMuchChangeErr checks whether an error is due to too much change | |||||
// between these validators sets | |||||
func IsTooMuchChangeErr(err error) bool { | |||||
return err != nil && (errors.Cause(err) == errTooMuchChange) | |||||
} | |||||
func ErrTooMuchChange() error { | |||||
return errors.WithStack(errTooMuchChange) | |||||
} | |||||
func IsPastTimeErr(err error) bool { | |||||
return err != nil && (errors.Cause(err) == errPastTime) | |||||
} | |||||
func ErrPastTime() error { | |||||
return errors.WithStack(errPastTime) | |||||
} | |||||
// IsNoPathFoundErr checks whether an error is due to no path of | |||||
// validators in provider from where we are to where we want to be | |||||
func IsNoPathFoundErr(err error) bool { | |||||
return err != nil && (errors.Cause(err) == errNoPathFound) | |||||
} | |||||
func ErrNoPathFound() error { | |||||
return errors.WithStack(errNoPathFound) | |||||
} | |||||
//-------------------------------------------- | |||||
type errHeightMismatch struct { | |||||
h1, h2 int | |||||
} | |||||
func (e errHeightMismatch) Error() string { | |||||
return fmt.Sprintf("Blocks don't match - %d vs %d", e.h1, e.h2) | |||||
} | |||||
// IsHeightMismatchErr checks whether an error is due to data from different blocks | |||||
func IsHeightMismatchErr(err error) bool { | |||||
if err == nil { | |||||
return false | |||||
} | |||||
_, ok := errors.Cause(err).(errHeightMismatch) | |||||
return ok | |||||
} | |||||
// ErrHeightMismatch returns an mismatch error with stack-trace | |||||
func ErrHeightMismatch(h1, h2 int) error { | |||||
return errors.WithStack(errHeightMismatch{h1, h2}) | |||||
} |
@ -0,0 +1,18 @@ | |||||
package errors | |||||
import ( | |||||
"errors" | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
) | |||||
func TestErrorHeight(t *testing.T) { | |||||
e1 := ErrHeightMismatch(2, 3) | |||||
e1.Error() | |||||
assert.True(t, IsHeightMismatchErr(e1)) | |||||
e2 := errors.New("foobar") | |||||
assert.False(t, IsHeightMismatchErr(e2)) | |||||
assert.False(t, IsHeightMismatchErr(nil)) | |||||
} |
@ -0,0 +1,77 @@ | |||||
package files | |||||
import ( | |||||
"encoding/json" | |||||
"os" | |||||
"github.com/pkg/errors" | |||||
wire "github.com/tendermint/go-wire" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
const ( | |||||
// MaxFullCommitSize is the maximum number of bytes we will | |||||
// read in for a full commit to avoid excessive allocations | |||||
// in the deserializer | |||||
MaxFullCommitSize = 1024 * 1024 | |||||
) | |||||
// SaveFullCommit exports the seed in binary / go-wire style | |||||
func SaveFullCommit(fc certifiers.FullCommit, path string) error { | |||||
f, err := os.Create(path) | |||||
if err != nil { | |||||
return errors.WithStack(err) | |||||
} | |||||
defer f.Close() | |||||
var n int | |||||
wire.WriteBinary(fc, f, &n, &err) | |||||
return errors.WithStack(err) | |||||
} | |||||
// SaveFullCommitJSON exports the seed in a json format | |||||
func SaveFullCommitJSON(fc certifiers.FullCommit, path string) error { | |||||
f, err := os.Create(path) | |||||
if err != nil { | |||||
return errors.WithStack(err) | |||||
} | |||||
defer f.Close() | |||||
stream := json.NewEncoder(f) | |||||
err = stream.Encode(fc) | |||||
return errors.WithStack(err) | |||||
} | |||||
func LoadFullCommit(path string) (certifiers.FullCommit, error) { | |||||
var fc certifiers.FullCommit | |||||
f, err := os.Open(path) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
return fc, certerr.ErrCommitNotFound() | |||||
} | |||||
return fc, errors.WithStack(err) | |||||
} | |||||
defer f.Close() | |||||
var n int | |||||
wire.ReadBinaryPtr(&fc, f, MaxFullCommitSize, &n, &err) | |||||
return fc, errors.WithStack(err) | |||||
} | |||||
func LoadFullCommitJSON(path string) (certifiers.FullCommit, error) { | |||||
var fc certifiers.FullCommit | |||||
f, err := os.Open(path) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
return fc, certerr.ErrCommitNotFound() | |||||
} | |||||
return fc, errors.WithStack(err) | |||||
} | |||||
defer f.Close() | |||||
stream := json.NewDecoder(f) | |||||
err = stream.Decode(&fc) | |||||
return fc, errors.WithStack(err) | |||||
} |
@ -0,0 +1,66 @@ | |||||
package files | |||||
import ( | |||||
"os" | |||||
"path/filepath" | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
"github.com/stretchr/testify/require" | |||||
cmn "github.com/tendermint/tmlibs/common" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
) | |||||
func tmpFile() string { | |||||
suffix := cmn.RandStr(16) | |||||
return filepath.Join(os.TempDir(), "fc-test-"+suffix) | |||||
} | |||||
func TestSerializeFullCommits(t *testing.T) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
// some constants | |||||
appHash := []byte("some crazy thing") | |||||
chainID := "ser-ial" | |||||
h := 25 | |||||
// build a fc | |||||
keys := certifiers.GenValKeys(5) | |||||
vals := keys.ToValidators(10, 0) | |||||
fc := keys.GenFullCommit(chainID, h, nil, vals, appHash, 0, 5) | |||||
require.Equal(h, fc.Height()) | |||||
require.Equal(vals.Hash(), fc.ValidatorsHash()) | |||||
// try read/write with json | |||||
jfile := tmpFile() | |||||
defer os.Remove(jfile) | |||||
jseed, err := LoadFullCommitJSON(jfile) | |||||
assert.NotNil(err) | |||||
err = SaveFullCommitJSON(fc, jfile) | |||||
require.Nil(err) | |||||
jseed, err = LoadFullCommitJSON(jfile) | |||||
assert.Nil(err, "%+v", err) | |||||
assert.Equal(h, jseed.Height()) | |||||
assert.Equal(vals.Hash(), jseed.ValidatorsHash()) | |||||
// try read/write with binary | |||||
bfile := tmpFile() | |||||
defer os.Remove(bfile) | |||||
bseed, err := LoadFullCommit(bfile) | |||||
assert.NotNil(err) | |||||
err = SaveFullCommit(fc, bfile) | |||||
require.Nil(err) | |||||
bseed, err = LoadFullCommit(bfile) | |||||
assert.Nil(err, "%+v", err) | |||||
assert.Equal(h, bseed.Height()) | |||||
assert.Equal(vals.Hash(), bseed.ValidatorsHash()) | |||||
// make sure they don't read the other format (different) | |||||
_, err = LoadFullCommit(jfile) | |||||
assert.NotNil(err) | |||||
_, err = LoadFullCommitJSON(bfile) | |||||
assert.NotNil(err) | |||||
} |
@ -0,0 +1,134 @@ | |||||
/* | |||||
Package files defines a Provider that stores all data in the filesystem | |||||
We assume the same validator hash may be reused by many different | |||||
headers/*Commits, and thus store it separately. This leaves us | |||||
with three issues: | |||||
1. Given a validator hash, retrieve the validator set if previously stored | |||||
2. Given a block height, find the *Commit with the highest height <= h | |||||
3. Given a FullCommit, store it quickly to satisfy 1 and 2 | |||||
Note that we do not worry about caching, as that can be achieved by | |||||
pairing this with a MemStoreProvider and CacheProvider from certifiers | |||||
*/ | |||||
package files | |||||
import ( | |||||
"encoding/hex" | |||||
"fmt" | |||||
"math" | |||||
"os" | |||||
"path/filepath" | |||||
"sort" | |||||
"github.com/pkg/errors" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
const ( | |||||
Ext = ".tsd" | |||||
ValDir = "validators" | |||||
CheckDir = "checkpoints" | |||||
dirPerm = os.FileMode(0755) | |||||
filePerm = os.FileMode(0644) | |||||
) | |||||
type provider struct { | |||||
valDir string | |||||
checkDir string | |||||
} | |||||
// NewProvider creates the parent dir and subdirs | |||||
// for validators and checkpoints as needed | |||||
func NewProvider(dir string) certifiers.Provider { | |||||
valDir := filepath.Join(dir, ValDir) | |||||
checkDir := filepath.Join(dir, CheckDir) | |||||
for _, d := range []string{valDir, checkDir} { | |||||
err := os.MkdirAll(d, dirPerm) | |||||
if err != nil { | |||||
panic(err) | |||||
} | |||||
} | |||||
return &provider{valDir: valDir, checkDir: checkDir} | |||||
} | |||||
func (p *provider) encodeHash(hash []byte) string { | |||||
return hex.EncodeToString(hash) + Ext | |||||
} | |||||
func (p *provider) encodeHeight(h int) string { | |||||
// pad up to 10^12 for height... | |||||
return fmt.Sprintf("%012d%s", h, Ext) | |||||
} | |||||
func (p *provider) StoreCommit(fc certifiers.FullCommit) error { | |||||
// make sure the fc is self-consistent before saving | |||||
err := fc.ValidateBasic(fc.Commit.Header.ChainID) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
paths := []string{ | |||||
filepath.Join(p.checkDir, p.encodeHeight(fc.Height())), | |||||
filepath.Join(p.valDir, p.encodeHash(fc.Header.ValidatorsHash)), | |||||
} | |||||
for _, path := range paths { | |||||
err := SaveFullCommit(fc, path) | |||||
// unknown error in creating or writing immediately breaks | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
func (p *provider) GetByHeight(h int) (certifiers.FullCommit, error) { | |||||
// first we look for exact match, then search... | |||||
path := filepath.Join(p.checkDir, p.encodeHeight(h)) | |||||
fc, err := LoadFullCommit(path) | |||||
if certerr.IsCommitNotFoundErr(err) { | |||||
path, err = p.searchForHeight(h) | |||||
if err == nil { | |||||
fc, err = LoadFullCommit(path) | |||||
} | |||||
} | |||||
return fc, err | |||||
} | |||||
func (p *provider) LatestCommit() (fc certifiers.FullCommit, err error) { | |||||
// Note to future: please update by 2077 to avoid rollover | |||||
return p.GetByHeight(math.MaxInt32 - 1) | |||||
} | |||||
// search for height, looks for a file with highest height < h | |||||
// return certifiers.ErrCommitNotFound() if not there... | |||||
func (p *provider) searchForHeight(h int) (string, error) { | |||||
d, err := os.Open(p.checkDir) | |||||
if err != nil { | |||||
return "", errors.WithStack(err) | |||||
} | |||||
files, err := d.Readdirnames(0) | |||||
d.Close() | |||||
if err != nil { | |||||
return "", errors.WithStack(err) | |||||
} | |||||
desired := p.encodeHeight(h) | |||||
sort.Strings(files) | |||||
i := sort.SearchStrings(files, desired) | |||||
if i == 0 { | |||||
return "", certerr.ErrCommitNotFound() | |||||
} | |||||
found := files[i-1] | |||||
path := filepath.Join(p.checkDir, found) | |||||
return path, errors.WithStack(err) | |||||
} | |||||
func (p *provider) GetByHash(hash []byte) (certifiers.FullCommit, error) { | |||||
path := filepath.Join(p.valDir, p.encodeHash(hash)) | |||||
return LoadFullCommit(path) | |||||
} |
@ -0,0 +1,96 @@ | |||||
package files_test | |||||
import ( | |||||
"bytes" | |||||
"errors" | |||||
"io/ioutil" | |||||
"os" | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
"github.com/stretchr/testify/require" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
"github.com/tendermint/tendermint/certifiers/files" | |||||
) | |||||
func checkEqual(stored, loaded certifiers.FullCommit, chainID string) error { | |||||
err := loaded.ValidateBasic(chainID) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if !bytes.Equal(stored.ValidatorsHash(), loaded.ValidatorsHash()) { | |||||
return errors.New("Different block hashes") | |||||
} | |||||
return nil | |||||
} | |||||
func TestFileProvider(t *testing.T) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
dir, err := ioutil.TempDir("", "fileprovider-test") | |||||
assert.Nil(err) | |||||
defer os.RemoveAll(dir) | |||||
p := files.NewProvider(dir) | |||||
chainID := "test-files" | |||||
appHash := []byte("some-data") | |||||
keys := certifiers.GenValKeys(5) | |||||
count := 10 | |||||
// make a bunch of seeds... | |||||
seeds := make([]certifiers.FullCommit, count) | |||||
for i := 0; i < count; i++ { | |||||
// two seeds for each validator, to check how we handle dups | |||||
// (10, 0), (10, 1), (10, 1), (10, 2), (10, 2), ... | |||||
vals := keys.ToValidators(10, int64(count/2)) | |||||
h := 20 + 10*i | |||||
check := keys.GenCommit(chainID, h, nil, vals, appHash, 0, 5) | |||||
seeds[i] = certifiers.NewFullCommit(check, vals) | |||||
} | |||||
// check provider is empty | |||||
seed, err := p.GetByHeight(20) | |||||
require.NotNil(err) | |||||
assert.True(certerr.IsCommitNotFoundErr(err)) | |||||
seed, err = p.GetByHash(seeds[3].ValidatorsHash()) | |||||
require.NotNil(err) | |||||
assert.True(certerr.IsCommitNotFoundErr(err)) | |||||
// now add them all to the provider | |||||
for _, s := range seeds { | |||||
err = p.StoreCommit(s) | |||||
require.Nil(err) | |||||
// and make sure we can get it back | |||||
s2, err := p.GetByHash(s.ValidatorsHash()) | |||||
assert.Nil(err) | |||||
err = checkEqual(s, s2, chainID) | |||||
assert.Nil(err) | |||||
// by height as well | |||||
s2, err = p.GetByHeight(s.Height()) | |||||
err = checkEqual(s, s2, chainID) | |||||
assert.Nil(err) | |||||
} | |||||
// make sure we get the last hash if we overstep | |||||
seed, err = p.GetByHeight(5000) | |||||
if assert.Nil(err, "%+v", err) { | |||||
assert.Equal(seeds[count-1].Height(), seed.Height()) | |||||
err = checkEqual(seeds[count-1], seed, chainID) | |||||
assert.Nil(err) | |||||
} | |||||
// and middle ones as well | |||||
seed, err = p.GetByHeight(47) | |||||
if assert.Nil(err, "%+v", err) { | |||||
// we only step by 10, so 40 must be the one below this | |||||
assert.Equal(40, seed.Height()) | |||||
} | |||||
// and proper error for too low | |||||
_, err = p.GetByHeight(5) | |||||
assert.NotNil(err) | |||||
assert.True(certerr.IsCommitNotFoundErr(err)) | |||||
} |
@ -0,0 +1,149 @@ | |||||
package certifiers | |||||
import ( | |||||
"time" | |||||
crypto "github.com/tendermint/go-crypto" | |||||
"github.com/tendermint/tendermint/types" | |||||
) | |||||
// ValKeys is a helper for testing. | |||||
// | |||||
// It lets us simulate signing with many keys, either ed25519 or secp256k1. | |||||
// The main use case is to create a set, and call GenCommit | |||||
// to get propely signed header for testing. | |||||
// | |||||
// You can set different weights of validators each time you call | |||||
// ToValidators, and can optionally extend the validator set later | |||||
// with Extend or ExtendSecp | |||||
type ValKeys []crypto.PrivKey | |||||
// GenValKeys produces an array of private keys to generate commits | |||||
func GenValKeys(n int) ValKeys { | |||||
res := make(ValKeys, n) | |||||
for i := range res { | |||||
res[i] = crypto.GenPrivKeyEd25519().Wrap() | |||||
} | |||||
return res | |||||
} | |||||
// Change replaces the key at index i | |||||
func (v ValKeys) Change(i int) ValKeys { | |||||
res := make(ValKeys, len(v)) | |||||
copy(res, v) | |||||
res[i] = crypto.GenPrivKeyEd25519().Wrap() | |||||
return res | |||||
} | |||||
// Extend adds n more keys (to remove, just take a slice) | |||||
func (v ValKeys) Extend(n int) ValKeys { | |||||
extra := GenValKeys(n) | |||||
return append(v, extra...) | |||||
} | |||||
// GenSecpValKeys produces an array of secp256k1 private keys to generate commits | |||||
func GenSecpValKeys(n int) ValKeys { | |||||
res := make(ValKeys, n) | |||||
for i := range res { | |||||
res[i] = crypto.GenPrivKeySecp256k1().Wrap() | |||||
} | |||||
return res | |||||
} | |||||
// ExtendSecp adds n more secp256k1 keys (to remove, just take a slice) | |||||
func (v ValKeys) ExtendSecp(n int) ValKeys { | |||||
extra := GenSecpValKeys(n) | |||||
return append(v, extra...) | |||||
} | |||||
// ToValidators produces a list of validators from the set of keys | |||||
// The first key has weight `init` and it increases by `inc` every step | |||||
// so we can have all the same weight, or a simple linear distribution | |||||
// (should be enough for testing) | |||||
func (v ValKeys) ToValidators(init, inc int64) *types.ValidatorSet { | |||||
res := make([]*types.Validator, len(v)) | |||||
for i, k := range v { | |||||
res[i] = types.NewValidator(k.PubKey(), init+int64(i)*inc) | |||||
} | |||||
return types.NewValidatorSet(res) | |||||
} | |||||
// signHeader properly signs the header with all keys from first to last exclusive | |||||
func (v ValKeys) signHeader(header *types.Header, first, last int) *types.Commit { | |||||
votes := make([]*types.Vote, len(v)) | |||||
// we need this list to keep the ordering... | |||||
vset := v.ToValidators(1, 0) | |||||
// fill in the votes we want | |||||
for i := first; i < last; i++ { | |||||
vote := makeVote(header, vset, v[i]) | |||||
votes[vote.ValidatorIndex] = vote | |||||
} | |||||
res := &types.Commit{ | |||||
BlockID: types.BlockID{Hash: header.Hash()}, | |||||
Precommits: votes, | |||||
} | |||||
return res | |||||
} | |||||
func makeVote(header *types.Header, vals *types.ValidatorSet, key crypto.PrivKey) *types.Vote { | |||||
addr := key.PubKey().Address() | |||||
idx, _ := vals.GetByAddress(addr) | |||||
vote := &types.Vote{ | |||||
ValidatorAddress: addr, | |||||
ValidatorIndex: idx, | |||||
Height: header.Height, | |||||
Round: 1, | |||||
Type: types.VoteTypePrecommit, | |||||
BlockID: types.BlockID{Hash: header.Hash()}, | |||||
} | |||||
// Sign it | |||||
signBytes := types.SignBytes(header.ChainID, vote) | |||||
vote.Signature = key.Sign(signBytes) | |||||
return vote | |||||
} | |||||
func genHeader(chainID string, height int, txs types.Txs, | |||||
vals *types.ValidatorSet, appHash []byte) *types.Header { | |||||
return &types.Header{ | |||||
ChainID: chainID, | |||||
Height: height, | |||||
Time: time.Now(), | |||||
NumTxs: len(txs), | |||||
// LastBlockID | |||||
// LastCommitHash | |||||
ValidatorsHash: vals.Hash(), | |||||
DataHash: txs.Hash(), | |||||
AppHash: appHash, | |||||
} | |||||
} | |||||
// GenCommit calls genHeader and signHeader and combines them into a *Commit | |||||
func (v ValKeys) GenCommit(chainID string, height int, txs types.Txs, | |||||
vals *types.ValidatorSet, appHash []byte, first, last int) *Commit { | |||||
header := genHeader(chainID, height, txs, vals, appHash) | |||||
check := &Commit{ | |||||
Header: header, | |||||
Commit: v.signHeader(header, first, last), | |||||
CanonicalCommit: true, | |||||
} | |||||
return check | |||||
} | |||||
// GenFullCommit calls genHeader and signHeader and combines them into a *Commit | |||||
func (v ValKeys) GenFullCommit(chainID string, height int, txs types.Txs, | |||||
vals *types.ValidatorSet, appHash []byte, first, last int) FullCommit { | |||||
header := genHeader(chainID, height, txs, vals, appHash) | |||||
commit := &Commit{ | |||||
Header: header, | |||||
Commit: v.signHeader(header, first, last), | |||||
CanonicalCommit: true, | |||||
} | |||||
return NewFullCommit(commit, vals) | |||||
} |
@ -0,0 +1,142 @@ | |||||
package certifiers | |||||
import ( | |||||
"github.com/tendermint/tendermint/types" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
type Inquiring struct { | |||||
cert *Dynamic | |||||
// These are only properly validated data, from local system | |||||
trusted Provider | |||||
// This is a source of new info, like a node rpc, or other import method | |||||
Source Provider | |||||
} | |||||
func NewInquiring(chainID string, fc FullCommit, trusted Provider, source Provider) *Inquiring { | |||||
// store the data in trusted | |||||
trusted.StoreCommit(fc) | |||||
return &Inquiring{ | |||||
cert: NewDynamic(chainID, fc.Validators, fc.Height()), | |||||
trusted: trusted, | |||||
Source: source, | |||||
} | |||||
} | |||||
func (c *Inquiring) ChainID() string { | |||||
return c.cert.ChainID() | |||||
} | |||||
func (c *Inquiring) Validators() *types.ValidatorSet { | |||||
return c.cert.cert.vSet | |||||
} | |||||
func (c *Inquiring) LastHeight() int { | |||||
return c.cert.lastHeight | |||||
} | |||||
// Certify makes sure this is checkpoint is valid. | |||||
// | |||||
// If the validators have changed since the last know time, it looks | |||||
// for a path to prove the new validators. | |||||
// | |||||
// On success, it will store the checkpoint in the store for later viewing | |||||
func (c *Inquiring) Certify(commit *Commit) error { | |||||
err := c.useClosestTrust(commit.Height()) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = c.cert.Certify(commit) | |||||
if !certerr.IsValidatorsChangedErr(err) { | |||||
return err | |||||
} | |||||
err = c.updateToHash(commit.Header.ValidatorsHash) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = c.cert.Certify(commit) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// store the new checkpoint | |||||
c.trusted.StoreCommit( | |||||
NewFullCommit(commit, c.Validators())) | |||||
return nil | |||||
} | |||||
func (c *Inquiring) Update(fc FullCommit) error { | |||||
err := c.useClosestTrust(fc.Height()) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = c.cert.Update(fc) | |||||
if err == nil { | |||||
c.trusted.StoreCommit(fc) | |||||
} | |||||
return err | |||||
} | |||||
func (c *Inquiring) useClosestTrust(h int) error { | |||||
closest, err := c.trusted.GetByHeight(h) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// if the best seed is not the one we currently use, | |||||
// let's just reset the dynamic validator | |||||
if closest.Height() != c.LastHeight() { | |||||
c.cert = NewDynamic(c.ChainID(), closest.Validators, closest.Height()) | |||||
} | |||||
return nil | |||||
} | |||||
// updateToHash gets the validator hash we want to update to | |||||
// if IsTooMuchChangeErr, we try to find a path by binary search over height | |||||
func (c *Inquiring) updateToHash(vhash []byte) error { | |||||
// try to get the match, and update | |||||
fc, err := c.Source.GetByHash(vhash) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = c.cert.Update(fc) | |||||
// handle IsTooMuchChangeErr by using divide and conquer | |||||
if certerr.IsTooMuchChangeErr(err) { | |||||
err = c.updateToHeight(fc.Height()) | |||||
} | |||||
return err | |||||
} | |||||
// updateToHeight will use divide-and-conquer to find a path to h | |||||
func (c *Inquiring) updateToHeight(h int) error { | |||||
// try to update to this height (with checks) | |||||
fc, err := c.Source.GetByHeight(h) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
start, end := c.LastHeight(), fc.Height() | |||||
if end <= start { | |||||
return certerr.ErrNoPathFound() | |||||
} | |||||
err = c.Update(fc) | |||||
// we can handle IsTooMuchChangeErr specially | |||||
if !certerr.IsTooMuchChangeErr(err) { | |||||
return err | |||||
} | |||||
// try to update to mid | |||||
mid := (start + end) / 2 | |||||
err = c.updateToHeight(mid) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// if we made it to mid, we recurse | |||||
return c.updateToHeight(h) | |||||
} |
@ -0,0 +1,165 @@ | |||||
package certifiers_test | |||||
import ( | |||||
"fmt" | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
"github.com/stretchr/testify/require" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
) | |||||
func TestInquirerValidPath(t *testing.T) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
trust := certifiers.NewMemStoreProvider() | |||||
source := certifiers.NewMemStoreProvider() | |||||
// set up the validators to generate test blocks | |||||
var vote int64 = 10 | |||||
keys := certifiers.GenValKeys(5) | |||||
vals := keys.ToValidators(vote, 0) | |||||
// construct a bunch of commits, each with one more height than the last | |||||
chainID := "inquiry-test" | |||||
count := 50 | |||||
commits := make([]certifiers.FullCommit, count) | |||||
for i := 0; i < count; i++ { | |||||
// extend the keys by 1 each time | |||||
keys = keys.Extend(1) | |||||
vals = keys.ToValidators(vote, 0) | |||||
h := 20 + 10*i | |||||
appHash := []byte(fmt.Sprintf("h=%d", h)) | |||||
commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, 0, len(keys)) | |||||
} | |||||
// initialize a certifier with the initial state | |||||
cert := certifiers.NewInquiring(chainID, commits[0], trust, source) | |||||
// this should fail validation.... | |||||
commit := commits[count-1].Commit | |||||
err := cert.Certify(commit) | |||||
require.NotNil(err) | |||||
// add a few seed in the middle should be insufficient | |||||
for i := 10; i < 13; i++ { | |||||
err := source.StoreCommit(commits[i]) | |||||
require.Nil(err) | |||||
} | |||||
err = cert.Certify(commit) | |||||
assert.NotNil(err) | |||||
// with more info, we succeed | |||||
for i := 0; i < count; i++ { | |||||
err := source.StoreCommit(commits[i]) | |||||
require.Nil(err) | |||||
} | |||||
err = cert.Certify(commit) | |||||
assert.Nil(err, "%+v", err) | |||||
} | |||||
func TestInquirerMinimalPath(t *testing.T) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
trust := certifiers.NewMemStoreProvider() | |||||
source := certifiers.NewMemStoreProvider() | |||||
// set up the validators to generate test blocks | |||||
var vote int64 = 10 | |||||
keys := certifiers.GenValKeys(5) | |||||
vals := keys.ToValidators(vote, 0) | |||||
// construct a bunch of commits, each with one more height than the last | |||||
chainID := "minimal-path" | |||||
count := 12 | |||||
commits := make([]certifiers.FullCommit, count) | |||||
for i := 0; i < count; i++ { | |||||
// extend the validators, so we are just below 2/3 | |||||
keys = keys.Extend(len(keys)/2 - 1) | |||||
vals = keys.ToValidators(vote, 0) | |||||
h := 5 + 10*i | |||||
appHash := []byte(fmt.Sprintf("h=%d", h)) | |||||
commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, 0, len(keys)) | |||||
} | |||||
// initialize a certifier with the initial state | |||||
cert := certifiers.NewInquiring(chainID, commits[0], trust, source) | |||||
// this should fail validation.... | |||||
commit := commits[count-1].Commit | |||||
err := cert.Certify(commit) | |||||
require.NotNil(err) | |||||
// add a few seed in the middle should be insufficient | |||||
for i := 5; i < 8; i++ { | |||||
err := source.StoreCommit(commits[i]) | |||||
require.Nil(err) | |||||
} | |||||
err = cert.Certify(commit) | |||||
assert.NotNil(err) | |||||
// with more info, we succeed | |||||
for i := 0; i < count; i++ { | |||||
err := source.StoreCommit(commits[i]) | |||||
require.Nil(err) | |||||
} | |||||
err = cert.Certify(commit) | |||||
assert.Nil(err, "%+v", err) | |||||
} | |||||
func TestInquirerVerifyHistorical(t *testing.T) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
trust := certifiers.NewMemStoreProvider() | |||||
source := certifiers.NewMemStoreProvider() | |||||
// set up the validators to generate test blocks | |||||
var vote int64 = 10 | |||||
keys := certifiers.GenValKeys(5) | |||||
vals := keys.ToValidators(vote, 0) | |||||
// construct a bunch of commits, each with one more height than the last | |||||
chainID := "inquiry-test" | |||||
count := 10 | |||||
commits := make([]certifiers.FullCommit, count) | |||||
for i := 0; i < count; i++ { | |||||
// extend the keys by 1 each time | |||||
keys = keys.Extend(1) | |||||
vals = keys.ToValidators(vote, 0) | |||||
h := 20 + 10*i | |||||
appHash := []byte(fmt.Sprintf("h=%d", h)) | |||||
commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, 0, len(keys)) | |||||
} | |||||
// initialize a certifier with the initial state | |||||
cert := certifiers.NewInquiring(chainID, commits[0], trust, source) | |||||
// store a few commits as trust | |||||
for _, i := range []int{2, 5} { | |||||
trust.StoreCommit(commits[i]) | |||||
} | |||||
// let's see if we can jump forward using trusted commits | |||||
err := source.StoreCommit(commits[7]) | |||||
require.Nil(err, "%+v", err) | |||||
check := commits[7].Commit | |||||
err = cert.Certify(check) | |||||
require.Nil(err, "%+v", err) | |||||
assert.Equal(check.Height(), cert.LastHeight()) | |||||
// add access to all commits via untrusted source | |||||
for i := 0; i < count; i++ { | |||||
err := source.StoreCommit(commits[i]) | |||||
require.Nil(err) | |||||
} | |||||
// try to check an unknown seed in the past | |||||
mid := commits[3].Commit | |||||
err = cert.Certify(mid) | |||||
require.Nil(err, "%+v", err) | |||||
assert.Equal(mid.Height(), cert.LastHeight()) | |||||
// and jump all the way forward again | |||||
end := commits[count-1].Commit | |||||
err = cert.Certify(end) | |||||
require.Nil(err, "%+v", err) | |||||
assert.Equal(end.Height(), cert.LastHeight()) | |||||
} |
@ -0,0 +1,78 @@ | |||||
package certifiers | |||||
import ( | |||||
"encoding/hex" | |||||
"sort" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
type memStoreProvider struct { | |||||
// byHeight is always sorted by Height... need to support range search (nil, h] | |||||
// btree would be more efficient for larger sets | |||||
byHeight fullCommits | |||||
byHash map[string]FullCommit | |||||
} | |||||
// fullCommits just exists to allow easy sorting | |||||
type fullCommits []FullCommit | |||||
func (s fullCommits) Len() int { return len(s) } | |||||
func (s fullCommits) Swap(i, j int) { s[i], s[j] = s[j], s[i] } | |||||
func (s fullCommits) Less(i, j int) bool { | |||||
return s[i].Height() < s[j].Height() | |||||
} | |||||
func NewMemStoreProvider() Provider { | |||||
return &memStoreProvider{ | |||||
byHeight: fullCommits{}, | |||||
byHash: map[string]FullCommit{}, | |||||
} | |||||
} | |||||
func (m *memStoreProvider) encodeHash(hash []byte) string { | |||||
return hex.EncodeToString(hash) | |||||
} | |||||
func (m *memStoreProvider) StoreCommit(fc FullCommit) error { | |||||
// make sure the fc is self-consistent before saving | |||||
err := fc.ValidateBasic(fc.Commit.Header.ChainID) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// store the valid fc | |||||
key := m.encodeHash(fc.ValidatorsHash()) | |||||
m.byHash[key] = fc | |||||
m.byHeight = append(m.byHeight, fc) | |||||
sort.Sort(m.byHeight) | |||||
return nil | |||||
} | |||||
func (m *memStoreProvider) GetByHeight(h int) (FullCommit, error) { | |||||
// search from highest to lowest | |||||
for i := len(m.byHeight) - 1; i >= 0; i-- { | |||||
fc := m.byHeight[i] | |||||
if fc.Height() <= h { | |||||
return fc, nil | |||||
} | |||||
} | |||||
return FullCommit{}, certerr.ErrCommitNotFound() | |||||
} | |||||
func (m *memStoreProvider) GetByHash(hash []byte) (FullCommit, error) { | |||||
var err error | |||||
fc, ok := m.byHash[m.encodeHash(hash)] | |||||
if !ok { | |||||
err = certerr.ErrCommitNotFound() | |||||
} | |||||
return fc, err | |||||
} | |||||
func (m *memStoreProvider) LatestCommit() (FullCommit, error) { | |||||
l := len(m.byHeight) | |||||
if l == 0 { | |||||
return FullCommit{}, certerr.ErrCommitNotFound() | |||||
} | |||||
return m.byHeight[l-1], nil | |||||
} |
@ -0,0 +1,116 @@ | |||||
package certifiers_test | |||||
import ( | |||||
"fmt" | |||||
"testing" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
) | |||||
func BenchmarkGenCommit20(b *testing.B) { | |||||
keys := certifiers.GenValKeys(20) | |||||
benchmarkGenCommit(b, keys) | |||||
} | |||||
func BenchmarkGenCommit100(b *testing.B) { | |||||
keys := certifiers.GenValKeys(100) | |||||
benchmarkGenCommit(b, keys) | |||||
} | |||||
func BenchmarkGenCommitSec20(b *testing.B) { | |||||
keys := certifiers.GenSecpValKeys(20) | |||||
benchmarkGenCommit(b, keys) | |||||
} | |||||
func BenchmarkGenCommitSec100(b *testing.B) { | |||||
keys := certifiers.GenSecpValKeys(100) | |||||
benchmarkGenCommit(b, keys) | |||||
} | |||||
func benchmarkGenCommit(b *testing.B, keys certifiers.ValKeys) { | |||||
chainID := fmt.Sprintf("bench-%d", len(keys)) | |||||
vals := keys.ToValidators(20, 10) | |||||
for i := 0; i < b.N; i++ { | |||||
h := 1 + i | |||||
appHash := []byte(fmt.Sprintf("h=%d", h)) | |||||
keys.GenCommit(chainID, h, nil, vals, appHash, 0, len(keys)) | |||||
} | |||||
} | |||||
// this benchmarks generating one key | |||||
func BenchmarkGenValKeys(b *testing.B) { | |||||
keys := certifiers.GenValKeys(20) | |||||
for i := 0; i < b.N; i++ { | |||||
keys = keys.Extend(1) | |||||
} | |||||
} | |||||
// this benchmarks generating one key | |||||
func BenchmarkGenSecpValKeys(b *testing.B) { | |||||
keys := certifiers.GenSecpValKeys(20) | |||||
for i := 0; i < b.N; i++ { | |||||
keys = keys.Extend(1) | |||||
} | |||||
} | |||||
func BenchmarkToValidators20(b *testing.B) { | |||||
benchmarkToValidators(b, 20) | |||||
} | |||||
func BenchmarkToValidators100(b *testing.B) { | |||||
benchmarkToValidators(b, 100) | |||||
} | |||||
// this benchmarks constructing the validator set (.PubKey() * nodes) | |||||
func benchmarkToValidators(b *testing.B, nodes int) { | |||||
keys := certifiers.GenValKeys(nodes) | |||||
for i := 1; i <= b.N; i++ { | |||||
keys.ToValidators(int64(2*i), int64(i)) | |||||
} | |||||
} | |||||
func BenchmarkToValidatorsSec100(b *testing.B) { | |||||
benchmarkToValidatorsSec(b, 100) | |||||
} | |||||
// this benchmarks constructing the validator set (.PubKey() * nodes) | |||||
func benchmarkToValidatorsSec(b *testing.B, nodes int) { | |||||
keys := certifiers.GenSecpValKeys(nodes) | |||||
for i := 1; i <= b.N; i++ { | |||||
keys.ToValidators(int64(2*i), int64(i)) | |||||
} | |||||
} | |||||
func BenchmarkCertifyCommit20(b *testing.B) { | |||||
keys := certifiers.GenValKeys(20) | |||||
benchmarkCertifyCommit(b, keys) | |||||
} | |||||
func BenchmarkCertifyCommit100(b *testing.B) { | |||||
keys := certifiers.GenValKeys(100) | |||||
benchmarkCertifyCommit(b, keys) | |||||
} | |||||
func BenchmarkCertifyCommitSec20(b *testing.B) { | |||||
keys := certifiers.GenSecpValKeys(20) | |||||
benchmarkCertifyCommit(b, keys) | |||||
} | |||||
func BenchmarkCertifyCommitSec100(b *testing.B) { | |||||
keys := certifiers.GenSecpValKeys(100) | |||||
benchmarkCertifyCommit(b, keys) | |||||
} | |||||
func benchmarkCertifyCommit(b *testing.B, keys certifiers.ValKeys) { | |||||
chainID := "bench-certify" | |||||
vals := keys.ToValidators(20, 10) | |||||
cert := certifiers.NewStatic(chainID, vals) | |||||
check := keys.GenCommit(chainID, 123, nil, vals, []byte("foo"), 0, len(keys)) | |||||
for i := 0; i < b.N; i++ { | |||||
err := cert.Certify(check) | |||||
if err != nil { | |||||
panic(err) | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,125 @@ | |||||
package certifiers | |||||
import ( | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
// Provider is used to get more validators by other means | |||||
// | |||||
// Examples: MemProvider, files.Provider, client.Provider.... | |||||
type Provider interface { | |||||
// StoreCommit saves a FullCommit after we have verified it, | |||||
// so we can query for it later. Important for updating our | |||||
// store of trusted commits | |||||
StoreCommit(fc FullCommit) error | |||||
// GetByHeight returns the closest commit with height <= h | |||||
GetByHeight(h int) (FullCommit, error) | |||||
// GetByHash returns a commit exactly matching this validator hash | |||||
GetByHash(hash []byte) (FullCommit, error) | |||||
// LatestCommit returns the newest commit stored | |||||
LatestCommit() (FullCommit, error) | |||||
} | |||||
// cacheProvider allows you to place one or more caches in front of a source | |||||
// Provider. It runs through them in order until a match is found. | |||||
// So you can keep a local cache, and check with the network if | |||||
// no data is there. | |||||
type cacheProvider struct { | |||||
Providers []Provider | |||||
} | |||||
func NewCacheProvider(providers ...Provider) Provider { | |||||
return cacheProvider{ | |||||
Providers: providers, | |||||
} | |||||
} | |||||
// StoreCommit tries to add the seed to all providers. | |||||
// | |||||
// Aborts on first error it encounters (closest provider) | |||||
func (c cacheProvider) StoreCommit(fc FullCommit) (err error) { | |||||
for _, p := range c.Providers { | |||||
err = p.StoreCommit(fc) | |||||
if err != nil { | |||||
break | |||||
} | |||||
} | |||||
return err | |||||
} | |||||
/* | |||||
GetByHeight should return the closest possible match from all providers. | |||||
The Cache is usually organized in order from cheapest call (memory) | |||||
to most expensive calls (disk/network). However, since GetByHeight returns | |||||
a FullCommit at h' <= h, if the memory has a seed at h-10, but the network would | |||||
give us the exact match, a naive "stop at first non-error" would hide | |||||
the actual desired results. | |||||
Thus, we query each provider in order until we find an exact match | |||||
or we finished querying them all. If at least one returned a non-error, | |||||
then this returns the best match (minimum h-h'). | |||||
*/ | |||||
func (c cacheProvider) GetByHeight(h int) (fc FullCommit, err error) { | |||||
for _, p := range c.Providers { | |||||
var tfc FullCommit | |||||
tfc, err = p.GetByHeight(h) | |||||
if err == nil { | |||||
if tfc.Height() > fc.Height() { | |||||
fc = tfc | |||||
} | |||||
if tfc.Height() == h { | |||||
break | |||||
} | |||||
} | |||||
} | |||||
// even if the last one had an error, if any was a match, this is good | |||||
if fc.Height() > 0 { | |||||
err = nil | |||||
} | |||||
return fc, err | |||||
} | |||||
func (c cacheProvider) GetByHash(hash []byte) (fc FullCommit, err error) { | |||||
for _, p := range c.Providers { | |||||
fc, err = p.GetByHash(hash) | |||||
if err == nil { | |||||
break | |||||
} | |||||
} | |||||
return fc, err | |||||
} | |||||
func (c cacheProvider) LatestCommit() (fc FullCommit, err error) { | |||||
for _, p := range c.Providers { | |||||
var tfc FullCommit | |||||
tfc, err = p.LatestCommit() | |||||
if err == nil && tfc.Height() > fc.Height() { | |||||
fc = tfc | |||||
} | |||||
} | |||||
// even if the last one had an error, if any was a match, this is good | |||||
if fc.Height() > 0 { | |||||
err = nil | |||||
} | |||||
return fc, err | |||||
} | |||||
// missingProvider doens't store anything, always a miss | |||||
// Designed as a mock for testing | |||||
type missingProvider struct{} | |||||
func NewMissingProvider() Provider { | |||||
return missingProvider{} | |||||
} | |||||
func (missingProvider) StoreCommit(_ FullCommit) error { return nil } | |||||
func (missingProvider) GetByHeight(_ int) (FullCommit, error) { | |||||
return FullCommit{}, certerr.ErrCommitNotFound() | |||||
} | |||||
func (missingProvider) GetByHash(_ []byte) (FullCommit, error) { | |||||
return FullCommit{}, certerr.ErrCommitNotFound() | |||||
} | |||||
func (missingProvider) LatestCommit() (FullCommit, error) { | |||||
return FullCommit{}, certerr.ErrCommitNotFound() | |||||
} |
@ -0,0 +1,128 @@ | |||||
package certifiers_test | |||||
import ( | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
"github.com/stretchr/testify/require" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
"github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
func TestMemProvider(t *testing.T) { | |||||
p := certifiers.NewMemStoreProvider() | |||||
checkProvider(t, p, "test-mem", "empty") | |||||
} | |||||
func TestCacheProvider(t *testing.T) { | |||||
p := certifiers.NewCacheProvider( | |||||
certifiers.NewMissingProvider(), | |||||
certifiers.NewMemStoreProvider(), | |||||
certifiers.NewMissingProvider(), | |||||
) | |||||
checkProvider(t, p, "test-cache", "kjfhekfhkewhgit") | |||||
} | |||||
func checkProvider(t *testing.T, p certifiers.Provider, chainID, app string) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
appHash := []byte(app) | |||||
keys := certifiers.GenValKeys(5) | |||||
count := 10 | |||||
// make a bunch of commits... | |||||
commits := make([]certifiers.FullCommit, count) | |||||
for i := 0; i < count; i++ { | |||||
// two commits for each validator, to check how we handle dups | |||||
// (10, 0), (10, 1), (10, 1), (10, 2), (10, 2), ... | |||||
vals := keys.ToValidators(10, int64(count/2)) | |||||
h := 20 + 10*i | |||||
commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, 0, 5) | |||||
} | |||||
// check provider is empty | |||||
fc, err := p.GetByHeight(20) | |||||
require.NotNil(err) | |||||
assert.True(errors.IsCommitNotFoundErr(err)) | |||||
fc, err = p.GetByHash(commits[3].ValidatorsHash()) | |||||
require.NotNil(err) | |||||
assert.True(errors.IsCommitNotFoundErr(err)) | |||||
// now add them all to the provider | |||||
for _, s := range commits { | |||||
err = p.StoreCommit(s) | |||||
require.Nil(err) | |||||
// and make sure we can get it back | |||||
s2, err := p.GetByHash(s.ValidatorsHash()) | |||||
assert.Nil(err) | |||||
assert.Equal(s, s2) | |||||
// by height as well | |||||
s2, err = p.GetByHeight(s.Height()) | |||||
assert.Nil(err) | |||||
assert.Equal(s, s2) | |||||
} | |||||
// make sure we get the last hash if we overstep | |||||
fc, err = p.GetByHeight(5000) | |||||
if assert.Nil(err) { | |||||
assert.Equal(commits[count-1].Height(), fc.Height()) | |||||
assert.Equal(commits[count-1], fc) | |||||
} | |||||
// and middle ones as well | |||||
fc, err = p.GetByHeight(47) | |||||
if assert.Nil(err) { | |||||
// we only step by 10, so 40 must be the one below this | |||||
assert.Equal(40, fc.Height()) | |||||
} | |||||
} | |||||
// this will make a get height, and if it is good, set the data as well | |||||
func checkGetHeight(t *testing.T, p certifiers.Provider, ask, expect int) { | |||||
fc, err := p.GetByHeight(ask) | |||||
require.Nil(t, err, "%+v", err) | |||||
if assert.Equal(t, expect, fc.Height()) { | |||||
err = p.StoreCommit(fc) | |||||
require.Nil(t, err, "%+v", err) | |||||
} | |||||
} | |||||
func TestCacheGetsBestHeight(t *testing.T) { | |||||
// assert, require := assert.New(t), require.New(t) | |||||
require := require.New(t) | |||||
// we will write data to the second level of the cache (p2), | |||||
// and see what gets cached, stored in | |||||
p := certifiers.NewMemStoreProvider() | |||||
p2 := certifiers.NewMemStoreProvider() | |||||
cp := certifiers.NewCacheProvider(p, p2) | |||||
chainID := "cache-best-height" | |||||
appHash := []byte("01234567") | |||||
keys := certifiers.GenValKeys(5) | |||||
count := 10 | |||||
// set a bunch of commits | |||||
for i := 0; i < count; i++ { | |||||
vals := keys.ToValidators(10, int64(count/2)) | |||||
h := 10 * (i + 1) | |||||
fc := keys.GenFullCommit(chainID, h, nil, vals, appHash, 0, 5) | |||||
err := p2.StoreCommit(fc) | |||||
require.NoError(err) | |||||
} | |||||
// let's get a few heights from the cache and set them proper | |||||
checkGetHeight(t, cp, 57, 50) | |||||
checkGetHeight(t, cp, 33, 30) | |||||
// make sure they are set in p as well (but nothing else) | |||||
checkGetHeight(t, p, 44, 30) | |||||
checkGetHeight(t, p, 50, 50) | |||||
checkGetHeight(t, p, 99, 50) | |||||
// now, query the cache for a higher value | |||||
checkGetHeight(t, p2, 99, 90) | |||||
checkGetHeight(t, cp, 99, 90) | |||||
} |
@ -0,0 +1,66 @@ | |||||
package certifiers | |||||
import ( | |||||
"bytes" | |||||
"github.com/pkg/errors" | |||||
"github.com/tendermint/tendermint/types" | |||||
certerr "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
var _ Certifier = &Static{} | |||||
// Static assumes a static set of validators, set on | |||||
// initilization and checks against them. | |||||
// The signatures on every header is checked for > 2/3 votes | |||||
// against the known validator set upon Certify | |||||
// | |||||
// Good for testing or really simple chains. Building block | |||||
// to support real-world functionality. | |||||
type Static struct { | |||||
chainID string | |||||
vSet *types.ValidatorSet | |||||
vhash []byte | |||||
} | |||||
func NewStatic(chainID string, vals *types.ValidatorSet) *Static { | |||||
return &Static{ | |||||
chainID: chainID, | |||||
vSet: vals, | |||||
} | |||||
} | |||||
func (c *Static) ChainID() string { | |||||
return c.chainID | |||||
} | |||||
func (c *Static) Validators() *types.ValidatorSet { | |||||
return c.vSet | |||||
} | |||||
func (c *Static) Hash() []byte { | |||||
if len(c.vhash) == 0 { | |||||
c.vhash = c.vSet.Hash() | |||||
} | |||||
return c.vhash | |||||
} | |||||
func (c *Static) Certify(commit *Commit) error { | |||||
// do basic sanity checks | |||||
err := commit.ValidateBasic(c.chainID) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// make sure it has the same validator set we have (static means static) | |||||
if !bytes.Equal(c.Hash(), commit.Header.ValidatorsHash) { | |||||
return certerr.ErrValidatorsChanged() | |||||
} | |||||
// then make sure we have the proper signatures for this | |||||
err = c.vSet.VerifyCommit(c.chainID, commit.Commit.BlockID, | |||||
commit.Header.Height, commit.Commit) | |||||
return errors.WithStack(err) | |||||
} |
@ -0,0 +1,59 @@ | |||||
package certifiers_test | |||||
import ( | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
"github.com/tendermint/tendermint/types" | |||||
"github.com/tendermint/tendermint/certifiers" | |||||
errors "github.com/tendermint/tendermint/certifiers/errors" | |||||
) | |||||
func TestStaticCert(t *testing.T) { | |||||
// assert, require := assert.New(t), require.New(t) | |||||
assert := assert.New(t) | |||||
// require := require.New(t) | |||||
keys := certifiers.GenValKeys(4) | |||||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do! | |||||
vals := keys.ToValidators(20, 10) | |||||
// and a certifier based on our known set | |||||
chainID := "test-static" | |||||
cert := certifiers.NewStatic(chainID, vals) | |||||
cases := []struct { | |||||
keys certifiers.ValKeys | |||||
vals *types.ValidatorSet | |||||
height int | |||||
first, last int // who actually signs | |||||
proper bool // true -> expect no error | |||||
changed bool // true -> expect validator change error | |||||
}{ | |||||
// perfect, signed by everyone | |||||
{keys, vals, 1, 0, len(keys), true, false}, | |||||
// skip little guy is okay | |||||
{keys, vals, 2, 1, len(keys), true, false}, | |||||
// but not the big guy | |||||
{keys, vals, 3, 0, len(keys) - 1, false, false}, | |||||
// even changing the power a little bit breaks the static validator | |||||
// the sigs are enough, but the validator hash is unknown | |||||
{keys, keys.ToValidators(20, 11), 4, 0, len(keys), false, true}, | |||||
} | |||||
for _, tc := range cases { | |||||
check := tc.keys.GenCommit(chainID, tc.height, nil, tc.vals, | |||||
[]byte("foo"), tc.first, tc.last) | |||||
err := cert.Certify(check) | |||||
if tc.proper { | |||||
assert.Nil(err, "%+v", err) | |||||
} else { | |||||
assert.NotNil(err) | |||||
if tc.changed { | |||||
assert.True(errors.IsValidatorsChangedErr(err), "%+v", err) | |||||
} | |||||
} | |||||
} | |||||
} |