|
package kvstore
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
dbm "github.com/tendermint/tm-db"
|
|
|
|
"github.com/tendermint/tendermint/abci/example/code"
|
|
"github.com/tendermint/tendermint/abci/types"
|
|
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
pc "github.com/tendermint/tendermint/proto/tendermint/crypto/keys"
|
|
tmtypes "github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
const (
|
|
ValidatorSetChangePrefix string = "val:"
|
|
)
|
|
|
|
//-----------------------------------------
|
|
|
|
var _ types.Application = (*PersistentKVStoreApplication)(nil)
|
|
|
|
type PersistentKVStoreApplication struct {
|
|
app *Application
|
|
|
|
// validator set
|
|
ValUpdates []types.ValidatorUpdate
|
|
|
|
valAddrToPubKeyMap map[string]pc.PublicKey
|
|
|
|
logger log.Logger
|
|
}
|
|
|
|
func NewPersistentKVStoreApplication(dbDir string) *PersistentKVStoreApplication {
|
|
name := "kvstore"
|
|
db, err := dbm.NewGoLevelDB(name, dbDir)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
state := loadState(db)
|
|
|
|
return &PersistentKVStoreApplication{
|
|
app: &Application{state: state},
|
|
valAddrToPubKeyMap: make(map[string]pc.PublicKey),
|
|
logger: log.NewNopLogger(),
|
|
}
|
|
}
|
|
|
|
func (app *PersistentKVStoreApplication) SetLogger(l log.Logger) {
|
|
app.logger = l
|
|
}
|
|
|
|
func (app *PersistentKVStoreApplication) Info(req types.RequestInfo) types.ResponseInfo {
|
|
res := app.app.Info(req)
|
|
res.LastBlockHeight = app.app.state.Height
|
|
res.LastBlockAppHash = app.app.state.AppHash
|
|
return res
|
|
}
|
|
|
|
func (app *PersistentKVStoreApplication) SetOption(req types.RequestSetOption) types.ResponseSetOption {
|
|
return app.app.SetOption(req)
|
|
}
|
|
|
|
// tx is either "val:pubkey!power" or "key=value" or just arbitrary bytes
|
|
func (app *PersistentKVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx {
|
|
// if it starts with "val:", update the validator set
|
|
// format is "val:pubkey!power"
|
|
if isValidatorTx(req.Tx) {
|
|
// update validators in the merkle tree
|
|
// and in app.ValUpdates
|
|
return app.execValidatorTx(req.Tx)
|
|
}
|
|
|
|
// otherwise, update the key-value store
|
|
return app.app.DeliverTx(req)
|
|
}
|
|
|
|
func (app *PersistentKVStoreApplication) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx {
|
|
return app.app.CheckTx(req)
|
|
}
|
|
|
|
// Commit will panic if InitChain was not called
|
|
func (app *PersistentKVStoreApplication) Commit() types.ResponseCommit {
|
|
return app.app.Commit()
|
|
}
|
|
|
|
// When path=/val and data={validator address}, returns the validator update (types.ValidatorUpdate) varint encoded.
|
|
// For any other path, returns an associated value or nil if missing.
|
|
func (app *PersistentKVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) {
|
|
switch reqQuery.Path {
|
|
case "/val":
|
|
key := []byte("val:" + string(reqQuery.Data))
|
|
value, err := app.app.state.db.Get(key)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
resQuery.Key = reqQuery.Data
|
|
resQuery.Value = value
|
|
return
|
|
default:
|
|
return app.app.Query(reqQuery)
|
|
}
|
|
}
|
|
|
|
// Save the validators in the merkle tree
|
|
func (app *PersistentKVStoreApplication) InitChain(req types.RequestInitChain) types.ResponseInitChain {
|
|
for _, v := range req.Validators {
|
|
r := app.updateValidator(v)
|
|
if r.IsErr() {
|
|
app.logger.Error("Error updating validators", "r", r)
|
|
}
|
|
}
|
|
return types.ResponseInitChain{}
|
|
}
|
|
|
|
// Track the block hash and header information
|
|
func (app *PersistentKVStoreApplication) BeginBlock(req types.RequestBeginBlock) types.ResponseBeginBlock {
|
|
// reset valset changes
|
|
app.ValUpdates = make([]types.ValidatorUpdate, 0)
|
|
|
|
for _, ev := range req.ByzantineValidators {
|
|
if ev.Type == tmtypes.ABCIEvidenceTypeDuplicateVote {
|
|
// decrease voting power by 1
|
|
if ev.TotalVotingPower == 0 {
|
|
continue
|
|
}
|
|
app.updateValidator(types.ValidatorUpdate{
|
|
PubKey: app.valAddrToPubKeyMap[string(ev.Validator.Address)],
|
|
Power: ev.TotalVotingPower - 1,
|
|
})
|
|
}
|
|
}
|
|
|
|
return types.ResponseBeginBlock{}
|
|
}
|
|
|
|
// Update the validator set
|
|
func (app *PersistentKVStoreApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock {
|
|
return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates}
|
|
}
|
|
|
|
func (app *PersistentKVStoreApplication) ListSnapshots(
|
|
req types.RequestListSnapshots) types.ResponseListSnapshots {
|
|
return types.ResponseListSnapshots{}
|
|
}
|
|
|
|
func (app *PersistentKVStoreApplication) LoadSnapshotChunk(
|
|
req types.RequestLoadSnapshotChunk) types.ResponseLoadSnapshotChunk {
|
|
return types.ResponseLoadSnapshotChunk{}
|
|
}
|
|
|
|
func (app *PersistentKVStoreApplication) OfferSnapshot(
|
|
req types.RequestOfferSnapshot) types.ResponseOfferSnapshot {
|
|
return types.ResponseOfferSnapshot{Result: types.ResponseOfferSnapshot_ABORT}
|
|
}
|
|
|
|
func (app *PersistentKVStoreApplication) ApplySnapshotChunk(
|
|
req types.RequestApplySnapshotChunk) types.ResponseApplySnapshotChunk {
|
|
return types.ResponseApplySnapshotChunk{Result: types.ResponseApplySnapshotChunk_ABORT}
|
|
}
|
|
|
|
//---------------------------------------------
|
|
// update validators
|
|
|
|
func (app *PersistentKVStoreApplication) Validators() (validators []types.ValidatorUpdate) {
|
|
itr, err := app.app.state.db.Iterator(nil, nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
for ; itr.Valid(); itr.Next() {
|
|
if isValidatorTx(itr.Key()) {
|
|
validator := new(types.ValidatorUpdate)
|
|
err := types.ReadMessage(bytes.NewBuffer(itr.Value()), validator)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
validators = append(validators, *validator)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func MakeValSetChangeTx(pubkey pc.PublicKey, power int64) []byte {
|
|
pk, err := cryptoenc.PubKeyFromProto(pubkey)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
pubStr := base64.StdEncoding.EncodeToString(pk.Bytes())
|
|
return []byte(fmt.Sprintf("val:%s!%d", pubStr, power))
|
|
}
|
|
|
|
func isValidatorTx(tx []byte) bool {
|
|
return strings.HasPrefix(string(tx), ValidatorSetChangePrefix)
|
|
}
|
|
|
|
// format is "val:pubkey!power"
|
|
// pubkey is a base64-encoded 32-byte ed25519 key
|
|
func (app *PersistentKVStoreApplication) execValidatorTx(tx []byte) types.ResponseDeliverTx {
|
|
tx = tx[len(ValidatorSetChangePrefix):]
|
|
|
|
//get the pubkey and power
|
|
pubKeyAndPower := strings.Split(string(tx), "!")
|
|
if len(pubKeyAndPower) != 2 {
|
|
return types.ResponseDeliverTx{
|
|
Code: code.CodeTypeEncodingError,
|
|
Log: fmt.Sprintf("Expected 'pubkey!power'. Got %v", pubKeyAndPower)}
|
|
}
|
|
pubkeyS, powerS := pubKeyAndPower[0], pubKeyAndPower[1]
|
|
|
|
// decode the pubkey
|
|
pubkey, err := base64.StdEncoding.DecodeString(pubkeyS)
|
|
if err != nil {
|
|
return types.ResponseDeliverTx{
|
|
Code: code.CodeTypeEncodingError,
|
|
Log: fmt.Sprintf("Pubkey (%s) is invalid base64", pubkeyS)}
|
|
}
|
|
|
|
// decode the power
|
|
power, err := strconv.ParseInt(powerS, 10, 64)
|
|
if err != nil {
|
|
return types.ResponseDeliverTx{
|
|
Code: code.CodeTypeEncodingError,
|
|
Log: fmt.Sprintf("Power (%s) is not an int", powerS)}
|
|
}
|
|
|
|
// update
|
|
return app.updateValidator(types.Ed25519ValidatorUpdate(pubkey, power))
|
|
}
|
|
|
|
// add, update, or remove a validator
|
|
func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate) types.ResponseDeliverTx {
|
|
key := []byte("val:" + string(v.PubKey.GetEd25519()))
|
|
pubkey, err := cryptoenc.PubKeyFromProto(v.PubKey)
|
|
if err != nil {
|
|
panic(fmt.Errorf("can't decode public key: %w", err))
|
|
}
|
|
|
|
if v.Power == 0 {
|
|
// remove validator
|
|
hasKey, err := app.app.state.db.Has(key)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if !hasKey {
|
|
pubStr := base64.StdEncoding.EncodeToString(pubkey.Bytes())
|
|
return types.ResponseDeliverTx{
|
|
Code: code.CodeTypeUnauthorized,
|
|
Log: fmt.Sprintf("Cannot remove non-existent validator %s", pubStr)}
|
|
}
|
|
app.app.state.db.Delete(key)
|
|
delete(app.valAddrToPubKeyMap, string(pubkey.Address()))
|
|
} else {
|
|
// add or update validator
|
|
value := bytes.NewBuffer(make([]byte, 0))
|
|
if err := types.WriteMessage(&v, value); err != nil {
|
|
return types.ResponseDeliverTx{
|
|
Code: code.CodeTypeEncodingError,
|
|
Log: fmt.Sprintf("Error encoding validator: %v", err)}
|
|
}
|
|
app.app.state.db.Set(key, value.Bytes())
|
|
app.valAddrToPubKeyMap[string(pubkey.Address())] = v.PubKey
|
|
}
|
|
|
|
// we only update the changes array if we successfully updated the tree
|
|
app.ValUpdates = append(app.ValUpdates, v)
|
|
|
|
return types.ResponseDeliverTx{Code: code.CodeTypeOK}
|
|
}
|