diff --git a/CHANGELOG.md b/CHANGELOG.md index a852e8a6a..c28f4f7a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,13 @@ # Changelog -## TBD - -- [api] Add validators rewards to block api - ## 0.3.3 *Sept 8th, 2018* IMPROVEMENT - [api] Add block size in bytes +- [api] #100 Add "events" to block response. To get events add ?withEvents=true to request URL. +WARNING! You should sync blockchain from scratch to get this feature working ## 0.3.2 *Sept 8th, 2018* diff --git a/api/api.go b/api/api.go index f9d1ae408..7e5223b53 100644 --- a/api/api.go +++ b/api/api.go @@ -2,6 +2,7 @@ package api import ( "github.com/MinterTeam/minter-go-node/config" + "github.com/MinterTeam/minter-go-node/eventsdb" "log" "net/http" @@ -26,6 +27,7 @@ var ( func init() { cryptoAmino.RegisterAmino(cdc) + eventsdb.RegisterAminoEvents(cdc) } func RunApi(b *minter.Blockchain, node *node.Node) { diff --git a/api/block.go b/api/block.go index d745fbbeb..a6e35f35b 100644 --- a/api/block.go +++ b/api/block.go @@ -6,6 +6,7 @@ import ( "github.com/MinterTeam/minter-go-node/core/rewards" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/eventsdb" "github.com/gorilla/mux" "github.com/tendermint/tendermint/libs/common" "math/big" @@ -21,6 +22,7 @@ type BlockResponse struct { NumTxs int64 `json:"num_txs"` TotalTxs int64 `json:"total_txs"` Transactions []BlockTransactionResponse `json:"transactions"` + Events json.RawMessage `json:"events,omitempty"` Precommits json.RawMessage `json:"precommits"` BlockReward string `json:"block_reward"` Size int `json:"size"` @@ -45,6 +47,7 @@ func Block(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) height, _ := strconv.ParseInt(vars["height"], 10, 64) + includeEvents, _ := strconv.ParseBool(r.URL.Query().Get("withEvents")) block, err := client.Block(&height) blockResults, err := client.BlockResults(&height) @@ -60,8 +63,6 @@ func Block(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusOK) - txs := make([]BlockTransactionResponse, len(block.Block.Data.Txs)) for i, rawTx := range block.Block.Data.Txs { @@ -98,6 +99,25 @@ func Block(w http.ResponseWriter, r *http.Request) { size := len(encodedBlock) + var eventsRaw []byte + + if includeEvents { + events := eventsdb.GetCurrent().GetEvents(height) + + if len(events) > 0 { + eventsRaw, err = cdc.MarshalJSON(events) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(Response{ + Code: 0, + Log: err.Error(), + }) + return + } + } + } + response := BlockResponse{ Hash: block.Block.Hash(), Height: block.Block.Height, @@ -105,11 +125,13 @@ func Block(w http.ResponseWriter, r *http.Request) { NumTxs: block.Block.NumTxs, TotalTxs: block.Block.TotalTxs, Transactions: txs, - Precommits: json.RawMessage(precommits), + Precommits: precommits, BlockReward: rewards.GetRewardForBlock(uint64(height)).String(), Size: size, + Events: eventsRaw, } + w.WriteHeader(http.StatusOK) err = json.NewEncoder(w).Encode(Response{ Code: 0, Result: response, diff --git a/core/minter/minter.go b/core/minter/minter.go index 0f8b060f8..5fd11f62b 100644 --- a/core/minter/minter.go +++ b/core/minter/minter.go @@ -126,7 +126,7 @@ func (app *Blockchain) BeginBlock(req abciTypes.RequestBeginBlock) abciTypes.Res app.stateDeliver.SetValidatorPresent(address) app.validatorsStatuses[address] = ValidatorPresent } else { - app.stateDeliver.SetValidatorAbsent(address) + app.stateDeliver.SetValidatorAbsent(req.Header.Height, address) app.validatorsStatuses[address] = ValidatorAbsent } } @@ -193,7 +193,7 @@ func (app *Blockchain) EndBlock(req abciTypes.RequestEndBlock) abciTypes.Respons // pay rewards if app.height%12 == 0 { - app.stateDeliver.PayRewards() + app.stateDeliver.PayRewards(req.Height) } hasDroppedValidators := false diff --git a/core/state/state_frozen_fund.go b/core/state/state_frozen_fund.go index 0bfc32246..c062e0f93 100644 --- a/core/state/state_frozen_fund.go +++ b/core/state/state_frozen_fund.go @@ -17,6 +17,7 @@ package state import ( + "github.com/MinterTeam/minter-go-node/eventsdb" "io" "fmt" @@ -119,6 +120,9 @@ func (c *stateFrozenFund) PunishFund(candidateAddress [20]byte) { } func (c *stateFrozenFund) punishFund(candidateAddress [20]byte) { + + edb := eventsdb.GetCurrent() + var NewList []FrozenFund for _, item := range c.data.List { @@ -134,6 +138,16 @@ func (c *stateFrozenFund) punishFund(candidateAddress [20]byte) { newValue.Mul(newValue, big.NewInt(95)) newValue.Div(newValue, big.NewInt(100)) + slashed := big.NewInt(0).Set(item.Value) + slashed.Sub(slashed, newValue) + + edb.SaveEvent(int64(c.blockHeight), eventsdb.SlashEvent{ + Address: item.Address, + Amount: slashed.String(), + Coin: item.Coin, + ValidatorPubKey: item.CandidateKey, + }) + item.Value = newValue } diff --git a/core/state/statedb.go b/core/state/statedb.go index ae7a622ed..c26f5f781 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -19,6 +19,7 @@ package state import ( "fmt" + "github.com/MinterTeam/minter-go-node/eventsdb" "math/big" "sync" @@ -812,7 +813,9 @@ func (s *StateDB) AddAccumReward(pubkey types.Pubkey, reward *big.Int) { } } -func (s *StateDB) PayRewards() { +func (s *StateDB) PayRewards(height int64) { + edb := eventsdb.GetCurrent() + validators := s.getStateValidators() for i := range validators.data { @@ -827,12 +830,24 @@ func (s *StateDB) PayRewards() { DAOReward.Mul(DAOReward, big.NewInt(int64(dao.Commission))) DAOReward.Div(DAOReward, big.NewInt(100)) s.AddBalance(dao.Address, types.GetBaseCoin(), DAOReward) + edb.SaveEvent(height, eventsdb.RewardEvent{ + Role: eventsdb.RoleDAO, + Address: dao.Address, + Amount: DAOReward.String(), + ValidatorPubKey: validator.PubKey, + }) // pay commission to Developers DevelopersReward := big.NewInt(0).Set(totalReward) DevelopersReward.Mul(DevelopersReward, big.NewInt(int64(developers.Commission))) DevelopersReward.Div(DevelopersReward, big.NewInt(100)) s.AddBalance(developers.Address, types.GetBaseCoin(), DevelopersReward) + edb.SaveEvent(height, eventsdb.RewardEvent{ + Role: eventsdb.RoleDevelopers, + Address: developers.Address, + Amount: DevelopersReward.String(), + ValidatorPubKey: validator.PubKey, + }) totalReward.Sub(totalReward, DevelopersReward) totalReward.Sub(totalReward, DAOReward) @@ -843,6 +858,12 @@ func (s *StateDB) PayRewards() { validatorReward.Div(validatorReward, big.NewInt(100)) totalReward.Sub(totalReward, validatorReward) s.AddBalance(validator.CandidateAddress, types.GetBaseCoin(), validatorReward) + edb.SaveEvent(height, eventsdb.RewardEvent{ + Role: eventsdb.RoleValidator, + Address: validator.CandidateAddress, + Amount: validatorReward.String(), + ValidatorPubKey: validator.PubKey, + }) candidate := s.GetStateCandidate(validator.PubKey) @@ -858,7 +879,18 @@ func (s *StateDB) PayRewards() { reward.Mul(reward, stake.BipValue) reward.Div(reward, validator.TotalBipStake) + if reward.Cmp(types.Big0) < 1 { + continue + } + s.AddBalance(stake.Owner, types.GetBaseCoin(), reward) + + edb.SaveEvent(height, eventsdb.RewardEvent{ + Role: eventsdb.RoleDelegator, + Address: stake.Owner, + Amount: reward.String(), + ValidatorPubKey: candidate.PubKey, + }) } validator.AccumReward.SetInt64(0) @@ -990,7 +1022,9 @@ func (s *StateDB) SetCandidateOffline(pubkey []byte) { s.MarkStateCandidateDirty() } -func (s *StateDB) SetValidatorAbsent(address [20]byte) { +func (s *StateDB) SetValidatorAbsent(height int64, address [20]byte) { + edb := eventsdb.GetCurrent() + validators := s.getStateValidators() for i := range validators.data { @@ -1026,6 +1060,16 @@ func (s *StateDB) SetValidatorAbsent(address [20]byte) { newValue.Mul(newValue, big.NewInt(99)) newValue.Div(newValue, big.NewInt(100)) + slashed := big.NewInt(0).Set(stake.Value) + slashed.Sub(slashed, newValue) + + edb.SaveEvent(height, eventsdb.SlashEvent{ + Address: stake.Owner, + Amount: slashed.String(), + Coin: stake.Coin, + ValidatorPubKey: candidate.PubKey, + }) + candidate.Stakes[j] = Stake{ Owner: stake.Owner, Coin: stake.Coin, @@ -1049,6 +1093,8 @@ func (s *StateDB) SetValidatorAbsent(address [20]byte) { func (s *StateDB) PunishByzantineValidator(currentBlock uint64, address [20]byte) { + edb := eventsdb.GetCurrent() + validators := s.getStateValidators() for i := range validators.data { @@ -1072,6 +1118,16 @@ func (s *StateDB) PunishByzantineValidator(currentBlock uint64, address [20]byte newValue.Mul(newValue, big.NewInt(95)) newValue.Div(newValue, big.NewInt(100)) + slashed := big.NewInt(0).Set(stake.Value) + slashed.Sub(slashed, newValue) + + edb.SaveEvent(int64(currentBlock), eventsdb.SlashEvent{ + Address: stake.Owner, + Amount: slashed.String(), + Coin: stake.Coin, + ValidatorPubKey: candidate.PubKey, + }) + s.GetOrNewStateFrozenFunds(currentBlock+UnbondPeriod).AddFund(stake.Owner, candidate.PubKey, stake.Coin, newValue) } diff --git a/core/types/types.go b/core/types/types.go index a032a7b70..c0dc0d57e 100644 --- a/core/types/types.go +++ b/core/types/types.go @@ -17,14 +17,13 @@ package types import ( + "bytes" "encoding/hex" "fmt" + "github.com/MinterTeam/minter-go-node/hexutil" "math/big" "math/rand" "reflect" - - "bytes" - "github.com/MinterTeam/minter-go-node/hexutil" ) const ( diff --git a/eventsdb/amino.go b/eventsdb/amino.go new file mode 100644 index 000000000..c443c8e63 --- /dev/null +++ b/eventsdb/amino.go @@ -0,0 +1,11 @@ +package eventsdb + +import "github.com/tendermint/go-amino" + +func RegisterAminoEvents(codec *amino.Codec) { + codec.RegisterInterface((*Event)(nil), nil) + codec.RegisterConcrete(RewardEvent{}, + "minter/RewardEvent", nil) + codec.RegisterConcrete(SlashEvent{}, + "minter/SlashEvent", nil) +} diff --git a/eventsdb/events.go b/eventsdb/events.go new file mode 100644 index 000000000..896110305 --- /dev/null +++ b/eventsdb/events.go @@ -0,0 +1,61 @@ +package eventsdb + +import ( + "encoding/json" + "fmt" + "github.com/MinterTeam/minter-go-node/core/types" +) + +type Role string + +var ( + RoleValidator Role = "Validator" + RoleDelegator Role = "Delegator" + RoleDAO Role = "DAO" + RoleDevelopers Role = "Developers" +) + +type Event interface{} +type Events []Event + +type RewardEvent struct { + Role Role + Address types.Address + Amount string + ValidatorPubKey types.Pubkey +} + +func (e RewardEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Role string `json:"role"` + Address string `json:"address"` + Amount string `json:"amount"` + ValidatorPubKey string `json:"validator_pub_key"` + }{ + Role: string(e.Role), + Address: e.Address.String(), + Amount: e.Amount, + ValidatorPubKey: fmt.Sprintf("Mp%x", e.ValidatorPubKey), + }) +} + +type SlashEvent struct { + Address types.Address + Amount string + Coin types.CoinSymbol + ValidatorPubKey types.Pubkey +} + +func (e SlashEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Address string `json:"address"` + Amount string `json:"amount"` + Coin string `json:"coin"` + ValidatorPubKey string `json:"validator_pub_key"` + }{ + Address: e.Address.String(), + Amount: e.Amount, + Coin: e.Coin.String(), + ValidatorPubKey: fmt.Sprintf("Mp%x", e.ValidatorPubKey), + }) +} diff --git a/eventsdb/eventsdb.go b/eventsdb/eventsdb.go new file mode 100644 index 000000000..620036790 --- /dev/null +++ b/eventsdb/eventsdb.go @@ -0,0 +1,88 @@ +package eventsdb + +import ( + "encoding/binary" + "github.com/MinterTeam/minter-go-node/cmd/utils" + "github.com/MinterTeam/minter-go-node/mintdb" + "github.com/syndtr/goleveldb/leveldb" + "github.com/tendermint/go-amino" +) + +var cdc = amino.NewCodec() + +var edb *EventsDB + +func init() { + RegisterAminoEvents(cdc) + + eventsDB, err := mintdb.NewLDBDatabase(utils.GetMinterHome()+"/events", 1000, 1000) + + if err != nil { + panic(err) + } + + edb = NewEventsDB(eventsDB) +} + +func GetCurrent() *EventsDB { + return edb +} + +type EventsDB struct { + db *mintdb.LDBDatabase +} + +func NewEventsDB(db *mintdb.LDBDatabase) *EventsDB { + return &EventsDB{ + db: db, + } +} + +func (db *EventsDB) SaveEvent(height int64, event Event) error { + events := db.GetEvents(height) + events = append(events, event) + + return db.SaveEvents(height, events) +} + +func (db *EventsDB) SaveEvents(height int64, events Events) error { + key := getKeyForHeight(height) + + bytes, err := cdc.MarshalBinary(events) + + if err != nil { + return err + } + + return db.db.Put(key, bytes) +} + +func (db *EventsDB) GetEvents(height int64) Events { + key := getKeyForHeight(height) + + data, err := db.db.Get(key) + + if err != nil { + if err == leveldb.ErrNotFound { + return Events{} + } + + panic(err) + } + + var decoded Events + err = cdc.UnmarshalBinary(data, &decoded) + + if err != nil { + panic(err) + } + + return decoded +} + +func getKeyForHeight(height int64) []byte { + var h = make([]byte, 8) + binary.BigEndian.PutUint64(h, uint64(height)) + + return h +}