Some checks are pending
docker-build-cometbft / vars (push) Waiting to run
docker-build-cometbft / build-images (amd64, ubuntu-24.04) (push) Blocked by required conditions
docker-build-cometbft / build-images (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
docker-build-cometbft / merge-images (push) Blocked by required conditions
docker-build-e2e-node / vars (push) Waiting to run
docker-build-e2e-node / build-images (amd64, ubuntu-24.04) (push) Blocked by required conditions
docker-build-e2e-node / build-images (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
docker-build-e2e-node / merge-images (push) Blocked by required conditions
553 lines
17 KiB
Go
553 lines
17 KiB
Go
package kvstore
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
dbm "git.cw.tr/mukan-network/mukan-consensus-db"
|
|
|
|
"git.cw.tr/mukan-network/mukan-consensus/abci/types"
|
|
cryptoencoding "git.cw.tr/mukan-network/mukan-consensus/crypto/encoding"
|
|
"git.cw.tr/mukan-network/mukan-consensus/libs/log"
|
|
cryptoproto "git.cw.tr/mukan-network/mukan-consensus/proto/tendermint/crypto"
|
|
"git.cw.tr/mukan-network/mukan-consensus/version"
|
|
)
|
|
|
|
var (
|
|
stateKey = []byte("stateKey")
|
|
kvPairPrefixKey = []byte("kvPairKey:")
|
|
)
|
|
|
|
const (
|
|
ValidatorPrefix = "val="
|
|
AppVersion uint64 = 1
|
|
)
|
|
|
|
var _ types.Application = (*Application)(nil)
|
|
|
|
// Application is the kvstore state machine. It complies with the abci.Application interface.
|
|
// It takes transactions in the form of key=value and saves them in a database. This is
|
|
// a somewhat trivial example as there is no real state execution
|
|
type Application struct {
|
|
types.BaseApplication
|
|
|
|
state State
|
|
RetainBlocks int64 // blocks to retain after commit (via ResponseCommit.RetainHeight)
|
|
stagedTxs [][]byte
|
|
logger log.Logger
|
|
|
|
// validator set
|
|
valUpdates []types.ValidatorUpdate
|
|
valAddrToPubKeyMap map[string]cryptoproto.PublicKey
|
|
|
|
// If true, the app will generate block events in BeginBlock. Used to test the event indexer
|
|
// Should be false by default to avoid generating too much data.
|
|
genBlockEvents bool
|
|
}
|
|
|
|
// NewApplication creates an instance of the kvstore from the provided database
|
|
func NewApplication(db dbm.DB) *Application {
|
|
return &Application{
|
|
logger: log.NewNopLogger(),
|
|
state: loadState(db),
|
|
valAddrToPubKeyMap: make(map[string]cryptoproto.PublicKey),
|
|
}
|
|
}
|
|
|
|
// NewPersistentApplication creates a new application using the goleveldb database engine
|
|
func NewPersistentApplication(dbDir string) *Application {
|
|
name := "kvstore"
|
|
db, err := dbm.NewGoLevelDB(name, dbDir)
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to create persistent app at %s: %w", dbDir, err))
|
|
}
|
|
return NewApplication(db)
|
|
}
|
|
|
|
// NewInMemoryApplication creates a new application from an in memory database.
|
|
// Nothing will be persisted.
|
|
func NewInMemoryApplication() *Application {
|
|
return NewApplication(dbm.NewMemDB())
|
|
}
|
|
|
|
func (app *Application) SetGenBlockEvents() {
|
|
app.genBlockEvents = true
|
|
}
|
|
|
|
// Info returns information about the state of the application. This is generally used everytime a Tendermint instance
|
|
// begins and let's the application know what Tendermint versions it's interacting with. Based from this information,
|
|
// Tendermint will ensure it is in sync with the application by potentially replaying the blocks it has. If the
|
|
// Application returns a 0 appBlockHeight, Tendermint will call InitChain to initialize the application with consensus related data
|
|
func (app *Application) Info(context.Context, *types.RequestInfo) (*types.ResponseInfo, error) {
|
|
// Tendermint expects the application to persist validators, on start-up we need to reload them to memory if they exist
|
|
if len(app.valAddrToPubKeyMap) == 0 && app.state.Height > 0 {
|
|
validators := app.getValidators()
|
|
for _, v := range validators {
|
|
pubkey, err := cryptoencoding.PubKeyFromProto(v.PubKey)
|
|
if err != nil {
|
|
panic(fmt.Errorf("can't decode public key: %w", err))
|
|
}
|
|
app.valAddrToPubKeyMap[string(pubkey.Address())] = v.PubKey
|
|
}
|
|
}
|
|
|
|
return &types.ResponseInfo{
|
|
Data: fmt.Sprintf("{\"size\":%v}", app.state.Size),
|
|
Version: version.ABCIVersion,
|
|
AppVersion: AppVersion,
|
|
LastBlockHeight: app.state.Height,
|
|
LastBlockAppHash: app.state.Hash(),
|
|
}, nil
|
|
}
|
|
|
|
// InitChain takes the genesis validators and stores them in the kvstore. It returns the application hash in the
|
|
// case that the application starts prepopulated with values. This method is called whenever a new instance of the application
|
|
// starts (i.e. app height = 0).
|
|
func (app *Application) InitChain(_ context.Context, req *types.RequestInitChain) (*types.ResponseInitChain, error) {
|
|
for _, v := range req.Validators {
|
|
app.updateValidator(v)
|
|
}
|
|
appHash := make([]byte, 8)
|
|
binary.PutVarint(appHash, app.state.Size)
|
|
return &types.ResponseInitChain{
|
|
AppHash: appHash,
|
|
}, nil
|
|
}
|
|
|
|
// CheckTx handles inbound transactions or in the case of recheckTx assesses old transaction validity after a state transition.
|
|
// As this is called frequently, it's preferably to keep the check as stateless and as quick as possible.
|
|
// Here we check that the transaction has the correctly key=value format.
|
|
// For the KVStore we check that each transaction has the valid tx format:
|
|
// - Contains one and only one `=`
|
|
// - `=` is not the first or last byte.
|
|
// - if key is `val` that the validator update transaction is also valid
|
|
func (app *Application) CheckTx(_ context.Context, req *types.RequestCheckTx) (*types.ResponseCheckTx, error) {
|
|
// If it is a validator update transaction, check that it is correctly formatted
|
|
if isValidatorTx(req.Tx) {
|
|
if _, _, _, err := parseValidatorTx(req.Tx); err != nil {
|
|
//nolint:nilerr
|
|
return &types.ResponseCheckTx{Code: CodeTypeInvalidTxFormat}, nil
|
|
}
|
|
} else if !isValidTx(req.Tx) {
|
|
return &types.ResponseCheckTx{Code: CodeTypeInvalidTxFormat}, nil
|
|
}
|
|
|
|
return &types.ResponseCheckTx{Code: CodeTypeOK, GasWanted: 1}, nil
|
|
}
|
|
|
|
// Tx must have a format like key:value or key=value. That is:
|
|
// - it must have one and only one ":" or "="
|
|
// - It must not begin or end with these special characters
|
|
func isValidTx(tx []byte) bool {
|
|
if bytes.Count(tx, []byte(":")) == 1 && bytes.Count(tx, []byte("=")) == 0 {
|
|
if !bytes.HasPrefix(tx, []byte(":")) && !bytes.HasSuffix(tx, []byte(":")) {
|
|
return true
|
|
}
|
|
} else if bytes.Count(tx, []byte("=")) == 1 && bytes.Count(tx, []byte(":")) == 0 {
|
|
if !bytes.HasPrefix(tx, []byte("=")) && !bytes.HasSuffix(tx, []byte("=")) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// PrepareProposal is called when the node is a proposer. CometBFT stages a set of transactions to the application. As the
|
|
// KVStore has two accepted formats, `:` and `=`, we modify all instances of `:` with `=` to make it consistent. Note: this is
|
|
// quite a trivial example of transaction modification.
|
|
// NOTE: we assume that CometBFT will never provide more transactions than can fit in a block.
|
|
func (app *Application) PrepareProposal(ctx context.Context, req *types.RequestPrepareProposal) (*types.ResponsePrepareProposal, error) {
|
|
return &types.ResponsePrepareProposal{Txs: app.formatTxs(ctx, req.Txs)}, nil
|
|
}
|
|
|
|
// formatTxs validates and excludes invalid transactions
|
|
// also substitutes all the transactions with x:y to x=y
|
|
func (app *Application) formatTxs(ctx context.Context, blockData [][]byte) [][]byte {
|
|
txs := make([][]byte, 0, len(blockData))
|
|
for _, tx := range blockData {
|
|
if resp, err := app.CheckTx(ctx, &types.RequestCheckTx{Tx: tx}); err == nil && resp.Code == CodeTypeOK {
|
|
txs = append(txs, bytes.Replace(tx, []byte(":"), []byte("="), 1))
|
|
}
|
|
}
|
|
return txs
|
|
}
|
|
|
|
// ProcessProposal is called whenever a node receives a complete proposal. It allows the application to validate the proposal.
|
|
// Only validators who can vote will have this method called. For the KVstore we reuse CheckTx.
|
|
func (app *Application) ProcessProposal(ctx context.Context, req *types.RequestProcessProposal) (*types.ResponseProcessProposal, error) {
|
|
for _, tx := range req.Txs {
|
|
// As CheckTx is a full validity check we can simply reuse this
|
|
if resp, err := app.CheckTx(ctx, &types.RequestCheckTx{Tx: tx}); err != nil || resp.Code != CodeTypeOK {
|
|
return &types.ResponseProcessProposal{Status: types.ResponseProcessProposal_REJECT}, nil
|
|
}
|
|
}
|
|
return &types.ResponseProcessProposal{Status: types.ResponseProcessProposal_ACCEPT}, nil
|
|
}
|
|
|
|
// FinalizeBlock executes the block against the application state. It punishes validators who equivocated and
|
|
// updates validators according to transactions in a block. The rest of the transactions are regular key value
|
|
// updates and are cached in memory and will be persisted once Commit is called.
|
|
// ConsensusParams are never changed.
|
|
func (app *Application) FinalizeBlock(_ context.Context, req *types.RequestFinalizeBlock) (*types.ResponseFinalizeBlock, error) {
|
|
// reset valset changes
|
|
app.valUpdates = make([]types.ValidatorUpdate, 0)
|
|
app.stagedTxs = make([][]byte, 0)
|
|
|
|
// Punish validators who committed equivocation.
|
|
for _, ev := range req.Misbehavior {
|
|
if ev.Type == types.MisbehaviorType_DUPLICATE_VOTE {
|
|
addr := string(ev.Validator.Address)
|
|
if pubKey, ok := app.valAddrToPubKeyMap[addr]; ok {
|
|
app.valUpdates = append(app.valUpdates, types.ValidatorUpdate{
|
|
PubKey: pubKey,
|
|
Power: ev.Validator.Power - 1,
|
|
})
|
|
app.logger.Info("Decreased val power by 1 because of the equivocation",
|
|
"val", addr)
|
|
} else {
|
|
panic(fmt.Errorf("wanted to punish val %q but can't find it", addr))
|
|
}
|
|
}
|
|
}
|
|
|
|
respTxs := make([]*types.ExecTxResult, len(req.Txs))
|
|
for i, tx := range req.Txs {
|
|
if isValidatorTx(tx) {
|
|
keyType, pubKey, power, err := parseValidatorTx(tx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
app.valUpdates = append(app.valUpdates, types.UpdateValidator(pubKey, power, keyType))
|
|
} else {
|
|
app.stagedTxs = append(app.stagedTxs, tx)
|
|
}
|
|
|
|
var key, value string
|
|
parts := bytes.Split(tx, []byte("="))
|
|
if len(parts) == 2 {
|
|
key, value = string(parts[0]), string(parts[1])
|
|
} else {
|
|
key, value = string(tx), string(tx)
|
|
}
|
|
respTxs[i] = &types.ExecTxResult{
|
|
Code: CodeTypeOK,
|
|
// With every transaction we can emit a series of events. To make it simple, we just emit the same events.
|
|
Events: []types.Event{
|
|
{
|
|
Type: "app",
|
|
Attributes: []types.EventAttribute{
|
|
{Key: "creator", Value: "Cosmoshi Netowoko", Index: true},
|
|
{Key: "key", Value: key, Index: true},
|
|
{Key: "index_key", Value: "index is working", Index: true},
|
|
{Key: "noindex_key", Value: "index is working", Index: false},
|
|
},
|
|
},
|
|
{
|
|
Type: "app",
|
|
Attributes: []types.EventAttribute{
|
|
{Key: "creator", Value: "Cosmoshi", Index: true},
|
|
{Key: "key", Value: value, Index: true},
|
|
{Key: "index_key", Value: "index is working", Index: true},
|
|
{Key: "noindex_key", Value: "index is working", Index: false},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
app.state.Size++
|
|
}
|
|
|
|
app.state.Height = req.Height
|
|
|
|
response := &types.ResponseFinalizeBlock{TxResults: respTxs, ValidatorUpdates: app.valUpdates, AppHash: app.state.Hash()}
|
|
if !app.genBlockEvents {
|
|
return response, nil
|
|
}
|
|
if app.state.Height%2 == 0 {
|
|
response.Events = []types.Event{
|
|
{
|
|
Type: "begin_event",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "foo",
|
|
Value: "100",
|
|
Index: true,
|
|
},
|
|
{
|
|
Key: "bar",
|
|
Value: "200",
|
|
Index: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "begin_event",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "foo",
|
|
Value: "200",
|
|
Index: true,
|
|
},
|
|
{
|
|
Key: "bar",
|
|
Value: "300",
|
|
Index: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
} else {
|
|
response.Events = []types.Event{
|
|
{
|
|
Type: "begin_event",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "foo",
|
|
Value: "400",
|
|
Index: true,
|
|
},
|
|
{
|
|
Key: "bar",
|
|
Value: "300",
|
|
Index: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
// Commit is called after FinalizeBlock and after Tendermint state which includes the updates to
|
|
// AppHash, ConsensusParams and ValidatorSet has occurred.
|
|
// The KVStore persists the validator updates and the new key values
|
|
func (app *Application) Commit(context.Context, *types.RequestCommit) (*types.ResponseCommit, error) {
|
|
// apply the validator updates to state (note this is really the validator set at h + 2)
|
|
for _, valUpdate := range app.valUpdates {
|
|
app.updateValidator(valUpdate)
|
|
}
|
|
|
|
// persist all the staged txs in the kvstore
|
|
for _, tx := range app.stagedTxs {
|
|
parts := bytes.Split(tx, []byte("="))
|
|
if len(parts) != 2 {
|
|
panic(fmt.Sprintf("unexpected tx format. Expected 2 got %d: %s", len(parts), parts))
|
|
}
|
|
key, value := string(parts[0]), string(parts[1])
|
|
err := app.state.db.Set(prefixKey([]byte(key)), []byte(value))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// persist the state (i.e. size and height)
|
|
saveState(app.state)
|
|
|
|
resp := &types.ResponseCommit{}
|
|
if app.RetainBlocks > 0 && app.state.Height >= app.RetainBlocks {
|
|
resp.RetainHeight = app.state.Height - app.RetainBlocks + 1
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// Returns an associated value or nil if missing.
|
|
func (app *Application) Query(_ context.Context, reqQuery *types.RequestQuery) (*types.ResponseQuery, error) {
|
|
resQuery := &types.ResponseQuery{}
|
|
|
|
if reqQuery.Path == "/val" {
|
|
key := []byte(ValidatorPrefix + string(reqQuery.Data))
|
|
value, err := app.state.db.Get(key)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return &types.ResponseQuery{
|
|
Key: reqQuery.Data,
|
|
Value: value,
|
|
}, nil
|
|
}
|
|
|
|
if reqQuery.Prove {
|
|
value, err := app.state.db.Get(prefixKey(reqQuery.Data))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if value == nil {
|
|
resQuery.Log = "does not exist"
|
|
} else {
|
|
resQuery.Log = "exists"
|
|
}
|
|
resQuery.Index = -1 // TODO make Proof return index
|
|
resQuery.Key = reqQuery.Data
|
|
resQuery.Value = value
|
|
resQuery.Height = app.state.Height
|
|
|
|
return resQuery, nil
|
|
}
|
|
|
|
resQuery.Key = reqQuery.Data
|
|
value, err := app.state.db.Get(prefixKey(reqQuery.Data))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if value == nil {
|
|
resQuery.Log = "does not exist"
|
|
} else {
|
|
resQuery.Log = "exists"
|
|
}
|
|
resQuery.Value = value
|
|
resQuery.Height = app.state.Height
|
|
|
|
return resQuery, nil
|
|
}
|
|
|
|
func (app *Application) Close() error {
|
|
return app.state.db.Close()
|
|
}
|
|
|
|
func isValidatorTx(tx []byte) bool {
|
|
return strings.HasPrefix(string(tx), ValidatorPrefix)
|
|
}
|
|
|
|
func parseValidatorTx(tx []byte) (string, []byte, int64, error) {
|
|
tx = tx[len(ValidatorPrefix):]
|
|
|
|
// get the pubkey and power
|
|
typePubKeyAndPower := strings.Split(string(tx), "!")
|
|
if len(typePubKeyAndPower) != 3 {
|
|
return "", nil, 0, fmt.Errorf("expected 'pubkeytype!pubkey!power'. Got %v", typePubKeyAndPower)
|
|
}
|
|
keyType, pubkeyS, powerS := typePubKeyAndPower[0], typePubKeyAndPower[1], typePubKeyAndPower[2]
|
|
|
|
// decode the pubkey
|
|
pubkey, err := base64.StdEncoding.DecodeString(pubkeyS)
|
|
if err != nil {
|
|
return "", nil, 0, fmt.Errorf("pubkey (%s) is invalid base64", pubkeyS)
|
|
}
|
|
|
|
// decode the power
|
|
power, err := strconv.ParseInt(powerS, 10, 64)
|
|
if err != nil {
|
|
return "", nil, 0, fmt.Errorf("power (%s) is not an int", powerS)
|
|
}
|
|
|
|
if power < 0 {
|
|
return "", nil, 0, fmt.Errorf("power can not be less than 0, got %d", power)
|
|
}
|
|
|
|
return keyType, pubkey, power, nil
|
|
}
|
|
|
|
// add, update, or remove a validator
|
|
func (app *Application) updateValidator(v types.ValidatorUpdate) {
|
|
pubkey, err := cryptoencoding.PubKeyFromProto(v.PubKey)
|
|
if err != nil {
|
|
panic(fmt.Errorf("can't decode public key: %w", err))
|
|
}
|
|
key := []byte(ValidatorPrefix + string(pubkey.Bytes()))
|
|
|
|
if v.Power == 0 {
|
|
// remove validator
|
|
hasKey, err := app.state.db.Has(key)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if !hasKey {
|
|
pubStr := base64.StdEncoding.EncodeToString(pubkey.Bytes())
|
|
app.logger.Info("tried to remove non existent validator. Skipping...", "pubKey", pubStr)
|
|
}
|
|
if err = app.state.db.Delete(key); err != nil {
|
|
panic(err)
|
|
}
|
|
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 {
|
|
panic(err)
|
|
}
|
|
if err = app.state.db.Set(key, value.Bytes()); err != nil {
|
|
panic(err)
|
|
}
|
|
app.valAddrToPubKeyMap[string(pubkey.Address())] = v.PubKey
|
|
}
|
|
}
|
|
|
|
func (app *Application) getValidators() (validators []types.ValidatorUpdate) {
|
|
itr, err := 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)
|
|
}
|
|
}
|
|
if err = itr.Error(); err != nil {
|
|
panic(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// -----------------------------
|
|
|
|
type State struct {
|
|
db dbm.DB
|
|
// Size is essentially the amount of transactions that have been processes.
|
|
// This is used for the appHash
|
|
Size int64 `json:"size"`
|
|
Height int64 `json:"height"`
|
|
}
|
|
|
|
func loadState(db dbm.DB) State {
|
|
var state State
|
|
state.db = db
|
|
stateBytes, err := db.Get(stateKey)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(stateBytes) == 0 {
|
|
return state
|
|
}
|
|
err = json.Unmarshal(stateBytes, &state)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return state
|
|
}
|
|
|
|
func saveState(state State) {
|
|
stateBytes, err := json.Marshal(state)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = state.db.Set(stateKey, stateBytes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Hash returns the hash of the application state. This is computed
|
|
// as the size or number of transactions processed within the state. Note that this isn't
|
|
// a strong guarantee of state machine replication because states could
|
|
// have different kv values but still have the same size.
|
|
// This function is used as the "AppHash"
|
|
func (s State) Hash() []byte {
|
|
appHash := make([]byte, 8)
|
|
binary.PutVarint(appHash, s.Size)
|
|
return appHash
|
|
}
|
|
|
|
func prefixKey(key []byte) []byte {
|
|
return append(kvPairPrefixKey, key...)
|
|
}
|