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