This PR removes lite & renames lite2 to light throughout the repo Signed-off-by: Marko Baricevic <marbar3778@yahoo.com> Closes: #4944pull/4948/head
@ -1,79 +0,0 @@ | |||
package lite | |||
import ( | |||
"bytes" | |||
"github.com/pkg/errors" | |||
lerr "github.com/tendermint/tendermint/lite/errors" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var _ Verifier = (*BaseVerifier)(nil) | |||
// BaseVerifier lets us check the validity of SignedHeaders at height or | |||
// later, requiring sufficient votes (> 2/3) from the given valset. | |||
// To verify blocks produced by a blockchain with mutable validator sets, | |||
// use the DynamicVerifier. | |||
// TODO: Handle unbonding time. | |||
type BaseVerifier struct { | |||
chainID string | |||
height int64 | |||
valset *types.ValidatorSet | |||
} | |||
// NewBaseVerifier returns a new Verifier initialized with a validator set at | |||
// some height. | |||
func NewBaseVerifier(chainID string, height int64, valset *types.ValidatorSet) *BaseVerifier { | |||
if valset.IsNilOrEmpty() { | |||
panic("NewBaseVerifier requires a valid valset") | |||
} | |||
return &BaseVerifier{ | |||
chainID: chainID, | |||
height: height, | |||
valset: valset, | |||
} | |||
} | |||
// Implements Verifier. | |||
func (bv *BaseVerifier) ChainID() string { | |||
return bv.chainID | |||
} | |||
// Implements Verifier. | |||
func (bv *BaseVerifier) Verify(signedHeader types.SignedHeader) error { | |||
// We can't verify commits for a different chain. | |||
if signedHeader.ChainID != bv.chainID { | |||
return errors.Errorf("BaseVerifier chainID is %v, cannot verify chainID %v", | |||
bv.chainID, signedHeader.ChainID) | |||
} | |||
// We can't verify commits older than bv.height. | |||
if signedHeader.Height < bv.height { | |||
return errors.Errorf("BaseVerifier height is %v, cannot verify height %v", | |||
bv.height, signedHeader.Height) | |||
} | |||
// We can't verify with the wrong validator set. | |||
if !bytes.Equal(signedHeader.ValidatorsHash, | |||
bv.valset.Hash()) { | |||
return lerr.ErrUnexpectedValidators(signedHeader.ValidatorsHash, bv.valset.Hash()) | |||
} | |||
// Do basic sanity checks. | |||
err := signedHeader.ValidateBasic(bv.chainID) | |||
if err != nil { | |||
return errors.Wrap(err, "in verify") | |||
} | |||
// Check commit signatures. | |||
err = bv.valset.VerifyCommit( | |||
bv.chainID, signedHeader.Commit.BlockID, | |||
signedHeader.Height, signedHeader.Commit) | |||
if err != nil { | |||
return errors.Wrap(err, "in verify") | |||
} | |||
return nil | |||
} |
@ -1,66 +0,0 @@ | |||
package lite | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/tendermint/tendermint/crypto/tmhash" | |||
lerr "github.com/tendermint/tendermint/lite/errors" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func TestBaseCert(t *testing.T) { | |||
// TODO: Requires proposer address to be set in header. | |||
t.SkipNow() | |||
assert := assert.New(t) | |||
keys := genPrivKeys(4) | |||
// 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do! | |||
vals := keys.ToValidators(20, 10) | |||
// and a Verifier based on our known set | |||
chainID := "test-static" | |||
cert := NewBaseVerifier(chainID, 2, vals) | |||
cases := []struct { | |||
keys privKeys | |||
vals *types.ValidatorSet | |||
height int64 | |||
first, last int // who actually signs | |||
proper bool // true -> expect no error | |||
changed bool // true -> expect validator change error | |||
}{ | |||
// height regression | |||
{keys, vals, 1, 0, len(keys), false, false}, | |||
// perfect, signed by everyone | |||
{keys, vals, 2, 0, len(keys), true, false}, | |||
// skip little guy is okay | |||
{keys, vals, 3, 1, len(keys), true, false}, | |||
// but not the big guy | |||
{keys, vals, 4, 0, len(keys) - 1, false, false}, | |||
// 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), 5, 0, len(keys), false, true}, | |||
} | |||
for _, tc := range cases { | |||
sh := tc.keys.GenSignedHeader( | |||
chainID, tc.height, nil, tc.vals, tc.vals, | |||
tmhash.Sum([]byte("foo")), | |||
tmhash.Sum([]byte("params")), | |||
tmhash.Sum([]byte("results")), | |||
tc.first, tc.last, | |||
) | |||
err := cert.Verify(sh) | |||
if tc.proper { | |||
assert.Nil(err, "%+v", err) | |||
} else { | |||
assert.NotNil(err) | |||
if tc.changed { | |||
assert.True(lerr.IsErrUnexpectedValidators(err), "%+v", err) | |||
} | |||
} | |||
} | |||
} |
@ -1,139 +0,0 @@ | |||
/* | |||
Package client defines a provider that uses a rpchttp | |||
to get information, which is used to get new headers | |||
and validators directly from a Tendermint client. | |||
*/ | |||
package client | |||
import ( | |||
"fmt" | |||
log "github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/lite" | |||
lerr "github.com/tendermint/tendermint/lite/errors" | |||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||
rpchttp "github.com/tendermint/tendermint/rpc/client/http" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// SignStatusClient combines a SignClient and StatusClient. | |||
type SignStatusClient interface { | |||
rpcclient.SignClient | |||
rpcclient.StatusClient | |||
} | |||
type provider struct { | |||
logger log.Logger | |||
chainID string | |||
client SignStatusClient | |||
} | |||
// NewProvider implements Provider (but not PersistentProvider). | |||
func NewProvider(chainID string, client SignStatusClient) lite.Provider { | |||
return &provider{ | |||
logger: log.NewNopLogger(), | |||
chainID: chainID, | |||
client: client, | |||
} | |||
} | |||
// NewHTTPProvider can connect to a tendermint json-rpc endpoint | |||
// at the given url, and uses that as a read-only provider. | |||
func NewHTTPProvider(chainID, remote string) (lite.Provider, error) { | |||
httpClient, err := rpchttp.New(remote, "/websocket") | |||
if err != nil { | |||
return nil, err | |||
} | |||
return NewProvider(chainID, httpClient), nil | |||
} | |||
// Implements Provider. | |||
func (p *provider) SetLogger(logger log.Logger) { | |||
logger = logger.With("module", "lite/client") | |||
p.logger = logger | |||
} | |||
// StatusClient returns the internal client as a StatusClient | |||
func (p *provider) StatusClient() rpcclient.StatusClient { | |||
return p.client | |||
} | |||
// LatestFullCommit implements Provider. | |||
func (p *provider) LatestFullCommit(chainID string, minHeight, maxHeight int64) (fc lite.FullCommit, err error) { | |||
if chainID != p.chainID { | |||
err = fmt.Errorf("expected chainID %s, got %s", p.chainID, chainID) | |||
return | |||
} | |||
if maxHeight != 0 && maxHeight < minHeight { | |||
err = fmt.Errorf("need maxHeight == 0 or minHeight <= maxHeight, got min %v and max %v", | |||
minHeight, maxHeight) | |||
return | |||
} | |||
commit, err := p.fetchLatestCommit(minHeight, maxHeight) | |||
if err != nil { | |||
return | |||
} | |||
fc, err = p.fillFullCommit(commit.SignedHeader) | |||
return | |||
} | |||
// fetchLatestCommit fetches the latest commit from the client. | |||
func (p *provider) fetchLatestCommit(minHeight int64, maxHeight int64) (*ctypes.ResultCommit, error) { | |||
status, err := p.client.Status() | |||
if err != nil { | |||
return nil, err | |||
} | |||
if status.SyncInfo.LatestBlockHeight < minHeight { | |||
err = fmt.Errorf("provider is at %v but require minHeight=%v", | |||
status.SyncInfo.LatestBlockHeight, minHeight) | |||
return nil, err | |||
} | |||
if maxHeight == 0 { | |||
maxHeight = status.SyncInfo.LatestBlockHeight | |||
} else if status.SyncInfo.LatestBlockHeight < maxHeight { | |||
maxHeight = status.SyncInfo.LatestBlockHeight | |||
} | |||
return p.client.Commit(&maxHeight) | |||
} | |||
// Implements Provider. | |||
func (p *provider) ValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) { | |||
return p.getValidatorSet(chainID, height) | |||
} | |||
func (p *provider) getValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) { | |||
if chainID != p.chainID { | |||
err = fmt.Errorf("expected chainID %s, got %s", p.chainID, chainID) | |||
return | |||
} | |||
if height < 1 { | |||
err = fmt.Errorf("expected height >= 1, got height %v", height) | |||
return | |||
} | |||
res, err := p.client.Validators(&height, 0, 0) | |||
if err != nil { | |||
// TODO pass through other types of errors. | |||
return nil, lerr.ErrUnknownValidators(chainID, height) | |||
} | |||
valset = types.NewValidatorSet(res.Validators) | |||
return | |||
} | |||
// This does no validation. | |||
func (p *provider) fillFullCommit(signedHeader types.SignedHeader) (fc lite.FullCommit, err error) { | |||
// Get the validators. | |||
valset, err := p.getValidatorSet(signedHeader.ChainID, signedHeader.Height) | |||
if err != nil { | |||
return lite.FullCommit{}, err | |||
} | |||
// Get the next validators. | |||
nextValset, err := p.getValidatorSet(signedHeader.ChainID, signedHeader.Height+1) | |||
if err != nil { | |||
return lite.FullCommit{}, err | |||
} | |||
return lite.NewFullCommit(signedHeader, valset, nextValset), nil | |||
} |
@ -1,62 +0,0 @@ | |||
package client | |||
import ( | |||
"os" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/abci/example/kvstore" | |||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||
rpctest "github.com/tendermint/tendermint/rpc/test" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func TestMain(m *testing.M) { | |||
app := kvstore.NewApplication() | |||
node := rpctest.StartTendermint(app) | |||
code := m.Run() | |||
rpctest.StopTendermint(node) | |||
os.Exit(code) | |||
} | |||
func TestProvider(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
cfg := rpctest.GetConfig() | |||
defer os.RemoveAll(cfg.RootDir) | |||
rpcAddr := cfg.RPC.ListenAddress | |||
genDoc, err := types.GenesisDocFromFile(cfg.GenesisFile()) | |||
if err != nil { | |||
panic(err) | |||
} | |||
chainID := genDoc.ChainID | |||
t.Log("chainID:", chainID) | |||
p, err := NewHTTPProvider(chainID, rpcAddr) | |||
require.Nil(err) | |||
require.NotNil(p) | |||
// let it produce some blocks | |||
err = rpcclient.WaitForHeight(p.(*provider).client, 6, nil) | |||
require.Nil(err) | |||
// let's get the highest block | |||
fc, err := p.LatestFullCommit(chainID, 1, 1<<63-1) | |||
require.Nil(err, "%+v", err) | |||
sh := fc.Height() | |||
assert.True(sh < 5000) | |||
// let's check this is valid somehow | |||
assert.Nil(fc.ValidateFull(chainID)) | |||
// historical queries now work :) | |||
lower := sh - 5 | |||
fc, err = p.LatestFullCommit(chainID, lower, lower) | |||
assert.Nil(err, "%+v", err) | |||
assert.Equal(lower, fc.Height()) | |||
} |
@ -1,87 +0,0 @@ | |||
package lite | |||
import ( | |||
"bytes" | |||
"errors" | |||
"fmt" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// FullCommit contains a SignedHeader (the block header and a commit that signs it), | |||
// the validator set which signed the commit, and the next validator set. The | |||
// next validator set (which is proven from the block header) allows us to | |||
// revert to block-by-block updating of lite Verifier's latest validator set, | |||
// even in the face of arbitrarily large power changes. | |||
type FullCommit struct { | |||
SignedHeader types.SignedHeader `json:"signed_header"` | |||
Validators *types.ValidatorSet `json:"validator_set"` | |||
NextValidators *types.ValidatorSet `json:"next_validator_set"` | |||
} | |||
// NewFullCommit returns a new FullCommit. | |||
func NewFullCommit(signedHeader types.SignedHeader, valset, nextValset *types.ValidatorSet) FullCommit { | |||
return FullCommit{ | |||
SignedHeader: signedHeader, | |||
Validators: valset, | |||
NextValidators: nextValset, | |||
} | |||
} | |||
// Validate the components and check for consistency. | |||
// This also checks to make sure that Validators actually | |||
// signed the SignedHeader.Commit. | |||
// If > 2/3 did not sign the Commit from fc.Validators, it | |||
// is not a valid commit! | |||
func (fc FullCommit) ValidateFull(chainID string) error { | |||
// Ensure that Validators exists and matches the header. | |||
if fc.Validators.Size() == 0 { | |||
return errors.New("need FullCommit.Validators") | |||
} | |||
if !bytes.Equal( | |||
fc.SignedHeader.ValidatorsHash, | |||
fc.Validators.Hash()) { | |||
return fmt.Errorf("header has vhash %X but valset hash is %X", | |||
fc.SignedHeader.ValidatorsHash, | |||
fc.Validators.Hash(), | |||
) | |||
} | |||
// Ensure that NextValidators exists and matches the header. | |||
if fc.NextValidators.Size() == 0 { | |||
return errors.New("need FullCommit.NextValidators") | |||
} | |||
if !bytes.Equal( | |||
fc.SignedHeader.NextValidatorsHash, | |||
fc.NextValidators.Hash()) { | |||
return fmt.Errorf("header has next vhash %X but next valset hash is %X", | |||
fc.SignedHeader.NextValidatorsHash, | |||
fc.NextValidators.Hash(), | |||
) | |||
} | |||
// Validate the header. | |||
err := fc.SignedHeader.ValidateBasic(chainID) | |||
if err != nil { | |||
return err | |||
} | |||
// Validate the signatures on the commit. | |||
hdr, cmt := fc.SignedHeader.Header, fc.SignedHeader.Commit | |||
return fc.Validators.VerifyCommit( | |||
hdr.ChainID, cmt.BlockID, | |||
hdr.Height, cmt) | |||
} | |||
// Height returns the height of the header. | |||
func (fc FullCommit) Height() int64 { | |||
if fc.SignedHeader.Header == nil { | |||
panic("should not happen") | |||
} | |||
return fc.SignedHeader.Height | |||
} | |||
// ChainID returns the chainID of the header. | |||
func (fc FullCommit) ChainID() string { | |||
if fc.SignedHeader.Header == nil { | |||
panic("should not happen") | |||
} | |||
return fc.SignedHeader.ChainID | |||
} |
@ -1,285 +0,0 @@ | |||
package lite | |||
import ( | |||
"fmt" | |||
"regexp" | |||
"strconv" | |||
amino "github.com/tendermint/go-amino" | |||
dbm "github.com/tendermint/tm-db" | |||
cryptoamino "github.com/tendermint/tendermint/crypto/encoding/amino" | |||
log "github.com/tendermint/tendermint/libs/log" | |||
lerr "github.com/tendermint/tendermint/lite/errors" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var _ PersistentProvider = (*DBProvider)(nil) | |||
// DBProvider stores commits and validator sets in a DB. | |||
type DBProvider struct { | |||
logger log.Logger | |||
label string | |||
db dbm.DB | |||
cdc *amino.Codec | |||
limit int | |||
} | |||
func NewDBProvider(label string, db dbm.DB) *DBProvider { | |||
// NOTE: when debugging, this type of construction might be useful. | |||
//db = dbm.NewDebugDB("db provider "+tmrand.Str(4), db) | |||
cdc := amino.NewCodec() | |||
cryptoamino.RegisterAmino(cdc) | |||
dbp := &DBProvider{ | |||
logger: log.NewNopLogger(), | |||
label: label, | |||
db: db, | |||
cdc: cdc, | |||
} | |||
return dbp | |||
} | |||
func (dbp *DBProvider) SetLogger(logger log.Logger) { | |||
dbp.logger = logger.With("label", dbp.label) | |||
} | |||
func (dbp *DBProvider) SetLimit(limit int) *DBProvider { | |||
dbp.limit = limit | |||
return dbp | |||
} | |||
// Implements PersistentProvider. | |||
func (dbp *DBProvider) SaveFullCommit(fc FullCommit) error { | |||
dbp.logger.Info("DBProvider.SaveFullCommit()...", "fc", fc) | |||
batch := dbp.db.NewBatch() | |||
defer batch.Close() | |||
// Save the fc.validators. | |||
// We might be overwriting what we already have, but | |||
// it makes the logic easier for now. | |||
vsKey := validatorSetKey(fc.ChainID(), fc.Height()) | |||
vsBz, err := dbp.cdc.MarshalBinaryLengthPrefixed(fc.Validators) | |||
if err != nil { | |||
return err | |||
} | |||
batch.Set(vsKey, vsBz) | |||
// Save the fc.NextValidators. | |||
nvsKey := validatorSetKey(fc.ChainID(), fc.Height()+1) | |||
nvsBz, err := dbp.cdc.MarshalBinaryLengthPrefixed(fc.NextValidators) | |||
if err != nil { | |||
return err | |||
} | |||
batch.Set(nvsKey, nvsBz) | |||
// Save the fc.SignedHeader | |||
shKey := signedHeaderKey(fc.ChainID(), fc.Height()) | |||
shBz, err := dbp.cdc.MarshalBinaryLengthPrefixed(fc.SignedHeader) | |||
if err != nil { | |||
return err | |||
} | |||
batch.Set(shKey, shBz) | |||
// And write sync. | |||
batch.WriteSync() | |||
// Garbage collect. | |||
// TODO: optimize later. | |||
if dbp.limit > 0 { | |||
dbp.deleteAfterN(fc.ChainID(), dbp.limit) | |||
} | |||
return nil | |||
} | |||
// Implements Provider. | |||
func (dbp *DBProvider) LatestFullCommit(chainID string, minHeight, maxHeight int64) ( | |||
FullCommit, error) { | |||
dbp.logger.Info("DBProvider.LatestFullCommit()...", | |||
"chainID", chainID, "minHeight", minHeight, "maxHeight", maxHeight) | |||
if minHeight <= 0 { | |||
minHeight = 1 | |||
} | |||
if maxHeight == 0 { | |||
maxHeight = 1<<63 - 1 | |||
} | |||
itr, err := dbp.db.ReverseIterator( | |||
signedHeaderKey(chainID, minHeight), | |||
append(signedHeaderKey(chainID, maxHeight), byte(0x00)), | |||
) | |||
if err != nil { | |||
panic(err) | |||
} | |||
defer itr.Close() | |||
for itr.Valid() { | |||
key := itr.Key() | |||
_, _, ok := parseSignedHeaderKey(key) | |||
if !ok { | |||
// Skip over other keys. | |||
itr.Next() | |||
continue | |||
} else { | |||
// Found the latest full commit signed header. | |||
shBz := itr.Value() | |||
sh := types.SignedHeader{} | |||
err := dbp.cdc.UnmarshalBinaryLengthPrefixed(shBz, &sh) | |||
if err != nil { | |||
return FullCommit{}, err | |||
} | |||
lfc, err := dbp.fillFullCommit(sh) | |||
if err == nil { | |||
dbp.logger.Info("DBProvider.LatestFullCommit() found latest.", "height", lfc.Height()) | |||
return lfc, nil | |||
} | |||
dbp.logger.Error("DBProvider.LatestFullCommit() got error", "lfc", lfc) | |||
dbp.logger.Error(fmt.Sprintf("%+v", err)) | |||
return lfc, err | |||
} | |||
} | |||
return FullCommit{}, lerr.ErrCommitNotFound() | |||
} | |||
func (dbp *DBProvider) ValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) { | |||
return dbp.getValidatorSet(chainID, height) | |||
} | |||
func (dbp *DBProvider) getValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) { | |||
vsBz, err := dbp.db.Get(validatorSetKey(chainID, height)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if len(vsBz) == 0 { | |||
err = lerr.ErrUnknownValidators(chainID, height) | |||
return | |||
} | |||
err = dbp.cdc.UnmarshalBinaryLengthPrefixed(vsBz, &valset) | |||
if err != nil { | |||
return | |||
} | |||
// To test deep equality. This makes it easier to test for e.g. valset | |||
// equivalence using assert.Equal (tests for deep equality) in our tests, | |||
// which also tests for unexported/private field equivalence. | |||
valset.TotalVotingPower() | |||
return | |||
} | |||
func (dbp *DBProvider) fillFullCommit(sh types.SignedHeader) (FullCommit, error) { | |||
var chainID = sh.ChainID | |||
var height = sh.Height | |||
var valset, nextValset *types.ValidatorSet | |||
// Load the validator set. | |||
valset, err := dbp.getValidatorSet(chainID, height) | |||
if err != nil { | |||
return FullCommit{}, err | |||
} | |||
// Load the next validator set. | |||
nextValset, err = dbp.getValidatorSet(chainID, height+1) | |||
if err != nil { | |||
return FullCommit{}, err | |||
} | |||
// Return filled FullCommit. | |||
return FullCommit{ | |||
SignedHeader: sh, | |||
Validators: valset, | |||
NextValidators: nextValset, | |||
}, nil | |||
} | |||
func (dbp *DBProvider) deleteAfterN(chainID string, after int) error { | |||
dbp.logger.Info("DBProvider.deleteAfterN()...", "chainID", chainID, "after", after) | |||
itr, err := dbp.db.ReverseIterator( | |||
signedHeaderKey(chainID, 1), | |||
append(signedHeaderKey(chainID, 1<<63-1), byte(0x00)), | |||
) | |||
if err != nil { | |||
panic(err) | |||
} | |||
defer itr.Close() | |||
var lastHeight int64 = 1<<63 - 1 | |||
var numSeen = 0 | |||
var numDeleted = 0 | |||
for itr.Valid() { | |||
key := itr.Key() | |||
_, height, ok := parseChainKeyPrefix(key) | |||
if !ok { | |||
return fmt.Errorf("unexpected key %v", key) | |||
} | |||
if height < lastHeight { | |||
lastHeight = height | |||
numSeen++ | |||
} | |||
if numSeen > after { | |||
dbp.db.Delete(key) | |||
numDeleted++ | |||
} | |||
itr.Next() | |||
} | |||
dbp.logger.Info(fmt.Sprintf("DBProvider.deleteAfterN() deleted %v items", numDeleted)) | |||
return nil | |||
} | |||
//---------------------------------------- | |||
// key encoding | |||
func signedHeaderKey(chainID string, height int64) []byte { | |||
return []byte(fmt.Sprintf("%s/%010d/sh", chainID, height)) | |||
} | |||
func validatorSetKey(chainID string, height int64) []byte { | |||
return []byte(fmt.Sprintf("%s/%010d/vs", chainID, height)) | |||
} | |||
//---------------------------------------- | |||
// key parsing | |||
var keyPattern = regexp.MustCompile(`^([^/]+)/([0-9]*)/(.*)$`) | |||
func parseKey(key []byte) (chainID string, height int64, part string, ok bool) { | |||
submatch := keyPattern.FindSubmatch(key) | |||
if submatch == nil { | |||
return "", 0, "", false | |||
} | |||
chainID = string(submatch[1]) | |||
heightStr := string(submatch[2]) | |||
heightInt, err := strconv.Atoi(heightStr) | |||
if err != nil { | |||
return "", 0, "", false | |||
} | |||
height = int64(heightInt) | |||
part = string(submatch[3]) | |||
ok = true // good! | |||
return | |||
} | |||
func parseSignedHeaderKey(key []byte) (chainID string, height int64, ok bool) { | |||
var part string | |||
chainID, height, part, ok = parseKey(key) | |||
if part != "sh" { | |||
return "", 0, false | |||
} | |||
return | |||
} | |||
func parseChainKeyPrefix(key []byte) (chainID string, height int64, ok bool) { | |||
chainID, height, _, ok = parseKey(key) | |||
return | |||
} |
@ -1,133 +0,0 @@ | |||
/* | |||
Package lite is deprecated and will be removed in v0.34! | |||
Package lite 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 applications. | |||
We design for clients who have no strong trust relationship with any Tendermint | |||
node, just the blockchain and validator set as a whole. | |||
SignedHeader | |||
SignedHeader is a block header along with a commit -- enough validator | |||
precommit-vote signatures to prove its validity (> 2/3 of the voting power) | |||
given the validator set responsible for signing that header. A FullCommit is a | |||
SignedHeader along with the current and next validator sets. | |||
The hash of the next validator set is included and signed in the SignedHeader. | |||
This lets the lite client keep track of arbitrary changes to the validator set, | |||
as every change to the validator set must be approved by inclusion in the | |||
header and signed in the commit. | |||
In the worst case, with every block changing the validators around completely, | |||
a lite client can sync up with every block header to verify each validator set | |||
change on the chain. In practice, most applications will not have frequent | |||
drastic updates to the validator set, so the logic defined in this package for | |||
lite client syncing is optimized to use intelligent bisection and | |||
block-skipping for efficient sourcing and verification of these data structures | |||
and updates to the validator set (see the DynamicVerifier for more | |||
information). | |||
The FullCommit is also declared in this package as a convenience structure, | |||
which includes the SignedHeader along with the full current and next | |||
ValidatorSets. | |||
Verifier | |||
A Verifier validates a new SignedHeader given the currently known state. There | |||
are two different types of Verifiers provided. | |||
BaseVerifier - given a validator set and a height, this Verifier verifies | |||
that > 2/3 of the voting power of the given validator set had signed the | |||
SignedHeader, and that the SignedHeader was to be signed by the exact given | |||
validator set, and that the height of the commit is at least height (or | |||
greater). | |||
DynamicVerifier - this Verifier implements an auto-update and persistence | |||
strategy to verify any SignedHeader of the blockchain. | |||
Provider and PersistentProvider | |||
A Provider allows us to store and retrieve the FullCommits. | |||
type Provider interface { | |||
// LatestFullCommit returns the latest commit with | |||
// minHeight <= height <= maxHeight. | |||
// If maxHeight is zero, returns the latest where | |||
// minHeight <= height. | |||
LatestFullCommit(chainID string, minHeight, maxHeight int64) (FullCommit, error) | |||
} | |||
* client.NewHTTPProvider - query Tendermint rpc. | |||
A PersistentProvider is a Provider that also allows for saving state. This is | |||
used by the DynamicVerifier for persistence. | |||
type PersistentProvider interface { | |||
Provider | |||
// SaveFullCommit saves a FullCommit (without verification). | |||
SaveFullCommit(fc FullCommit) error | |||
} | |||
* DBProvider - persistence provider for use with any libs/DB. | |||
* MultiProvider - combine multiple providers. | |||
The suggested use for local light clients is client.NewHTTPProvider(...) for | |||
getting new data (Source), and NewMultiProvider(NewDBProvider("label", | |||
dbm.NewMemDB()), NewDBProvider("label", db.NewFileDB(...))) to store confirmed | |||
full commits (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 | |||
static validator set, you store it locally upon initialization of the client, | |||
and check against that every time. | |||
If the validator set for the blockchain is dynamic, verifying block commits is | |||
a bit more involved -- 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, we get the new (unconfirmed) validator set V' and verify that 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. Then, we accept H' | |||
and V' as valid and trusted and use that to validate for heights X > H' until a | |||
more recent and updated validator set is found. | |||
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 then Hm -> H' in two steps. If one | |||
of these steps doesn't work, then we continue bisecting, until we eventually | |||
have to externally validate the validator 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. | |||
The software currently assumes that the unbonding period is infinite in | |||
duration. If the DynamicVerifier hasn't been updated in a while, you should | |||
manually verify the block headers using other sources. | |||
TODO: Update the software to handle cases around the unbonding period. | |||
*/ | |||
package lite |
@ -1,275 +0,0 @@ | |||
package lite | |||
import ( | |||
"bytes" | |||
"fmt" | |||
"sync" | |||
log "github.com/tendermint/tendermint/libs/log" | |||
lerr "github.com/tendermint/tendermint/lite/errors" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
const sizeOfPendingMap = 1024 | |||
var _ Verifier = (*DynamicVerifier)(nil) | |||
// DynamicVerifier implements an auto-updating Verifier. It uses a | |||
// "source" provider to obtain the needed FullCommits to securely sync with | |||
// validator set changes. It stores properly validated data on the | |||
// "trusted" local system. | |||
// TODO: make this single threaded and create a new | |||
// ConcurrentDynamicVerifier that wraps it with concurrency. | |||
// see https://github.com/tendermint/tendermint/issues/3170 | |||
type DynamicVerifier struct { | |||
chainID string | |||
logger log.Logger | |||
// Already validated, stored locally | |||
trusted PersistentProvider | |||
// New info, like a node rpc, or other import method. | |||
source Provider | |||
// pending map to synchronize concurrent verification requests | |||
mtx sync.Mutex | |||
pendingVerifications map[int64]chan struct{} | |||
} | |||
// NewDynamicVerifier returns a new DynamicVerifier. It uses the | |||
// trusted provider to store validated data and the source provider to | |||
// obtain missing data (e.g. FullCommits). | |||
// | |||
// The trusted provider should be a DBProvider. | |||
// The source provider should be a client.HTTPProvider. | |||
func NewDynamicVerifier(chainID string, trusted PersistentProvider, source Provider) *DynamicVerifier { | |||
return &DynamicVerifier{ | |||
logger: log.NewNopLogger(), | |||
chainID: chainID, | |||
trusted: trusted, | |||
source: source, | |||
pendingVerifications: make(map[int64]chan struct{}, sizeOfPendingMap), | |||
} | |||
} | |||
func (dv *DynamicVerifier) SetLogger(logger log.Logger) { | |||
logger = logger.With("module", "lite") | |||
dv.logger = logger | |||
dv.trusted.SetLogger(logger) | |||
dv.source.SetLogger(logger) | |||
} | |||
// Implements Verifier. | |||
func (dv *DynamicVerifier) ChainID() string { | |||
return dv.chainID | |||
} | |||
// Implements Verifier. | |||
// | |||
// If the validators have changed since the last known time, it looks to | |||
// dv.trusted and dv.source to prove the new validators. On success, it will | |||
// try to store the SignedHeader in dv.trusted if the next | |||
// validator can be sourced. | |||
func (dv *DynamicVerifier) Verify(shdr types.SignedHeader) error { | |||
// Performs synchronization for multi-threads verification at the same height. | |||
dv.mtx.Lock() | |||
if pending := dv.pendingVerifications[shdr.Height]; pending != nil { | |||
dv.mtx.Unlock() | |||
<-pending // pending is chan struct{} | |||
} else { | |||
pending := make(chan struct{}) | |||
dv.pendingVerifications[shdr.Height] = pending | |||
defer func() { | |||
close(pending) | |||
dv.mtx.Lock() | |||
delete(dv.pendingVerifications, shdr.Height) | |||
dv.mtx.Unlock() | |||
}() | |||
dv.mtx.Unlock() | |||
} | |||
//Get the exact trusted commit for h, and if it is | |||
// equal to shdr, then it's already trusted, so | |||
// just return nil. | |||
trustedFCSameHeight, err := dv.trusted.LatestFullCommit(dv.chainID, shdr.Height, shdr.Height) | |||
if err == nil { | |||
// If loading trust commit successfully, and trust commit equal to shdr, then don't verify it, | |||
// just return nil. | |||
if bytes.Equal(trustedFCSameHeight.SignedHeader.Hash(), shdr.Hash()) { | |||
dv.logger.Info(fmt.Sprintf("Load full commit at height %d from cache, there is not need to verify.", shdr.Height)) | |||
return nil | |||
} | |||
} else if !lerr.IsErrCommitNotFound(err) { | |||
// Return error if it is not CommitNotFound error | |||
dv.logger.Info(fmt.Sprintf("Encountered unknown error in loading full commit at height %d.", shdr.Height)) | |||
return err | |||
} | |||
// Get the latest known full commit <= h-1 from our trusted providers. | |||
// The full commit at h-1 contains the valset to sign for h. | |||
prevHeight := shdr.Height - 1 | |||
trustedFC, err := dv.trusted.LatestFullCommit(dv.chainID, 1, prevHeight) | |||
if err != nil { | |||
return err | |||
} | |||
// sync up to the prevHeight and assert our latest NextValidatorSet | |||
// is the ValidatorSet for the SignedHeader | |||
if trustedFC.Height() == prevHeight { | |||
// Return error if valset doesn't match. | |||
if !bytes.Equal( | |||
trustedFC.NextValidators.Hash(), | |||
shdr.Header.ValidatorsHash) { | |||
return lerr.ErrUnexpectedValidators( | |||
trustedFC.NextValidators.Hash(), | |||
shdr.Header.ValidatorsHash) | |||
} | |||
} else { | |||
// If valset doesn't match, try to update | |||
if !bytes.Equal( | |||
trustedFC.NextValidators.Hash(), | |||
shdr.Header.ValidatorsHash) { | |||
// ... update. | |||
trustedFC, err = dv.updateToHeight(prevHeight) | |||
if err != nil { | |||
return err | |||
} | |||
// Return error if valset _still_ doesn't match. | |||
if !bytes.Equal(trustedFC.NextValidators.Hash(), | |||
shdr.Header.ValidatorsHash) { | |||
return lerr.ErrUnexpectedValidators( | |||
trustedFC.NextValidators.Hash(), | |||
shdr.Header.ValidatorsHash) | |||
} | |||
} | |||
} | |||
// Verify the signed header using the matching valset. | |||
cert := NewBaseVerifier(dv.chainID, trustedFC.Height()+1, trustedFC.NextValidators) | |||
err = cert.Verify(shdr) | |||
if err != nil { | |||
return err | |||
} | |||
// By now, the SignedHeader is fully validated and we're synced up to | |||
// SignedHeader.Height - 1. To sync to SignedHeader.Height, we need | |||
// the validator set at SignedHeader.Height + 1 so we can verify the | |||
// SignedHeader.NextValidatorSet. | |||
// TODO: is the ValidateFull below mostly redundant with the BaseVerifier.Verify above? | |||
// See https://github.com/tendermint/tendermint/issues/3174. | |||
// Get the next validator set. | |||
nextValset, err := dv.source.ValidatorSet(dv.chainID, shdr.Height+1) | |||
if lerr.IsErrUnknownValidators(err) { | |||
// Ignore this error. | |||
return nil | |||
} else if err != nil { | |||
return err | |||
} | |||
// Create filled FullCommit. | |||
nfc := FullCommit{ | |||
SignedHeader: shdr, | |||
Validators: trustedFC.NextValidators, | |||
NextValidators: nextValset, | |||
} | |||
// Validate the full commit. This checks the cryptographic | |||
// signatures of Commit against Validators. | |||
if err := nfc.ValidateFull(dv.chainID); err != nil { | |||
return err | |||
} | |||
// Trust it. | |||
return dv.trusted.SaveFullCommit(nfc) | |||
} | |||
// verifyAndSave will verify if this is a valid source full commit given the | |||
// best match trusted full commit, and if good, persist to dv.trusted. | |||
// Returns ErrNotEnoughVotingPowerSigned when >2/3 of trustedFC did not sign sourceFC. | |||
// Panics if trustedFC.Height() >= sourceFC.Height(). | |||
func (dv *DynamicVerifier) verifyAndSave(trustedFC, sourceFC FullCommit) error { | |||
if trustedFC.Height() >= sourceFC.Height() { | |||
panic("should not happen") | |||
} | |||
err := trustedFC.NextValidators.VerifyFutureCommit( | |||
sourceFC.Validators, | |||
dv.chainID, sourceFC.SignedHeader.Commit.BlockID, | |||
sourceFC.SignedHeader.Height, sourceFC.SignedHeader.Commit, | |||
) | |||
if err != nil { | |||
return err | |||
} | |||
return dv.trusted.SaveFullCommit(sourceFC) | |||
} | |||
// updateToHeight will use divide-and-conquer to find a path to h. | |||
// Returns nil error iff we successfully verify and persist a full commit | |||
// for height h, using repeated applications of bisection if necessary. | |||
// | |||
// Returns ErrCommitNotFound if source provider doesn't have the commit for h. | |||
func (dv *DynamicVerifier) updateToHeight(h int64) (FullCommit, error) { | |||
// Fetch latest full commit from source. | |||
sourceFC, err := dv.source.LatestFullCommit(dv.chainID, h, h) | |||
if err != nil { | |||
return FullCommit{}, err | |||
} | |||
// If sourceFC.Height() != h, we can't do it. | |||
if sourceFC.Height() != h { | |||
return FullCommit{}, lerr.ErrCommitNotFound() | |||
} | |||
// Validate the full commit. This checks the cryptographic | |||
// signatures of Commit against Validators. | |||
if err := sourceFC.ValidateFull(dv.chainID); err != nil { | |||
return FullCommit{}, err | |||
} | |||
// Verify latest FullCommit against trusted FullCommits | |||
FOR_LOOP: | |||
for { | |||
// Fetch latest full commit from trusted. | |||
trustedFC, err := dv.trusted.LatestFullCommit(dv.chainID, 1, h) | |||
if err != nil { | |||
return FullCommit{}, err | |||
} | |||
// We have nothing to do. | |||
if trustedFC.Height() == h { | |||
return trustedFC, nil | |||
} | |||
// Try to update to full commit with checks. | |||
err = dv.verifyAndSave(trustedFC, sourceFC) | |||
if err == nil { | |||
// All good! | |||
return sourceFC, nil | |||
} | |||
// Handle special case when err is ErrNotEnoughVotingPowerSigned. | |||
if types.IsErrNotEnoughVotingPowerSigned(err) { | |||
// Divide and conquer. | |||
start, end := trustedFC.Height(), sourceFC.Height() | |||
if !(start < end) { | |||
panic("should not happen") | |||
} | |||
mid := (start + end) / 2 | |||
_, err = dv.updateToHeight(mid) | |||
if err != nil { | |||
return FullCommit{}, err | |||
} | |||
// If we made it to mid, we retry. | |||
continue FOR_LOOP | |||
} | |||
return FullCommit{}, err | |||
} | |||
} | |||
func (dv *DynamicVerifier) LastTrustedHeight() int64 { | |||
fc, err := dv.trusted.LatestFullCommit(dv.chainID, 1, 1<<63-1) | |||
if err != nil { | |||
panic("should not happen") | |||
} | |||
return fc.Height() | |||
} |
@ -1,299 +0,0 @@ | |||
package lite | |||
import ( | |||
"fmt" | |||
"sync" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
dbm "github.com/tendermint/tm-db" | |||
"github.com/tendermint/tendermint/crypto/tmhash" | |||
log "github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
const testChainID = "inquiry-test" | |||
func TestInquirerValidPath(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
trust := NewDBProvider("trust", dbm.NewMemDB()) | |||
source := NewDBProvider("source", dbm.NewMemDB()) | |||
// Set up the validators to generate test blocks. | |||
var vote int64 = 10 | |||
keys := genPrivKeys(5) | |||
nkeys := keys.Extend(1) | |||
// Construct a bunch of commits, each with one more height than the last. | |||
chainID := testChainID | |||
consHash := []byte("params") | |||
resHash := []byte("results") | |||
count := 50 | |||
fcz := make([]FullCommit, count) | |||
for i := 0; i < count; i++ { | |||
vals := keys.ToValidators(vote, 0) | |||
nextVals := nkeys.ToValidators(vote, 0) | |||
h := int64(1 + i) | |||
appHash := []byte(fmt.Sprintf("h=%d", h)) | |||
fcz[i] = keys.GenFullCommit( | |||
chainID, h, nil, | |||
vals, nextVals, | |||
appHash, consHash, resHash, 0, len(keys)) | |||
// Extend the keys by 1 each time. | |||
keys = nkeys | |||
nkeys = nkeys.Extend(1) | |||
} | |||
// Initialize a Verifier with the initial state. | |||
err := trust.SaveFullCommit(fcz[0]) | |||
require.Nil(err) | |||
cert := NewDynamicVerifier(chainID, trust, source) | |||
cert.SetLogger(log.TestingLogger()) | |||
// This should fail validation: | |||
sh := fcz[count-1].SignedHeader | |||
err = cert.Verify(sh) | |||
require.NotNil(err) | |||
// Adding a few commits in the middle should be insufficient. | |||
for i := 10; i < 13; i++ { | |||
err := source.SaveFullCommit(fcz[i]) | |||
require.Nil(err) | |||
} | |||
err = cert.Verify(sh) | |||
assert.NotNil(err) | |||
// With more info, we succeed. | |||
for i := 0; i < count; i++ { | |||
err := source.SaveFullCommit(fcz[i]) | |||
require.Nil(err) | |||
} | |||
// TODO: Requires proposer address to be set in header. | |||
// err = cert.Verify(sh) | |||
// assert.Nil(err, "%+v", err) | |||
} | |||
func TestDynamicVerify(t *testing.T) { | |||
trust := NewDBProvider("trust", dbm.NewMemDB()) | |||
source := NewDBProvider("source", dbm.NewMemDB()) | |||
// 10 commits with one valset, 1 to change, | |||
// 10 commits with the next one | |||
n1, n2 := 10, 10 | |||
nCommits := n1 + n2 + 1 | |||
maxHeight := int64(nCommits) | |||
fcz := make([]FullCommit, nCommits) | |||
// gen the 2 val sets | |||
chainID := "dynamic-verifier" | |||
power := int64(10) | |||
keys1 := genPrivKeys(5) | |||
vals1 := keys1.ToValidators(power, 0) | |||
keys2 := genPrivKeys(5) | |||
vals2 := keys2.ToValidators(power, 0) | |||
// make some commits with the first | |||
for i := 0; i < n1; i++ { | |||
fcz[i] = makeFullCommit(int64(i), keys1, vals1, vals1, chainID) | |||
} | |||
// update the val set | |||
fcz[n1] = makeFullCommit(int64(n1), keys1, vals1, vals2, chainID) | |||
// make some commits with the new one | |||
for i := n1 + 1; i < nCommits; i++ { | |||
fcz[i] = makeFullCommit(int64(i), keys2, vals2, vals2, chainID) | |||
} | |||
// Save everything in the source | |||
for _, fc := range fcz { | |||
source.SaveFullCommit(fc) | |||
} | |||
// Initialize a Verifier with the initial state. | |||
err := trust.SaveFullCommit(fcz[0]) | |||
require.Nil(t, err) | |||
ver := NewDynamicVerifier(chainID, trust, source) | |||
ver.SetLogger(log.TestingLogger()) | |||
// fetch the latest from the source | |||
_, err = source.LatestFullCommit(chainID, 1, maxHeight) | |||
require.NoError(t, err) | |||
// TODO: Requires proposer address to be set in header. | |||
// try to update to the latest | |||
// err = ver.Verify(latestFC.SignedHeader) | |||
// require.NoError(t, err) | |||
} | |||
func makeFullCommit(height int64, keys privKeys, vals, nextVals *types.ValidatorSet, chainID string) FullCommit { | |||
height++ | |||
consHash := tmhash.Sum([]byte("special-params")) | |||
appHash := tmhash.Sum([]byte(fmt.Sprintf("h=%d", height))) | |||
resHash := tmhash.Sum([]byte(fmt.Sprintf("res=%d", height))) | |||
return keys.GenFullCommit( | |||
chainID, height, nil, | |||
vals, nextVals, | |||
appHash, consHash, resHash, 0, len(keys), | |||
) | |||
} | |||
func TestInquirerVerifyHistorical(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
trust := NewDBProvider("trust", dbm.NewMemDB()) | |||
source := NewDBProvider("source", dbm.NewMemDB()) | |||
// Set up the validators to generate test blocks. | |||
var vote int64 = 10 | |||
keys := genPrivKeys(5) | |||
nkeys := keys.Extend(1) | |||
// Construct a bunch of commits, each with one more height than the last. | |||
chainID := testChainID | |||
count := 10 | |||
consHash := []byte("special-params") | |||
fcz := make([]FullCommit, count) | |||
for i := 0; i < count; i++ { | |||
vals := keys.ToValidators(vote, 0) | |||
nextVals := nkeys.ToValidators(vote, 0) | |||
h := int64(1 + i) | |||
appHash := []byte(fmt.Sprintf("h=%d", h)) | |||
resHash := []byte(fmt.Sprintf("res=%d", h)) | |||
fcz[i] = keys.GenFullCommit( | |||
chainID, h, nil, | |||
vals, nextVals, | |||
appHash, consHash, resHash, 0, len(keys)) | |||
// Extend the keys by 1 each time. | |||
keys = nkeys | |||
nkeys = nkeys.Extend(1) | |||
} | |||
// Initialize a Verifier with the initial state. | |||
err := trust.SaveFullCommit(fcz[0]) | |||
require.Nil(err) | |||
cert := NewDynamicVerifier(chainID, trust, source) | |||
cert.SetLogger(log.TestingLogger()) | |||
// Store a few full commits as trust. | |||
for _, i := range []int{2, 5} { | |||
trust.SaveFullCommit(fcz[i]) | |||
} | |||
// See if we can jump forward using trusted full commits. | |||
// Souce doesn't have fcz[9] so cert.LastTrustedHeight wont' change. | |||
err = source.SaveFullCommit(fcz[7]) | |||
require.Nil(err, "%+v", err) | |||
// TODO: Requires proposer address to be set in header. | |||
// sh := fcz[8].SignedHeader | |||
// err = cert.Verify(sh) | |||
// require.Nil(err, "%+v", err) | |||
// assert.Equal(fcz[7].Height(), cert.LastTrustedHeight()) | |||
commit, err := trust.LatestFullCommit(chainID, fcz[8].Height(), fcz[8].Height()) | |||
require.NotNil(err, "%+v", err) | |||
assert.Equal(commit, (FullCommit{})) | |||
// With fcz[9] Verify will update last trusted height. | |||
err = source.SaveFullCommit(fcz[9]) | |||
require.Nil(err, "%+v", err) | |||
// TODO: Requires proposer address to be set in header. | |||
// sh = fcz[8].SignedHeader | |||
// err = cert.Verify(sh) | |||
// require.Nil(err, "%+v", err) | |||
// assert.Equal(fcz[8].Height(), cert.LastTrustedHeight()) | |||
// TODO: Requires proposer address to be set in header. | |||
// commit, err = trust.LatestFullCommit(chainID, fcz[8].Height(), fcz[8].Height()) | |||
// require.Nil(err, "%+v", err) | |||
// assert.Equal(commit.Height(), fcz[8].Height()) | |||
// Add access to all full commits via untrusted source. | |||
for i := 0; i < count; i++ { | |||
err := source.SaveFullCommit(fcz[i]) | |||
require.Nil(err) | |||
} | |||
// TODO: Requires proposer address to be set in header. | |||
// Try to check an unknown seed in the past. | |||
// sh = fcz[3].SignedHeader | |||
// err = cert.Verify(sh) | |||
// require.Nil(err, "%+v", err) | |||
// assert.Equal(fcz[8].Height(), cert.LastTrustedHeight()) | |||
// TODO: Requires proposer address to be set in header. | |||
// Jump all the way forward again. | |||
// sh = fcz[count-1].SignedHeader | |||
// err = cert.Verify(sh) | |||
// require.Nil(err, "%+v", err) | |||
// assert.Equal(fcz[9].Height(), cert.LastTrustedHeight()) | |||
} | |||
func TestConcurrencyInquirerVerify(t *testing.T) { | |||
_, require := assert.New(t), require.New(t) | |||
trust := NewDBProvider("trust", dbm.NewMemDB()).SetLimit(10) | |||
source := NewDBProvider("source", dbm.NewMemDB()) | |||
// Set up the validators to generate test blocks. | |||
var vote int64 = 10 | |||
keys := genPrivKeys(5) | |||
nkeys := keys.Extend(1) | |||
// Construct a bunch of commits, each with one more height than the last. | |||
chainID := testChainID | |||
count := 10 | |||
consHash := []byte("special-params") | |||
fcz := make([]FullCommit, count) | |||
for i := 0; i < count; i++ { | |||
vals := keys.ToValidators(vote, 0) | |||
nextVals := nkeys.ToValidators(vote, 0) | |||
h := int64(1 + i) | |||
appHash := []byte(fmt.Sprintf("h=%d", h)) | |||
resHash := []byte(fmt.Sprintf("res=%d", h)) | |||
fcz[i] = keys.GenFullCommit( | |||
chainID, h, nil, | |||
vals, nextVals, | |||
appHash, consHash, resHash, 0, len(keys)) | |||
// Extend the keys by 1 each time. | |||
keys = nkeys | |||
nkeys = nkeys.Extend(1) | |||
} | |||
// Initialize a Verifier with the initial state. | |||
err := trust.SaveFullCommit(fcz[0]) | |||
require.Nil(err) | |||
cert := NewDynamicVerifier(chainID, trust, source) | |||
cert.SetLogger(log.TestingLogger()) | |||
err = source.SaveFullCommit(fcz[7]) | |||
require.Nil(err, "%+v", err) | |||
err = source.SaveFullCommit(fcz[8]) | |||
require.Nil(err, "%+v", err) | |||
sh := fcz[8].SignedHeader | |||
var wg sync.WaitGroup | |||
count = 100 | |||
errList := make([]error, count) | |||
for i := 0; i < count; i++ { | |||
wg.Add(1) | |||
go func(index int) { | |||
errList[index] = cert.Verify(sh) | |||
defer wg.Done() | |||
}(i) | |||
} | |||
wg.Wait() | |||
// TODO: Requires proposer address to be set in header. | |||
// for _, err := range errList { | |||
// require.Nil(err) | |||
// } | |||
} |
@ -1,99 +0,0 @@ | |||
package errors | |||
import ( | |||
"fmt" | |||
"github.com/pkg/errors" | |||
) | |||
//---------------------------------------- | |||
// Error types | |||
type errCommitNotFound struct{} | |||
func (e errCommitNotFound) Error() string { | |||
return "Commit not found by provider" | |||
} | |||
type errUnexpectedValidators struct { | |||
got []byte | |||
want []byte | |||
} | |||
func (e errUnexpectedValidators) Error() string { | |||
return fmt.Sprintf("Validator set is different. Got %X want %X", | |||
e.got, e.want) | |||
} | |||
type errUnknownValidators struct { | |||
chainID string | |||
height int64 | |||
} | |||
func (e errUnknownValidators) Error() string { | |||
return fmt.Sprintf("Validators are unknown or missing for chain %s and height %d", | |||
e.chainID, e.height) | |||
} | |||
type errEmptyTree struct{} | |||
func (e errEmptyTree) Error() string { | |||
return "Tree is empty" | |||
} | |||
//---------------------------------------- | |||
// Methods for above error types | |||
//----------------- | |||
// ErrCommitNotFound | |||
// ErrCommitNotFound indicates that a the requested commit was not found. | |||
func ErrCommitNotFound() error { | |||
return errors.Wrap(errCommitNotFound{}, "") | |||
} | |||
func IsErrCommitNotFound(err error) bool { | |||
_, ok := errors.Cause(err).(errCommitNotFound) | |||
return ok | |||
} | |||
//----------------- | |||
// ErrUnexpectedValidators | |||
// ErrUnexpectedValidators indicates a validator set mismatch. | |||
func ErrUnexpectedValidators(got, want []byte) error { | |||
return errors.Wrap(errUnexpectedValidators{ | |||
got: got, | |||
want: want, | |||
}, "") | |||
} | |||
func IsErrUnexpectedValidators(err error) bool { | |||
_, ok := errors.Cause(err).(errUnexpectedValidators) | |||
return ok | |||
} | |||
//----------------- | |||
// ErrUnknownValidators | |||
// ErrUnknownValidators indicates that some validator set was missing or unknown. | |||
func ErrUnknownValidators(chainID string, height int64) error { | |||
return errors.Wrap(errUnknownValidators{chainID, height}, "") | |||
} | |||
func IsErrUnknownValidators(err error) bool { | |||
_, ok := errors.Cause(err).(errUnknownValidators) | |||
return ok | |||
} | |||
//----------------- | |||
// ErrEmptyTree | |||
func ErrEmptyTree() error { | |||
return errors.Wrap(errEmptyTree{}, "") | |||
} | |||
func IsErrEmptyTree(err error) bool { | |||
_, ok := errors.Cause(err).(errEmptyTree) | |||
return ok | |||
} |
@ -1,159 +0,0 @@ | |||
package lite | |||
import ( | |||
"github.com/tendermint/tendermint/crypto" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
"github.com/tendermint/tendermint/crypto/secp256k1" | |||
"github.com/tendermint/tendermint/types" | |||
tmtime "github.com/tendermint/tendermint/types/time" | |||
) | |||
// PrivKeys is a helper type for testing. | |||
// | |||
// It lets us simulate signing with many keys. The main use case is to create | |||
// a set, and call GenSignedHeader to get properly 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. | |||
type privKeys []crypto.PrivKey | |||
// genPrivKeys produces an array of private keys to generate commits. | |||
func genPrivKeys(n int) privKeys { | |||
res := make(privKeys, n) | |||
for i := range res { | |||
res[i] = ed25519.GenPrivKey() | |||
} | |||
return res | |||
} | |||
// Change replaces the key at index i. | |||
func (pkz privKeys) Change(i int) privKeys { | |||
res := make(privKeys, len(pkz)) | |||
copy(res, pkz) | |||
res[i] = ed25519.GenPrivKey() | |||
return res | |||
} | |||
// Extend adds n more keys (to remove, just take a slice). | |||
func (pkz privKeys) Extend(n int) privKeys { | |||
extra := genPrivKeys(n) | |||
return append(pkz, extra...) | |||
} | |||
// GenSecpPrivKeys produces an array of secp256k1 private keys to generate commits. | |||
func genSecpPrivKeys(n int) privKeys { | |||
res := make(privKeys, n) | |||
for i := range res { | |||
res[i] = secp256k1.GenPrivKey() | |||
} | |||
return res | |||
} | |||
// ExtendSecp adds n more secp256k1 keys (to remove, just take a slice). | |||
func (pkz privKeys) ExtendSecp(n int) privKeys { | |||
extra := genSecpPrivKeys(n) | |||
return append(pkz, extra...) | |||
} | |||
// ToValidators produces a valset 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 (pkz privKeys) ToValidators(init, inc int64) *types.ValidatorSet { | |||
res := make([]*types.Validator, len(pkz)) | |||
for i, k := range pkz { | |||
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 (pkz privKeys) signHeader(header *types.Header, first, last int) *types.Commit { | |||
commitSigs := make([]types.CommitSig, len(pkz)) | |||
for i := 0; i < len(pkz); i++ { | |||
commitSigs[i] = types.NewCommitSigAbsent() | |||
} | |||
// We need this list to keep the ordering. | |||
vset := pkz.ToValidators(1, 0) | |||
blockID := types.BlockID{ | |||
Hash: header.Hash(), | |||
PartsHeader: types.PartSetHeader{Total: 1, Hash: crypto.CRandBytes(32)}, | |||
} | |||
// Fill in the votes we want. | |||
for i := first; i < last && i < len(pkz); i++ { | |||
vote := makeVote(header, vset, pkz[i], blockID) | |||
commitSigs[vote.ValidatorIndex] = vote.CommitSig() | |||
} | |||
return types.NewCommit(header.Height, 1, blockID, commitSigs) | |||
} | |||
func makeVote(header *types.Header, valset *types.ValidatorSet, key crypto.PrivKey, blockID types.BlockID) *types.Vote { | |||
addr := key.PubKey().Address() | |||
idx, _ := valset.GetByAddress(addr) | |||
vote := &types.Vote{ | |||
ValidatorAddress: addr, | |||
ValidatorIndex: idx, | |||
Height: header.Height, | |||
Round: 1, | |||
Timestamp: tmtime.Now(), | |||
Type: types.PrecommitType, | |||
BlockID: blockID, | |||
} | |||
// Sign it | |||
signBytes := vote.SignBytes(header.ChainID) | |||
// TODO Consider reworking makeVote API to return an error | |||
sig, err := key.Sign(signBytes) | |||
if err != nil { | |||
panic(err) | |||
} | |||
vote.Signature = sig | |||
return vote | |||
} | |||
func genHeader(chainID string, height int64, txs types.Txs, | |||
valset, nextValset *types.ValidatorSet, appHash, consHash, resHash []byte) *types.Header { | |||
return &types.Header{ | |||
ChainID: chainID, | |||
Height: height, | |||
Time: tmtime.Now(), | |||
// LastBlockID | |||
// LastCommitHash | |||
ValidatorsHash: valset.Hash(), | |||
NextValidatorsHash: nextValset.Hash(), | |||
DataHash: txs.Hash(), | |||
AppHash: appHash, | |||
ConsensusHash: consHash, | |||
LastResultsHash: resHash, | |||
} | |||
} | |||
// GenSignedHeader calls genHeader and signHeader and combines them into a SignedHeader. | |||
func (pkz privKeys) GenSignedHeader(chainID string, height int64, txs types.Txs, | |||
valset, nextValset *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int) types.SignedHeader { | |||
header := genHeader(chainID, height, txs, valset, nextValset, appHash, consHash, resHash) | |||
check := types.SignedHeader{ | |||
Header: header, | |||
Commit: pkz.signHeader(header, first, last), | |||
} | |||
return check | |||
} | |||
// GenFullCommit calls genHeader and signHeader and combines them into a FullCommit. | |||
func (pkz privKeys) GenFullCommit(chainID string, height int64, txs types.Txs, | |||
valset, nextValset *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int) FullCommit { | |||
header := genHeader(chainID, height, txs, valset, nextValset, appHash, consHash, resHash) | |||
commit := types.SignedHeader{ | |||
Header: header, | |||
Commit: pkz.signHeader(header, first, last), | |||
} | |||
return NewFullCommit(commit, valset, nextValset) | |||
} |
@ -1,85 +0,0 @@ | |||
package lite | |||
import ( | |||
log "github.com/tendermint/tendermint/libs/log" | |||
lerr "github.com/tendermint/tendermint/lite/errors" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var _ PersistentProvider = (*multiProvider)(nil) | |||
// multiProvider 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. | |||
type multiProvider struct { | |||
logger log.Logger | |||
providers []PersistentProvider | |||
} | |||
// NewMultiProvider returns a new provider which wraps multiple other providers. | |||
func NewMultiProvider(providers ...PersistentProvider) PersistentProvider { | |||
return &multiProvider{ | |||
logger: log.NewNopLogger(), | |||
providers: providers, | |||
} | |||
} | |||
// SetLogger sets logger on self and all subproviders. | |||
func (mc *multiProvider) SetLogger(logger log.Logger) { | |||
mc.logger = logger | |||
for _, p := range mc.providers { | |||
p.SetLogger(logger) | |||
} | |||
} | |||
// SaveFullCommit saves on all providers, and aborts on the first error. | |||
func (mc *multiProvider) SaveFullCommit(fc FullCommit) (err error) { | |||
for _, p := range mc.providers { | |||
err = p.SaveFullCommit(fc) | |||
if err != nil { | |||
return | |||
} | |||
} | |||
return | |||
} | |||
// LatestFullCommit loads the latest from all providers and provides | |||
// the latest FullCommit that satisfies the conditions. | |||
// Returns the first error encountered. | |||
func (mc *multiProvider) LatestFullCommit(chainID string, minHeight, maxHeight int64) (fc FullCommit, err error) { | |||
for _, p := range mc.providers { | |||
var commit FullCommit | |||
commit, err = p.LatestFullCommit(chainID, minHeight, maxHeight) | |||
if lerr.IsErrCommitNotFound(err) { | |||
err = nil | |||
continue | |||
} else if err != nil { | |||
return | |||
} | |||
if fc == (FullCommit{}) { | |||
fc = commit | |||
} else if commit.Height() > fc.Height() { | |||
fc = commit | |||
} | |||
if fc.Height() == maxHeight { | |||
return | |||
} | |||
} | |||
if fc == (FullCommit{}) { | |||
err = lerr.ErrCommitNotFound() | |||
return | |||
} | |||
return | |||
} | |||
// ValidatorSet returns validator set at height as provided by the first | |||
// provider which has it, or an error otherwise. | |||
func (mc *multiProvider) ValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) { | |||
for _, p := range mc.providers { | |||
valset, err = p.ValidatorSet(chainID, height) | |||
if err == nil { | |||
// TODO Log unexpected types of errors. | |||
return valset, nil | |||
} | |||
} | |||
return nil, lerr.ErrUnknownValidators(chainID, height) | |||
} |
@ -1,32 +0,0 @@ | |||
package lite | |||
import ( | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// Provider provides information for the lite client to sync validators. | |||
// Examples: MemProvider, files.Provider, client.Provider, CacheProvider. | |||
type Provider interface { | |||
// LatestFullCommit returns the latest commit with minHeight <= height <= | |||
// maxHeight. | |||
// If maxHeight is zero, returns the latest where minHeight <= height. | |||
LatestFullCommit(chainID string, minHeight, maxHeight int64) (FullCommit, error) | |||
// Get the valset that corresponds to chainID and height and return. | |||
// Height must be >= 1. | |||
ValidatorSet(chainID string, height int64) (*types.ValidatorSet, error) | |||
// Set a logger. | |||
SetLogger(logger log.Logger) | |||
} | |||
// A provider that can also persist new information. | |||
// Examples: MemProvider, files.Provider, CacheProvider. | |||
type PersistentProvider interface { | |||
Provider | |||
// SaveFullCommit saves a FullCommit (without verification). | |||
SaveFullCommit(fc FullCommit) error | |||
} |
@ -1,141 +0,0 @@ | |||
package lite | |||
import ( | |||
"errors" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
dbm "github.com/tendermint/tm-db" | |||
log "github.com/tendermint/tendermint/libs/log" | |||
lerr "github.com/tendermint/tendermint/lite/errors" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// missingProvider doesn't store anything, always a miss. | |||
// Designed as a mock for testing. | |||
type missingProvider struct{} | |||
// NewMissingProvider returns a provider which does not store anything and always misses. | |||
func NewMissingProvider() PersistentProvider { | |||
return missingProvider{} | |||
} | |||
func (missingProvider) SaveFullCommit(FullCommit) error { return nil } | |||
func (missingProvider) LatestFullCommit(chainID string, minHeight, maxHeight int64) (FullCommit, error) { | |||
return FullCommit{}, lerr.ErrCommitNotFound() | |||
} | |||
func (missingProvider) ValidatorSet(chainID string, height int64) (*types.ValidatorSet, error) { | |||
return nil, errors.New("missing validator set") | |||
} | |||
func (missingProvider) SetLogger(_ log.Logger) {} | |||
func TestMemProvider(t *testing.T) { | |||
p := NewDBProvider("mem", dbm.NewMemDB()) | |||
checkProvider(t, p, "test-mem", "empty") | |||
} | |||
func TestMultiProvider(t *testing.T) { | |||
p := NewMultiProvider( | |||
NewMissingProvider(), | |||
NewDBProvider("mem", dbm.NewMemDB()), | |||
NewMissingProvider(), | |||
) | |||
checkProvider(t, p, "test-cache", "kjfhekfhkewhgit") | |||
} | |||
func checkProvider(t *testing.T, p PersistentProvider, chainID, app string) { | |||
assert, require := assert.New(t), require.New(t) | |||
appHash := []byte(app) | |||
keys := genPrivKeys(5) | |||
count := 10 | |||
// Make a bunch of full commits. | |||
fcz := make([]FullCommit, count) | |||
for i := 0; i < count; i++ { | |||
vals := keys.ToValidators(10, int64(count/2)) | |||
h := int64(20 + 10*i) | |||
fcz[i] = keys.GenFullCommit(chainID, h, nil, vals, vals, appHash, []byte("params"), []byte("results"), 0, 5) | |||
} | |||
// Check that provider is initially empty. | |||
fc, err := p.LatestFullCommit(chainID, 1, 1<<63-1) | |||
require.NotNil(err) | |||
assert.True(lerr.IsErrCommitNotFound(err)) | |||
// Save all full commits to the provider. | |||
for _, fc := range fcz { | |||
err = p.SaveFullCommit(fc) | |||
require.Nil(err) | |||
// Make sure we can get it back. | |||
fc2, err := p.LatestFullCommit(chainID, fc.Height(), fc.Height()) | |||
assert.Nil(err) | |||
assert.Equal(fc.SignedHeader, fc2.SignedHeader) | |||
assert.Equal(fc.Validators, fc2.Validators) | |||
assert.Equal(fc.NextValidators, fc2.NextValidators) | |||
} | |||
// Make sure we get the last hash if we overstep. | |||
fc, err = p.LatestFullCommit(chainID, 1, 5000) | |||
if assert.Nil(err) { | |||
assert.Equal(fcz[count-1].Height(), fc.Height()) | |||
assert.Equal(fcz[count-1], fc) | |||
} | |||
// ... and middle ones as well. | |||
fc, err = p.LatestFullCommit(chainID, 1, 47) | |||
if assert.Nil(err) { | |||
// we only step by 10, so 40 must be the one below this | |||
assert.EqualValues(40, fc.Height()) | |||
} | |||
} | |||
// This will make a get height, and if it is good, set the data as well. | |||
func checkLatestFullCommit(t *testing.T, p PersistentProvider, chainID string, ask, expect int64) { | |||
fc, err := p.LatestFullCommit(chainID, 1, ask) | |||
require.Nil(t, err) | |||
if assert.Equal(t, expect, fc.Height()) { | |||
err = p.SaveFullCommit(fc) | |||
require.Nil(t, err) | |||
} | |||
} | |||
func TestMultiLatestFullCommit(t *testing.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 := NewDBProvider("mem1", dbm.NewMemDB()) | |||
p2 := NewDBProvider("mem2", dbm.NewMemDB()) | |||
cp := NewMultiProvider(p, p2) | |||
chainID := "cache-best-height" | |||
appHash := []byte("01234567") | |||
keys := genPrivKeys(5) | |||
count := 10 | |||
// Set a bunch of full commits. | |||
for i := 0; i < count; i++ { | |||
vals := keys.ToValidators(10, int64(count/2)) | |||
h := int64(10 * (i + 1)) | |||
fc := keys.GenFullCommit(chainID, h, nil, vals, vals, appHash, []byte("params"), []byte("results"), 0, 5) | |||
err := p2.SaveFullCommit(fc) | |||
require.NoError(err) | |||
} | |||
// Get a few heights from the cache and set them proper. | |||
checkLatestFullCommit(t, cp, chainID, 57, 50) | |||
checkLatestFullCommit(t, cp, chainID, 33, 30) | |||
// make sure they are set in p as well (but nothing else) | |||
checkLatestFullCommit(t, p, chainID, 44, 30) | |||
checkLatestFullCommit(t, p, chainID, 50, 50) | |||
checkLatestFullCommit(t, p, chainID, 99, 50) | |||
// now, query the cache for a higher value | |||
checkLatestFullCommit(t, p2, chainID, 99, 90) | |||
checkLatestFullCommit(t, cp, chainID, 99, 90) | |||
} |
@ -1,48 +0,0 @@ | |||
package proxy | |||
import ( | |||
"bytes" | |||
"errors" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func ValidateBlockMeta(meta *types.BlockMeta, sh types.SignedHeader) error { | |||
if meta == nil { | |||
return errors.New("expecting a non-nil BlockMeta") | |||
} | |||
// TODO: check the BlockID?? | |||
return ValidateHeader(&meta.Header, sh) | |||
} | |||
func ValidateBlock(meta *types.Block, sh types.SignedHeader) error { | |||
if meta == nil { | |||
return errors.New("expecting a non-nil Block") | |||
} | |||
err := ValidateHeader(&meta.Header, sh) | |||
if err != nil { | |||
return err | |||
} | |||
if !bytes.Equal(meta.Data.Hash(), meta.Header.DataHash) { | |||
return errors.New("data hash doesn't match header") | |||
} | |||
return nil | |||
} | |||
func ValidateHeader(head *types.Header, sh types.SignedHeader) error { | |||
if head == nil { | |||
return errors.New("expecting a non-nil Header") | |||
} | |||
if sh.Header == nil { | |||
return errors.New("unexpected empty SignedHeader") | |||
} | |||
// Make sure they are for the same height (obvious fail). | |||
if head.Height != sh.Height { | |||
return errors.New("header heights mismatched") | |||
} | |||
// Check if they are equal by using hashes. | |||
if !bytes.Equal(head.Hash(), sh.Hash()) { | |||
return errors.New("headers don't match") | |||
} | |||
return nil | |||
} |
@ -1,21 +0,0 @@ | |||
package proxy | |||
import ( | |||
"github.com/pkg/errors" | |||
) | |||
type errNoData struct{} | |||
func (e errNoData) Error() string { | |||
return "No data returned for query" | |||
} | |||
// IsErrNoData checks whether an error is due to a query returning empty data | |||
func IsErrNoData(err error) bool { | |||
_, ok := errors.Cause(err).(errNoData) | |||
return ok | |||
} | |||
func ErrNoData() error { | |||
return errors.Wrap(errNoData{}, "") | |||
} |
@ -1,14 +0,0 @@ | |||
package proxy | |||
import ( | |||
"github.com/tendermint/tendermint/crypto/merkle" | |||
) | |||
func defaultProofRuntime() *merkle.ProofRuntime { | |||
prt := merkle.NewProofRuntime() | |||
prt.RegisterOpDecoder( | |||
merkle.ProofOpSimpleValue, | |||
merkle.SimpleValueOpDecoder, | |||
) | |||
return prt | |||
} |
@ -1,187 +0,0 @@ | |||
package proxy | |||
import ( | |||
"context" | |||
"net/http" | |||
amino "github.com/tendermint/go-amino" | |||
"github.com/tendermint/tendermint/libs/bytes" | |||
"github.com/tendermint/tendermint/libs/log" | |||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
rpcserver "github.com/tendermint/tendermint/rpc/jsonrpc/server" | |||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
const ( | |||
wsEndpoint = "/websocket" | |||
) | |||
// StartProxy will start the websocket manager on the client, | |||
// set up the rpc routes to proxy via the given client, | |||
// and start up an http/rpc server on the location given by bind (eg. :1234) | |||
// NOTE: This function blocks - you may want to call it in a go-routine. | |||
func StartProxy(c rpcclient.Client, listenAddr string, logger log.Logger, maxOpenConnections int) error { | |||
err := c.Start() | |||
if err != nil { | |||
return err | |||
} | |||
cdc := amino.NewCodec() | |||
ctypes.RegisterAmino(cdc) | |||
r := RPCRoutes(c) | |||
// build the handler... | |||
mux := http.NewServeMux() | |||
rpcserver.RegisterRPCFuncs(mux, r, cdc, logger) | |||
unsubscribeFromAllEvents := func(remoteAddr string) { | |||
if err := c.UnsubscribeAll(context.Background(), remoteAddr); err != nil { | |||
logger.Error("Failed to unsubscribe from events", "err", err) | |||
} | |||
} | |||
wm := rpcserver.NewWebsocketManager(r, cdc, rpcserver.OnDisconnect(unsubscribeFromAllEvents)) | |||
wm.SetLogger(logger) | |||
// core.SetLogger(logger) | |||
mux.HandleFunc(wsEndpoint, wm.WebsocketHandler) | |||
config := rpcserver.DefaultConfig() | |||
config.MaxOpenConnections = maxOpenConnections | |||
l, err := rpcserver.Listen(listenAddr, config) | |||
if err != nil { | |||
return err | |||
} | |||
return rpcserver.Serve(l, mux, logger, config) | |||
} | |||
// RPCRoutes just routes everything to the given client, as if it were | |||
// a tendermint fullnode. | |||
// | |||
// if we want security, the client must implement it as a secure client | |||
func RPCRoutes(c rpcclient.Client) map[string]*rpcserver.RPCFunc { | |||
return map[string]*rpcserver.RPCFunc{ | |||
// Subscribe/unsubscribe are reserved for websocket events. | |||
"subscribe": rpcserver.NewWSRPCFunc(c.(Wrapper).SubscribeWS, "query"), | |||
"unsubscribe": rpcserver.NewWSRPCFunc(c.(Wrapper).UnsubscribeWS, "query"), | |||
"unsubscribe_all": rpcserver.NewWSRPCFunc(c.(Wrapper).UnsubscribeAllWS, ""), | |||
// info API | |||
"status": rpcserver.NewRPCFunc(makeStatusFunc(c), ""), | |||
"blockchain": rpcserver.NewRPCFunc(makeBlockchainInfoFunc(c), "minHeight,maxHeight"), | |||
"genesis": rpcserver.NewRPCFunc(makeGenesisFunc(c), ""), | |||
"block": rpcserver.NewRPCFunc(makeBlockFunc(c), "height"), | |||
"block_by_hash": rpcserver.NewRPCFunc(makeBlockByHashFunc(c), "hash"), | |||
"commit": rpcserver.NewRPCFunc(makeCommitFunc(c), "height"), | |||
"tx": rpcserver.NewRPCFunc(makeTxFunc(c), "hash,prove"), | |||
"validators": rpcserver.NewRPCFunc(makeValidatorsFunc(c), "height"), | |||
// broadcast API | |||
"broadcast_tx_commit": rpcserver.NewRPCFunc(makeBroadcastTxCommitFunc(c), "tx"), | |||
"broadcast_tx_sync": rpcserver.NewRPCFunc(makeBroadcastTxSyncFunc(c), "tx"), | |||
"broadcast_tx_async": rpcserver.NewRPCFunc(makeBroadcastTxAsyncFunc(c), "tx"), | |||
// abci API | |||
"abci_query": rpcserver.NewRPCFunc(makeABCIQueryFunc(c), "path,data"), | |||
"abci_info": rpcserver.NewRPCFunc(makeABCIInfoFunc(c), ""), | |||
} | |||
} | |||
func makeStatusFunc(c rpcclient.StatusClient) func(ctx *rpctypes.Context) (*ctypes.ResultStatus, error) { | |||
return func(ctx *rpctypes.Context) (*ctypes.ResultStatus, error) { | |||
return c.Status() | |||
} | |||
} | |||
func makeBlockchainInfoFunc(c rpcclient.Client) func( | |||
ctx *rpctypes.Context, | |||
minHeight, | |||
maxHeight int64, | |||
) (*ctypes.ResultBlockchainInfo, error) { | |||
return func(ctx *rpctypes.Context, minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { | |||
return c.BlockchainInfo(minHeight, maxHeight) | |||
} | |||
} | |||
func makeGenesisFunc(c rpcclient.Client) func(ctx *rpctypes.Context) (*ctypes.ResultGenesis, error) { | |||
return func(ctx *rpctypes.Context) (*ctypes.ResultGenesis, error) { | |||
return c.Genesis() | |||
} | |||
} | |||
func makeBlockFunc(c rpcclient.Client) func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultBlock, error) { | |||
return func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultBlock, error) { | |||
return c.Block(height) | |||
} | |||
} | |||
func makeBlockByHashFunc(c rpcclient.Client) func(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultBlock, error) { | |||
return func(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultBlock, error) { | |||
return c.BlockByHash(hash) | |||
} | |||
} | |||
func makeCommitFunc(c rpcclient.Client) func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultCommit, error) { | |||
return func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultCommit, error) { | |||
return c.Commit(height) | |||
} | |||
} | |||
func makeTxFunc(c rpcclient.Client) func(ctx *rpctypes.Context, hash []byte, prove bool) (*ctypes.ResultTx, error) { | |||
return func(ctx *rpctypes.Context, hash []byte, prove bool) (*ctypes.ResultTx, error) { | |||
return c.Tx(hash, prove) | |||
} | |||
} | |||
func makeValidatorsFunc(c rpcclient.Client) func( | |||
ctx *rpctypes.Context, | |||
height *int64, | |||
) (*ctypes.ResultValidators, error) { | |||
return func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultValidators, error) { | |||
return c.Validators(height, 0, 0) | |||
} | |||
} | |||
func makeBroadcastTxCommitFunc(c rpcclient.Client) func( | |||
ctx *rpctypes.Context, | |||
tx types.Tx, | |||
) (*ctypes.ResultBroadcastTxCommit, error) { | |||
return func(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { | |||
return c.BroadcastTxCommit(tx) | |||
} | |||
} | |||
func makeBroadcastTxSyncFunc(c rpcclient.Client) func( | |||
ctx *rpctypes.Context, | |||
tx types.Tx, | |||
) (*ctypes.ResultBroadcastTx, error) { | |||
return func(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
return c.BroadcastTxSync(tx) | |||
} | |||
} | |||
func makeBroadcastTxAsyncFunc(c rpcclient.Client) func( | |||
ctx *rpctypes.Context, | |||
tx types.Tx, | |||
) (*ctypes.ResultBroadcastTx, error) { | |||
return func(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
return c.BroadcastTxAsync(tx) | |||
} | |||
} | |||
func makeABCIQueryFunc(c rpcclient.Client) func( | |||
ctx *rpctypes.Context, | |||
path string, | |||
data bytes.HexBytes, | |||
) (*ctypes.ResultABCIQuery, error) { | |||
return func(ctx *rpctypes.Context, path string, data bytes.HexBytes) (*ctypes.ResultABCIQuery, error) { | |||
return c.ABCIQuery(path, data) | |||
} | |||
} | |||
func makeABCIInfoFunc(c rpcclient.Client) func(ctx *rpctypes.Context) (*ctypes.ResultABCIInfo, error) { | |||
return func(ctx *rpctypes.Context) (*ctypes.ResultABCIInfo, error) { | |||
return c.ABCIInfo() | |||
} | |||
} |
@ -1,148 +0,0 @@ | |||
package proxy | |||
import ( | |||
"fmt" | |||
"strings" | |||
"github.com/pkg/errors" | |||
"github.com/tendermint/tendermint/crypto/merkle" | |||
"github.com/tendermint/tendermint/libs/bytes" | |||
"github.com/tendermint/tendermint/lite" | |||
lerr "github.com/tendermint/tendermint/lite/errors" | |||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// GetWithProof will query the key on the given node, and verify it has | |||
// a valid proof, as defined by the Verifier. | |||
// | |||
// If there is any error in checking, returns an error. | |||
func GetWithProof(prt *merkle.ProofRuntime, key []byte, reqHeight int64, node rpcclient.Client, | |||
cert lite.Verifier) ( | |||
val bytes.HexBytes, height int64, proof *merkle.Proof, err error) { | |||
if reqHeight < 0 { | |||
err = errors.New("height cannot be negative") | |||
return | |||
} | |||
res, err := GetWithProofOptions(prt, "/key", key, | |||
rpcclient.ABCIQueryOptions{Height: reqHeight, Prove: true}, | |||
node, cert) | |||
if err != nil { | |||
return | |||
} | |||
resp := res.Response | |||
val, height = resp.Value, resp.Height | |||
return val, height, proof, err | |||
} | |||
// GetWithProofOptions is useful if you want full access to the ABCIQueryOptions. | |||
// XXX Usage of path? It's not used, and sometimes it's /, sometimes /key, sometimes /store. | |||
func GetWithProofOptions(prt *merkle.ProofRuntime, path string, key []byte, opts rpcclient.ABCIQueryOptions, | |||
node rpcclient.Client, cert lite.Verifier) ( | |||
*ctypes.ResultABCIQuery, error) { | |||
opts.Prove = true | |||
res, err := node.ABCIQueryWithOptions(path, key, opts) | |||
if err != nil { | |||
return nil, err | |||
} | |||
resp := res.Response | |||
// Validate the response, e.g. height. | |||
if resp.IsErr() { | |||
err = errors.Errorf("query error for key %d: %d", key, resp.Code) | |||
return nil, err | |||
} | |||
if len(resp.Key) == 0 || resp.Proof == nil { | |||
return nil, lerr.ErrEmptyTree() | |||
} | |||
if resp.Height == 0 { | |||
return nil, errors.New("height returned is zero") | |||
} | |||
// AppHash for height H is in header H+1 | |||
signedHeader, err := GetCertifiedCommit(resp.Height+1, node, cert) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Validate the proof against the certified header to ensure data integrity. | |||
if resp.Value != nil { | |||
// Value exists | |||
// XXX How do we encode the key into a string... | |||
storeName, err := parseQueryStorePath(path) | |||
if err != nil { | |||
return nil, err | |||
} | |||
kp := merkle.KeyPath{} | |||
kp = kp.AppendKey([]byte(storeName), merkle.KeyEncodingURL) | |||
kp = kp.AppendKey(resp.Key, merkle.KeyEncodingURL) | |||
err = prt.VerifyValue(resp.Proof, signedHeader.AppHash, kp.String(), resp.Value) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "couldn't verify value proof") | |||
} | |||
return &ctypes.ResultABCIQuery{Response: resp}, nil | |||
} | |||
// Value absent | |||
// Validate the proof against the certified header to ensure data integrity. | |||
// XXX How do we encode the key into a string... | |||
err = prt.VerifyAbsence(resp.Proof, signedHeader.AppHash, string(resp.Key)) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "couldn't verify absence proof") | |||
} | |||
return &ctypes.ResultABCIQuery{Response: resp}, nil | |||
} | |||
func parseQueryStorePath(path string) (storeName string, err error) { | |||
if !strings.HasPrefix(path, "/") { | |||
return "", fmt.Errorf("expected path to start with /") | |||
} | |||
paths := strings.SplitN(path[1:], "/", 3) | |||
switch { | |||
case len(paths) != 3: | |||
return "", fmt.Errorf("expected format like /store/<storeName>/key") | |||
case paths[0] != "store": | |||
return "", fmt.Errorf("expected format like /store/<storeName>/key") | |||
case paths[2] != "key": | |||
return "", fmt.Errorf("expected format like /store/<storeName>/key") | |||
} | |||
return paths[1], nil | |||
} | |||
// GetCertifiedCommit gets the signed header for a given height and certifies | |||
// it. Returns error if unable to get a proven header. | |||
func GetCertifiedCommit(h int64, client rpcclient.Client, cert lite.Verifier) (types.SignedHeader, error) { | |||
// FIXME: cannot use cert.GetByHeight for now, as it also requires | |||
// Validators and will fail on querying tendermint for non-current height. | |||
// When this is supported, we should use it instead... | |||
rpcclient.WaitForHeight(client, h, nil) | |||
cresp, err := client.Commit(&h) | |||
if err != nil { | |||
return types.SignedHeader{}, err | |||
} | |||
// Validate downloaded checkpoint with our request and trust store. | |||
sh := cresp.SignedHeader | |||
if sh.Height != h { | |||
return types.SignedHeader{}, fmt.Errorf("height mismatch: want %v got %v", | |||
h, sh.Height) | |||
} | |||
if err = cert.Verify(sh); err != nil { | |||
return types.SignedHeader{}, err | |||
} | |||
return sh, nil | |||
} |
@ -1,163 +0,0 @@ | |||
package proxy | |||
import ( | |||
"fmt" | |||
"os" | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/abci/example/kvstore" | |||
"github.com/tendermint/tendermint/crypto/merkle" | |||
"github.com/tendermint/tendermint/lite" | |||
certclient "github.com/tendermint/tendermint/lite/client" | |||
nm "github.com/tendermint/tendermint/node" | |||
"github.com/tendermint/tendermint/rpc/client" | |||
rpclocal "github.com/tendermint/tendermint/rpc/client/local" | |||
rpctest "github.com/tendermint/tendermint/rpc/test" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var node *nm.Node | |||
var chainID = "tendermint_test" // TODO use from config. | |||
//nolint:unused | |||
var waitForEventTimeout = 5 * time.Second | |||
// TODO fix tests!! | |||
func TestMain(m *testing.M) { | |||
app := kvstore.NewApplication() | |||
node = rpctest.StartTendermint(app) | |||
code := m.Run() | |||
rpctest.StopTendermint(node) | |||
os.Exit(code) | |||
} | |||
func kvstoreTx(k, v []byte) []byte { | |||
return []byte(fmt.Sprintf("%s=%s", k, v)) | |||
} | |||
// TODO: enable it after general proof format has been adapted | |||
// in abci/examples/kvstore.go | |||
//nolint:unused,deadcode | |||
func _TestAppProofs(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
prt := defaultProofRuntime() | |||
cl := rpclocal.New(node) | |||
client.WaitForHeight(cl, 1, nil) | |||
// This sets up our trust on the node based on some past point. | |||
source := certclient.NewProvider(chainID, cl) | |||
seed, err := source.LatestFullCommit(chainID, 1, 1) | |||
require.NoError(err, "%#v", err) | |||
cert := lite.NewBaseVerifier(chainID, seed.Height(), seed.Validators) | |||
// Wait for tx confirmation. | |||
done := make(chan int64) | |||
go func() { | |||
evtTyp := types.EventTx | |||
_, err = client.WaitForOneEvent(cl, evtTyp, waitForEventTimeout) | |||
require.Nil(err, "%#v", err) | |||
close(done) | |||
}() | |||
// Submit a transaction. | |||
k := []byte("my-key") | |||
v := []byte("my-value") | |||
tx := kvstoreTx(k, v) | |||
br, err := cl.BroadcastTxCommit(tx) | |||
require.NoError(err, "%#v", err) | |||
require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx) | |||
require.EqualValues(0, br.DeliverTx.Code) | |||
brh := br.Height | |||
// Fetch latest after tx commit. | |||
<-done | |||
latest, err := source.LatestFullCommit(chainID, 1, 1<<63-1) | |||
require.NoError(err, "%#v", err) | |||
rootHash := latest.SignedHeader.AppHash | |||
if rootHash == nil { | |||
// Fetch one block later, AppHash hasn't been committed yet. | |||
// TODO find a way to avoid doing this. | |||
client.WaitForHeight(cl, latest.SignedHeader.Height+1, nil) | |||
latest, err = source.LatestFullCommit(chainID, latest.SignedHeader.Height+1, 1<<63-1) | |||
require.NoError(err, "%#v", err) | |||
rootHash = latest.SignedHeader.AppHash | |||
} | |||
require.NotNil(rootHash) | |||
// verify a query before the tx block has no data (and valid non-exist proof) | |||
bs, height, proof, err := GetWithProof(prt, k, brh-1, cl, cert) | |||
require.NoError(err, "%#v", err) | |||
require.NotNil(proof) | |||
require.Equal(height, brh-1) | |||
// require.NotNil(proof) | |||
// TODO: Ensure that *some* keys will be there, ensuring that proof is nil, | |||
// (currently there's a race condition) | |||
// and ensure that proof proves absence of k. | |||
require.Nil(bs) | |||
// but given that block it is good | |||
bs, height, proof, err = GetWithProof(prt, k, brh, cl, cert) | |||
require.NoError(err, "%#v", err) | |||
require.NotNil(proof) | |||
require.Equal(height, brh) | |||
assert.EqualValues(v, bs) | |||
err = prt.VerifyValue(proof, rootHash, string(k), bs) // XXX key encoding | |||
assert.NoError(err, "%#v", err) | |||
// Test non-existing key. | |||
missing := []byte("my-missing-key") | |||
bs, _, proof, err = GetWithProof(prt, missing, 0, cl, cert) | |||
require.NoError(err) | |||
require.Nil(bs) | |||
require.NotNil(proof) | |||
err = prt.VerifyAbsence(proof, rootHash, string(missing)) // XXX VerifyAbsence(), keyencoding | |||
assert.NoError(err, "%#v", err) | |||
err = prt.VerifyAbsence(proof, rootHash, string(k)) // XXX VerifyAbsence(), keyencoding | |||
assert.Error(err, "%#v", err) | |||
} | |||
func TestTxProofs(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
cl := rpclocal.New(node) | |||
client.WaitForHeight(cl, 1, nil) | |||
tx := kvstoreTx([]byte("key-a"), []byte("value-a")) | |||
br, err := cl.BroadcastTxCommit(tx) | |||
require.NoError(err, "%#v", err) | |||
require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx) | |||
require.EqualValues(0, br.DeliverTx.Code) | |||
brh := br.Height | |||
source := certclient.NewProvider(chainID, cl) | |||
seed, err := source.LatestFullCommit(chainID, brh-2, brh-2) | |||
require.NoError(err, "%#v", err) | |||
cert := lite.NewBaseVerifier(chainID, seed.Height(), seed.Validators) | |||
// First let's make sure a bogus transaction hash returns a valid non-existence proof. | |||
key := types.Tx([]byte("bogus")).Hash() | |||
_, err = cl.Tx(key, true) | |||
require.NotNil(err) | |||
require.Contains(err.Error(), "not found") | |||
// Now let's check with the real tx root hash. | |||
key = types.Tx(tx).Hash() | |||
res, err := cl.Tx(key, true) | |||
require.NoError(err, "%#v", err) | |||
require.NotNil(res) | |||
keyHash := merkle.SimpleHashFromByteSlices([][]byte{key}) | |||
err = res.Proof.Validate(keyHash) | |||
assert.NoError(err, "%#v", err) | |||
commit, err := GetCertifiedCommit(br.Height, cl, cert) | |||
require.Nil(err, "%#v", err) | |||
require.Equal(res.Proof.RootHash, commit.Header.DataHash) | |||
} |
@ -1,211 +0,0 @@ | |||
package proxy_test | |||
import ( | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/tendermint/tendermint/lite/proxy" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var ( | |||
deadBeefTxs = types.Txs{[]byte("DE"), []byte("AD"), []byte("BE"), []byte("EF")} | |||
deadBeefHash = deadBeefTxs.Hash() | |||
testTime1 = time.Date(2018, 1, 1, 1, 1, 1, 1, time.UTC) | |||
testTime2 = time.Date(2017, 1, 2, 1, 1, 1, 1, time.UTC) | |||
) | |||
var hdrHeight11 = types.Header{ | |||
Height: 11, | |||
Time: testTime1, | |||
ValidatorsHash: []byte("Tendermint"), | |||
} | |||
func TestValidateBlock(t *testing.T) { | |||
tests := []struct { | |||
block *types.Block | |||
signedHeader types.SignedHeader | |||
wantErr string | |||
}{ | |||
{ | |||
block: nil, wantErr: "non-nil Block", | |||
}, | |||
{ | |||
block: &types.Block{}, wantErr: "unexpected empty SignedHeader", | |||
}, | |||
// Start Header.Height mismatch test | |||
{ | |||
block: &types.Block{Header: types.Header{Height: 10}}, | |||
signedHeader: types.SignedHeader{Header: &types.Header{Height: 11}}, | |||
wantErr: "header heights mismatched", | |||
}, | |||
{ | |||
block: &types.Block{Header: types.Header{Height: 11}}, | |||
signedHeader: types.SignedHeader{Header: &types.Header{Height: 11}}, | |||
}, | |||
// End Header.Height mismatch test | |||
// Start Header.Hash mismatch test | |||
{ | |||
block: &types.Block{Header: hdrHeight11}, | |||
signedHeader: types.SignedHeader{Header: &types.Header{Height: 11}}, | |||
wantErr: "headers don't match", | |||
}, | |||
{ | |||
block: &types.Block{Header: hdrHeight11}, | |||
signedHeader: types.SignedHeader{Header: &hdrHeight11}, | |||
}, | |||
// End Header.Hash mismatch test | |||
// Start Header.Data hash mismatch test | |||
{ | |||
block: &types.Block{ | |||
Header: types.Header{Height: 11}, | |||
Data: types.Data{Txs: []types.Tx{[]byte("0xDE"), []byte("AD")}}, | |||
}, | |||
signedHeader: types.SignedHeader{ | |||
Header: &types.Header{Height: 11}, | |||
Commit: types.NewCommit(11, 0, types.BlockID{Hash: []byte("0xDEADBEEF")}, nil), | |||
}, | |||
wantErr: "data hash doesn't match header", | |||
}, | |||
{ | |||
block: &types.Block{ | |||
Header: types.Header{Height: 11, DataHash: deadBeefHash}, | |||
Data: types.Data{Txs: deadBeefTxs}, | |||
}, | |||
signedHeader: types.SignedHeader{ | |||
Header: &types.Header{Height: 11}, | |||
Commit: types.NewCommit(11, 0, types.BlockID{Hash: []byte("DEADBEEF")}, nil), | |||
}, | |||
}, | |||
// End Header.Data hash mismatch test | |||
} | |||
for i, tt := range tests { | |||
err := proxy.ValidateBlock(tt.block, tt.signedHeader) | |||
if tt.wantErr != "" { | |||
if err == nil { | |||
assert.FailNowf(t, "Unexpectedly passed", "#%d", i) | |||
} else { | |||
assert.Contains(t, err.Error(), tt.wantErr, "#%d should contain the substring\n\n", i) | |||
} | |||
continue | |||
} | |||
assert.Nil(t, err, "#%d: expecting a nil error", i) | |||
} | |||
} | |||
func TestValidateBlockMeta(t *testing.T) { | |||
tests := []struct { | |||
meta *types.BlockMeta | |||
signedHeader types.SignedHeader | |||
wantErr string | |||
}{ | |||
{ | |||
meta: nil, wantErr: "non-nil BlockMeta", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{}, wantErr: "unexpected empty SignedHeader", | |||
}, | |||
// Start Header.Height mismatch test | |||
{ | |||
meta: &types.BlockMeta{Header: types.Header{Height: 10}}, | |||
signedHeader: types.SignedHeader{Header: &types.Header{Height: 11}}, | |||
wantErr: "header heights mismatched", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{Header: types.Header{Height: 11}}, | |||
signedHeader: types.SignedHeader{Header: &types.Header{Height: 11}}, | |||
}, | |||
// End Header.Height mismatch test | |||
// Start Headers don't match test | |||
{ | |||
meta: &types.BlockMeta{Header: hdrHeight11}, | |||
signedHeader: types.SignedHeader{Header: &types.Header{Height: 11}}, | |||
wantErr: "headers don't match", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{Header: hdrHeight11}, | |||
signedHeader: types.SignedHeader{Header: &hdrHeight11}, | |||
}, | |||
{ | |||
meta: &types.BlockMeta{ | |||
Header: types.Header{ | |||
Height: 11, | |||
ValidatorsHash: []byte("lite-test"), | |||
// TODO: should be able to use empty time after Amino upgrade | |||
Time: testTime1, | |||
}, | |||
}, | |||
signedHeader: types.SignedHeader{ | |||
Header: &types.Header{Height: 11, DataHash: deadBeefHash}, | |||
}, | |||
wantErr: "headers don't match", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{ | |||
Header: types.Header{ | |||
Height: 11, DataHash: deadBeefHash, | |||
ValidatorsHash: []byte("Tendermint"), | |||
Time: testTime1, | |||
}, | |||
}, | |||
signedHeader: types.SignedHeader{ | |||
Header: &types.Header{ | |||
Height: 11, DataHash: deadBeefHash, | |||
ValidatorsHash: []byte("Tendermint"), | |||
Time: testTime2, | |||
}, | |||
Commit: types.NewCommit(11, 0, types.BlockID{Hash: []byte("DEADBEEF")}, nil), | |||
}, | |||
wantErr: "headers don't match", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{ | |||
Header: types.Header{ | |||
Height: 11, DataHash: deadBeefHash, | |||
ValidatorsHash: []byte("Tendermint"), | |||
Time: testTime2, | |||
}, | |||
}, | |||
signedHeader: types.SignedHeader{ | |||
Header: &types.Header{ | |||
Height: 11, DataHash: deadBeefHash, | |||
ValidatorsHash: []byte("Tendermint-x"), | |||
Time: testTime2, | |||
}, | |||
Commit: types.NewCommit(11, 0, types.BlockID{Hash: []byte("DEADBEEF")}, nil), | |||
}, | |||
wantErr: "headers don't match", | |||
}, | |||
// End Headers don't match test | |||
} | |||
for i, tt := range tests { | |||
err := proxy.ValidateBlockMeta(tt.meta, tt.signedHeader) | |||
if tt.wantErr != "" { | |||
if err == nil { | |||
assert.FailNowf(t, "Unexpectedly passed", "#%d: wanted error %q", i, tt.wantErr) | |||
} else { | |||
assert.Contains(t, err.Error(), tt.wantErr, "#%d should contain the substring\n\n", i) | |||
} | |||
continue | |||
} | |||
assert.Nil(t, err, "#%d: expecting a nil error", i) | |||
} | |||
} |
@ -1,49 +0,0 @@ | |||
package proxy | |||
import ( | |||
"github.com/pkg/errors" | |||
dbm "github.com/tendermint/tm-db" | |||
log "github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/lite" | |||
lclient "github.com/tendermint/tendermint/lite/client" | |||
) | |||
func NewVerifier( | |||
chainID, | |||
rootDir string, | |||
client lclient.SignStatusClient, | |||
logger log.Logger, | |||
cacheSize int, | |||
) (*lite.DynamicVerifier, error) { | |||
logger = logger.With("module", "lite/proxy") | |||
logger.Info("lite/proxy/NewVerifier()...", "chainID", chainID, "rootDir", rootDir, "client", client) | |||
memProvider := lite.NewDBProvider("trusted.mem", dbm.NewMemDB()).SetLimit(cacheSize) | |||
lvlProvider := lite.NewDBProvider("trusted.lvl", dbm.NewDB("trust-base", dbm.GoLevelDBBackend, rootDir)) | |||
trust := lite.NewMultiProvider( | |||
memProvider, | |||
lvlProvider, | |||
) | |||
source := lclient.NewProvider(chainID, client) | |||
cert := lite.NewDynamicVerifier(chainID, trust, source) | |||
cert.SetLogger(logger) // Sets logger recursively. | |||
// TODO: Make this more secure, e.g. make it interactive in the console? | |||
_, err := trust.LatestFullCommit(chainID, 1, 1<<63-1) | |||
if err != nil { | |||
logger.Info("lite/proxy/NewVerifier found no trusted full commit, initializing from source from height 1...") | |||
fc, err := source.LatestFullCommit(chainID, 1, 1) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "fetching source full commit @ height 1") | |||
} | |||
err = trust.SaveFullCommit(fc) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "saving full commit to trusted") | |||
} | |||
} | |||
return cert, nil | |||
} |
@ -1,275 +0,0 @@ | |||
package proxy | |||
import ( | |||
"context" | |||
"github.com/tendermint/tendermint/crypto/merkle" | |||
"github.com/tendermint/tendermint/libs/bytes" | |||
"github.com/tendermint/tendermint/lite" | |||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" | |||
) | |||
var _ rpcclient.Client = Wrapper{} | |||
// Wrapper wraps a rpcclient with a Verifier and double-checks any input that is | |||
// provable before passing it along. Allows you to make any rpcclient fully secure. | |||
type Wrapper struct { | |||
rpcclient.Client | |||
cert *lite.DynamicVerifier | |||
prt *merkle.ProofRuntime | |||
} | |||
// SecureClient uses a given Verifier to wrap an connection to an untrusted | |||
// host and return a cryptographically secure rpc client. | |||
// | |||
// If it is wrapping an HTTP rpcclient, it will also wrap the websocket interface | |||
func SecureClient(c rpcclient.Client, cert *lite.DynamicVerifier) Wrapper { | |||
prt := defaultProofRuntime() | |||
wrap := Wrapper{c, cert, prt} | |||
// TODO: no longer possible as no more such interface exposed.... | |||
// if we wrap http client, then we can swap out the event switch to filter | |||
// if hc, ok := c.(*rpcclient.HTTP); ok { | |||
// evt := hc.WSEvents.EventSwitch | |||
// hc.WSEvents.EventSwitch = WrappedSwitch{evt, wrap} | |||
// } | |||
return wrap | |||
} | |||
// ABCIQueryWithOptions exposes all options for the ABCI query and verifies the returned proof | |||
func (w Wrapper) ABCIQueryWithOptions(path string, data bytes.HexBytes, | |||
opts rpcclient.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { | |||
res, err := GetWithProofOptions(w.prt, path, data, opts, w.Client, w.cert) | |||
return res, err | |||
} | |||
// ABCIQuery uses default options for the ABCI query and verifies the returned proof | |||
func (w Wrapper) ABCIQuery(path string, data bytes.HexBytes) (*ctypes.ResultABCIQuery, error) { | |||
return w.ABCIQueryWithOptions(path, data, rpcclient.DefaultABCIQueryOptions) | |||
} | |||
// Tx queries for a given tx and verifies the proof if it was requested | |||
func (w Wrapper) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { | |||
res, err := w.Client.Tx(hash, prove) | |||
if !prove || err != nil { | |||
return res, err | |||
} | |||
h := res.Height | |||
sh, err := GetCertifiedCommit(h, w.Client, w.cert) | |||
if err != nil { | |||
return res, err | |||
} | |||
err = res.Proof.Validate(sh.DataHash) | |||
return res, err | |||
} | |||
// BlockchainInfo requests a list of headers and verifies them all... | |||
// Rather expensive. | |||
// | |||
// TODO: optimize this if used for anything needing performance | |||
func (w Wrapper) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { | |||
r, err := w.Client.BlockchainInfo(minHeight, maxHeight) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// go and verify every blockmeta in the result.... | |||
for _, meta := range r.BlockMetas { | |||
// get a checkpoint to verify from | |||
res, err := w.Commit(&meta.Header.Height) | |||
if err != nil { | |||
return nil, err | |||
} | |||
sh := res.SignedHeader | |||
err = ValidateBlockMeta(meta, sh) | |||
if err != nil { | |||
return nil, err | |||
} | |||
} | |||
return r, nil | |||
} | |||
// Block returns an entire block and verifies all signatures | |||
func (w Wrapper) Block(height *int64) (*ctypes.ResultBlock, error) { | |||
resBlock, err := w.Client.Block(height) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// get a checkpoint to verify from | |||
resCommit, err := w.Commit(height) | |||
if err != nil { | |||
return nil, err | |||
} | |||
sh := resCommit.SignedHeader | |||
err = ValidateBlock(resBlock.Block, sh) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return resBlock, nil | |||
} | |||
// BlockByHash returns an entire block and verifies all signatures | |||
func (w Wrapper) BlockByHash(hash []byte) (*ctypes.ResultBlock, error) { | |||
resBlock, err := w.Client.BlockByHash(hash) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// get a checkpoint to verify from | |||
resCommit, err := w.Commit(&resBlock.Block.Height) | |||
if err != nil { | |||
return nil, err | |||
} | |||
sh := resCommit.SignedHeader | |||
err = ValidateBlock(resBlock.Block, sh) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return resBlock, nil | |||
} | |||
// Commit downloads the Commit and certifies it with the lite. | |||
// | |||
// This is the foundation for all other verification in this module | |||
func (w Wrapper) Commit(height *int64) (*ctypes.ResultCommit, error) { | |||
if height == nil { | |||
resStatus, err := w.Client.Status() | |||
if err != nil { | |||
return nil, err | |||
} | |||
// NOTE: If resStatus.CatchingUp, there is a race | |||
// condition where the validator set for the next height | |||
// isn't available until some time after the blockstore | |||
// has height h on the remote node. This isn't an issue | |||
// once the node has caught up, and a syncing node likely | |||
// won't have this issue esp with the implementation we | |||
// have here, but we may have to address this at some | |||
// point. | |||
height = new(int64) | |||
*height = resStatus.SyncInfo.LatestBlockHeight | |||
} | |||
rpcclient.WaitForHeight(w.Client, *height, nil) | |||
res, err := w.Client.Commit(height) | |||
// if we got it, then verify it | |||
if err == nil { | |||
sh := res.SignedHeader | |||
err = w.cert.Verify(sh) | |||
} | |||
return res, err | |||
} | |||
func (w Wrapper) RegisterOpDecoder(typ string, dec merkle.OpDecoder) { | |||
w.prt.RegisterOpDecoder(typ, dec) | |||
} | |||
// SubscribeWS subscribes for events using the given query and remote address as | |||
// a subscriber, but does not verify responses (UNSAFE)! | |||
func (w Wrapper) SubscribeWS(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, error) { | |||
out, err := w.Client.Subscribe(context.Background(), ctx.RemoteAddr(), query) | |||
if err != nil { | |||
return nil, err | |||
} | |||
go func() { | |||
for { | |||
select { | |||
case resultEvent := <-out: | |||
// XXX(melekes) We should have a switch here that performs a validation | |||
// depending on the event's type. | |||
ctx.WSConn.TryWriteRPCResponse( | |||
rpctypes.NewRPCSuccessResponse( | |||
ctx.WSConn.Codec(), | |||
ctx.JSONReq.ID, | |||
resultEvent, | |||
)) | |||
case <-w.Client.Quit(): | |||
return | |||
} | |||
} | |||
}() | |||
return &ctypes.ResultSubscribe{}, nil | |||
} | |||
// UnsubscribeWS calls original client's Unsubscribe using remote address as a | |||
// subscriber. | |||
func (w Wrapper) UnsubscribeWS(ctx *rpctypes.Context, query string) (*ctypes.ResultUnsubscribe, error) { | |||
err := w.Client.Unsubscribe(context.Background(), ctx.RemoteAddr(), query) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &ctypes.ResultUnsubscribe{}, nil | |||
} | |||
// UnsubscribeAllWS calls original client's UnsubscribeAll using remote address | |||
// as a subscriber. | |||
func (w Wrapper) UnsubscribeAllWS(ctx *rpctypes.Context) (*ctypes.ResultUnsubscribe, error) { | |||
err := w.Client.UnsubscribeAll(context.Background(), ctx.RemoteAddr()) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &ctypes.ResultUnsubscribe{}, nil | |||
} | |||
// // WrappedSwitch creates a websocket connection that auto-verifies any info | |||
// // coming through before passing it along. | |||
// // | |||
// // Since the verification takes 1-2 rpc calls, this is obviously only for | |||
// // relatively low-throughput situations that can tolerate a bit extra latency | |||
// type WrappedSwitch struct { | |||
// types.EventSwitch | |||
// client rpcclient.Client | |||
// } | |||
// // FireEvent verifies any block or header returned from the eventswitch | |||
// func (s WrappedSwitch) FireEvent(event string, data events.EventData) { | |||
// tm, ok := data.(types.TMEventData) | |||
// if !ok { | |||
// fmt.Printf("bad type %#v\n", data) | |||
// return | |||
// } | |||
// // check to validate it if possible, and drop if not valid | |||
// switch t := tm.(type) { | |||
// case types.EventDataNewBlockHeader: | |||
// err := verifyHeader(s.client, t.Header) | |||
// if err != nil { | |||
// fmt.Printf("Invalid header: %#v\n", err) | |||
// return | |||
// } | |||
// case types.EventDataNewBlock: | |||
// err := verifyBlock(s.client, t.Block) | |||
// if err != nil { | |||
// fmt.Printf("Invalid block: %#v\n", err) | |||
// return | |||
// } | |||
// // TODO: can we verify tx as well? anything else | |||
// } | |||
// // looks good, we fire it | |||
// s.EventSwitch.FireEvent(event, data) | |||
// } | |||
// func verifyHeader(c rpcclient.Client, head *types.Header) error { | |||
// // get a checkpoint to verify from | |||
// commit, err := c.Commit(&head.Height) | |||
// if err != nil { | |||
// return err | |||
// } | |||
// check := certclient.CommitFromResult(commit) | |||
// return ValidateHeader(head, check) | |||
// } | |||
// | |||
// func verifyBlock(c rpcclient.Client, block *types.Block) error { | |||
// // get a checkpoint to verify from | |||
// commit, err := c.Commit(&block.Height) | |||
// if err != nil { | |||
// return err | |||
// } | |||
// check := certclient.CommitFromResult(commit) | |||
// return ValidateBlock(block, check) | |||
// } |
@ -1,13 +0,0 @@ | |||
package lite | |||
import ( | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// Verifier checks the votes to make sure the block really is signed properly. | |||
// Verifier must know the current or recent set of validitors by some other | |||
// means. | |||
type Verifier interface { | |||
Verify(sheader types.SignedHeader) error | |||
ChainID() string | |||
} |