Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DPOS example #2

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
131 changes: 130 additions & 1 deletion jsonstore/jsonstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import (
"gopkg.in/mgo.v2/bson"
)

const (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we set it to at least 4? so that we can test out the byzantine fault tolerance. Tendermint will continue to produce blocks as long as more than 2/3 of validators are non-malicious.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you are right, that was originally 4 but I've started decreasing it when I struggled to set up nodes locally.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

numActiveValidators = 3
)

var _ types.Application = (*JSONStoreApplication)(nil)
var db *mgo.Database

Expand Down Expand Up @@ -74,6 +78,14 @@ type UserCommentVote struct {
CommentID bson.ObjectId `bson:"commentID" json:"commentID"`
}

// Validator
type Validator struct {
ID bson.ObjectId `bson:"_id" json:"_id"`
Name string `bson:"name" json:"name"`
PublicKey []byte `bson:"publicKey" json:"publicKey"`
Votes []bson.ObjectId `bson:"votes" json:"votes"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validator documents can quickly grow in size (MongoDB documents have a 16MB size limit) if we store the ObjectIDs of voters in the same document (Votes array). There is no upper limit to the number of voters. I would suggest storing the votes count in the document itself and move the ObjectIDs of voters to a separate junction collection. This will also help us sort the validators efficiently.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed added UserValidatorVote collection

}

// JSONStoreApplication ...
type JSONStoreApplication struct {
types.BaseApplication
Expand All @@ -88,7 +100,7 @@ func byteToHex(input []byte) string {
}

func findTotalDocuments(db *mgo.Database) int64 {
collections := [5]string{"posts", "comments", "users", "userpostvotes", "usercommentvotes"}
collections := [6]string{"posts", "comments", "users", "userpostvotes", "usercommentvotes", "validators"}
var sum int64

for _, collection := range collections {
Expand Down Expand Up @@ -235,6 +247,35 @@ func (app *JSONStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx {
}

break

case "upvoteValidator":
entity := body["entity"].(map[string]interface{})

// validate user exists
pubKeyBytes, errDecode := base64.StdEncoding.DecodeString(message["publicKey"].(string))

if errDecode != nil {
panic(errDecode)
}

publicKey := strings.ToUpper(byteToHex(pubKeyBytes))

var user User
err := db.C("users").Find(bson.M{"publicKey": publicKey}).One(&user)
if err != nil {
panic(err)
}

fmt.Println("user validated!")

// validate validator exists & update votes
validatorID := bson.ObjectIdHex(entity["validator"].(string))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also possible to retrieve validatorID via the publicKey. This way you won't have to supply validatorID separately for voting from the front-end. Supplying just the publicKey of the person will suffice.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice. Didn't realise that. Make sense since NodeId is derived the same way

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i got back to this comment now and realised it doesnt work for me.. let's discuss it in the morning

err = db.C("validators").Update(bson.M{"_id": validatorID}, bson.M{"$addToSet": bson.M{"votes": user.ID}})
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are doing only $addToSet here. What about the scenario when the user wants to retract the vote assigned to a particular person?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought of that but I didnt implement it as I wanted to test what I had first and was struggling with that at the end.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added scenario to downvote validator.

panic(err)
}
fmt.Println("validator validated!")

case "upvotePost":
entity := body["entity"].(map[string]interface{})

Expand Down Expand Up @@ -446,6 +487,24 @@ func (app *JSONStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx {

// ===== Data Validation =======
switch body["type"] {
// TODO consider doing more sophisticated validations here like
// checking existance of users and validators here
// assuming tx cannot be tempered with beteen CheckTx and DeliverTx
case "upvoteValidator":
fmt.Println("received upvote start")
entity := body["entity"].(map[string]interface{})

if (entity["validator"] == nil) || (bson.IsObjectIdHex(entity["validator"].(string)) != true) {
codeType = code.CodeTypeBadData
break
}
fmt.Println("received upvote here")
if (entity["user"] == nil) || (bson.IsObjectIdHex(entity["user"].(string)) != true) {
codeType = code.CodeTypeBadData
break
}
fmt.Println("received upvote end")

case "createPost":
entity := body["entity"].(map[string]interface{})

Expand Down Expand Up @@ -524,3 +583,73 @@ func (app *JSONStoreApplication) Commit() types.ResponseCommit {
func (app *JSONStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) {
return
}

func (app *JSONStoreApplication) InitChain(req types.RequestInitChain) types.ResponseInitChain {
fmt.Println("calling InitChain")
validators := req.GetValidators()
var mintValidators []interface{}
if validators != nil {
for _, element := range validators {
validator := Validator{
ID: bson.NewObjectId(),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a bug. We cannot generate random value for each like that cos it will create different IDs on on every validator node.

Name: "validatorName",
PublicKey: element.GetPubKey(),
Votes: []bson.ObjectId{},
}
mintValidators = append(mintValidators, validator)
}
dbErr := db.C("validators").Insert(mintValidators...)
if dbErr != nil {
panic(dbErr)
}
}
return types.ResponseInitChain{}
}

func (app *JSONStoreApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock {
// TODO below solution needs confirmation through testing on multiple nodes.
// according to documentation "To add a new validator or update an existing one,
// simply include them in the list returned in the EndBlock response.
// To remove one, include it in the list with a power equal to 0."
// That means below solution may not be removing any validators atm!

// TODO I've noticed that the order of elements with the same vote value is not nondeterministic
// which means last active can be swapped with first standby potentially on every query
// that could be considered an issue depending on requirements
project := bson.M{
"$project": bson.M{
"name": 1,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have to use aggregation if we just maintain another property called votes (integer) in the validator documents.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. Could have added "increment" and new field

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added vote count as a part of adding extra collection

"publicKey": 1,
"votes": bson.M{"$size": "$votes"},
},
}
sort := bson.M{
"$sort": bson.M{
"votes": -1,
},
}
limit := bson.M{
"$limit": numActiveValidators,
}

iter := db.C("validators").Pipe([]bson.M{project, sort, limit}).Iter()
defer iter.Close()

var tdValidators []types.Validator

var dbResult bson.M
for iter.Next(&dbResult) {
// fmt.Println(dbResult)
validator := types.Validator{
PubKey: dbResult["publicKey"].([]byte),
Power: 10,
}
tdValidators = append(tdValidators, validator)
}
if iter.Err() != nil {
panic(iter.Err())
}
fmt.Println(tdValidators)

return types.ResponseEndBlock{ValidatorUpdates: tdValidators}
}
19 changes: 16 additions & 3 deletions mint.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package main

import (
"fmt"
"mint/jsonstore"
"os"
"strconv"

"github.com/tendermint/abci/server"
"github.com/tendermint/abci/types"
Expand All @@ -22,14 +24,16 @@ func initJSONStore() error {
// Create the application
var app types.Application

session, err := mgo.Dial("localhost")
fmt.Println("running aginst mongo: ", os.Getenv("MONGO_URL"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Mongo_URL env is not set, what does it use by default?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it uses localhost:27017 but tbh its not obvious from the client library code... When I added it and run it just still worked which surprised me too. (unless there is another bug I missed ;) )

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so turned out i was wrong and it doesn't default to localhost:27017.. must have forgotten i've added it. This way or the other im removing it as well as removing override for the ABCI port. Since you may be merging it there is no point in me forcing code basically added for testing

session, err := mgo.Dial(os.Getenv("MONGO_URL"))
if err != nil {
panic(err)
}
db := session.DB("tendermintdb")

// Clean the DB on each reboot
collections := [5]string{"posts", "comments", "users", "userpostvotes", "usercommentvotes"}
collections := [6]string{"posts", "comments", "users", "userpostvotes", "usercommentvotes", "validators"}
// collections := [6]string{"posts", "comments", "users", "userpostvotes", "usercommentvotes"}

for _, collection := range collections {
db.C(collection).RemoveAll(nil)
Expand All @@ -38,7 +42,16 @@ func initJSONStore() error {
app = jsonstore.NewJSONStoreApplication(db)

// Start the listener
srv, err := server.NewServer("tcp://0.0.0.0:46658", "socket", app)
port := 46658
if os.Args[1] != "" {
port, err = strconv.Atoi(os.Args[1])
if err != nil {
panic(err)
}
}
fmt.Println(os.Args[1:])
fmt.Println(port)
srv, err := server.NewServer(fmt.Sprintf("tcp://0.0.0.0:%d", port), "socket", app)
if err != nil {
return err
}
Expand Down