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

Added paytovote plugin #9

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cmd/paytovote/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/tendermint/abci/server"
"github.com/tendermint/basecoin/app"
"github.com/tendermint/basecoin/plugins/counter"
"github.com/tendermint/basecoin/plugins/paytovote"
cmn "github.com/tendermint/go-common"
eyes "github.com/tendermint/merkleeyes/client"
)
Expand All @@ -25,10 +26,11 @@ func main() {
// Create Basecoin app
app := app.NewBasecoin(eyesCli)

// add plugins
// TODO: add some more, like the cool voting app
// create/add plugins
counter := counter.New("counter")
paytovote := paytovote.New("p2v")
app.RegisterPlugin(counter)
app.RegisterPlugin(paytovote)

// If genesis file was specified, set key-value options
if *genFilePath != "" {
Expand Down
23 changes: 23 additions & 0 deletions plugins/paytovote/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# paytovote plugin

### Description
paytovote is a basic application which demonstrates how to leverage the basecoin library to create an instance of the basecoin system which utilizes a custom paytovote plugin. The premise of this plugin is to allow users to pay a fee to create or vote for user-specified issues. When implementing this plugin, the fee associated with voting may separate from the fee associated for creating a new issue. Additionally, each fee may utilize custom and unique token types (for example "voteTokens" or "newIssueTokens").

### Use
A good way to get a general sense of the technical implementation of a paytovote system is to check out the test file which can be found under basecoin/plugins/paytovote/paytovote\_test.go. The application specific transaction data which is sent through the AppTx.Data term is as follow:
- Valid (bool)
- Transactions will only run if this term is true
- Issue (string)
- Name of the issue which is being voted for or created
- ActionTypeByte (byte)
- TypeByte field which specifies the action to be taken by the paytovote transaction
- Available actions:
- Create a non-existent issue
- submit a vote for an existing issue
- submit a vote against an existing issue
- submit a spoiled vote to an existing issue
- CostToVote (types.Coins)
- The cost charged by the plugin to submit a vote on an existing issue
- CostToCreateIssue (types.Coins)
- The cost charged by the plugin when creating a new issue

183 changes: 183 additions & 0 deletions plugins/paytovote/paytovote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package paytovote

import (
"fmt"

abci "github.com/tendermint/abci/types"
"github.com/tendermint/basecoin/types"
"github.com/tendermint/go-wire"
)

const (
TypeByteCreateIssue byte = 0x00
TypeByteVoteFor byte = 0x01
TypeByteVoteAgainst byte = 0x02
TypeByteVoteSpoiled byte = 0x03
)

type P2VPluginState struct {
TotalCost types.Coins
Issue string
votesFor int
votesAgainst int
votesSpoiled int
}

type P2VTx struct {
Valid bool
Issue string //Issue being voted for
ActionTypeByte byte //How is the vote being cast
Cost2Vote types.Coins //Cost to vote
Cost2CreateIssue types.Coins //Cost to create a new issue
}

//--------------------------------------------------------------------------------

type P2VPlugin struct {
name string
}

func (p2v *P2VPlugin) Name() string {
return p2v.name
}

func (p2v *P2VPlugin) StateKey(issue string) []byte {
return []byte(fmt.Sprintf("P2VPlugin{name=%v,issue=%v}.State", p2v.name, issue))
}

func New(name string) *P2VPlugin {
return &P2VPlugin{
name: name,
}
}

func newState(issue string) P2VPluginState {
return P2VPluginState{
TotalCost: types.Coins{},
Issue: issue,
votesFor: 0,
votesAgainst: 0,
votesSpoiled: 0,
}
}

func (p2v *P2VPlugin) SetOption(store types.KVStore, key string, value string) (log string) {
return ""
}

func (p2v *P2VPlugin) RunTx(store types.KVStore, ctx types.CallContext, txBytes []byte) (res abci.Result) {

// Decode tx
var tx P2VTx
err := wire.ReadBinaryBytes(txBytes, &tx)
if err != nil {
return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error())
}

// Validate tx
if !tx.Valid {
return abci.ErrInternalError.AppendLog("P2VTx.Valid must be true")
}

if len(tx.Issue) == 0 {
return abci.ErrInternalError.AppendLog("P2VTx.Issue must have a length greater than 0")
}

if !tx.Cost2Vote.IsValid() {
return abci.ErrInternalError.AppendLog("P2VTx.Cost2Vote is not sorted or has zero amounts")
}

if !tx.Cost2Vote.IsNonnegative() {
return abci.ErrInternalError.AppendLog("P2VTx.Cost2Vote must be nonnegative")
}

if !tx.Cost2CreateIssue.IsValid() {
return abci.ErrInternalError.AppendLog("P2VTx.Cost2CreateIssue is not sorted or has zero amounts")
}

if !tx.Cost2CreateIssue.IsNonnegative() {
return abci.ErrInternalError.AppendLog("P2VTx.Cost2CreateIssue must be nonnegative")
}

// Load P2VPluginState
var p2vState P2VPluginState
p2vStateBytes := store.Get(p2v.StateKey(tx.Issue))

//Determine if the issue already exists
issueExists := true

if len(p2vStateBytes) > 0 { //is there a record of the issue existing?
err = wire.ReadBinaryBytes(p2vStateBytes, &p2vState)
if err != nil {
return abci.ErrInternalError.AppendLog("Error decoding state: " + err.Error())
}
} else {
issueExists = false
}

returnLeftover := func(cost types.Coins) {
leftoverCoins := ctx.Coins.Minus(cost)
if !leftoverCoins.IsZero() {
// TODO If there are any funds left over, return funds.
// ctx.CallerAccount is synced w/ store, so just modify that and store it.
}
}

switch {
case tx.ActionTypeByte == TypeByteCreateIssue && issueExists:
return abci.ErrInsufficientFunds.AppendLog("Cannot create an already existing issue")
case tx.ActionTypeByte != TypeByteCreateIssue && !issueExists:
return abci.ErrInsufficientFunds.AppendLog("Tx Issue not found")
case tx.ActionTypeByte == TypeByteCreateIssue && !issueExists:
// Did the caller provide enough coins?
if !ctx.Coins.IsGTE(tx.Cost2CreateIssue) {
return abci.ErrInsufficientFunds.AppendLog("Tx Funds insufficient for creating a new issue")
}

// Update P2VPluginState
newP2VState := newState(tx.Issue)
newP2VState.TotalCost = newP2VState.TotalCost.Plus(tx.Cost2Vote)

// Save P2VPluginState
store.Set(p2v.StateKey(tx.Issue), wire.BinaryBytes(newP2VState))

returnLeftover(tx.Cost2CreateIssue)

case tx.ActionTypeByte != TypeByteCreateIssue && issueExists:
// Did the caller provide enough coins?
if !ctx.Coins.IsGTE(tx.Cost2Vote) {
return abci.ErrInsufficientFunds.AppendLog("Tx Funds insufficient for voting")
}

switch tx.ActionTypeByte {
case TypeByteVoteFor:
p2vState.votesFor += 1
case TypeByteVoteAgainst:
p2vState.votesAgainst += 1
case TypeByteVoteSpoiled:
p2vState.votesSpoiled += 1
default:
return abci.ErrInternalError.AppendLog("P2VTx.ActionTypeByte was not recognized")
}

// Update P2VPluginState
p2vState.TotalCost = p2vState.TotalCost.Plus(tx.Cost2Vote)

// Save P2VPluginState
store.Set(p2v.StateKey(tx.Issue), wire.BinaryBytes(p2vState))

returnLeftover(tx.Cost2CreateIssue)
}

return abci.NewResultOK(wire.BinaryBytes(p2vState), "")
}

func (p2v *P2VPlugin) InitChain(store types.KVStore, vals []*abci.Validator) {
}

func (p2v *P2VPlugin) BeginBlock(store types.KVStore, height uint64) {
}

func (p2v *P2VPlugin) EndBlock(store types.KVStore, height uint64) []*abci.Validator {
return nil
}
127 changes: 127 additions & 0 deletions plugins/paytovote/paytovote_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package paytovote

import (
"testing"

"github.com/stretchr/testify/assert"
abci "github.com/tendermint/abci/types"
"github.com/tendermint/basecoin/app"
"github.com/tendermint/basecoin/testutils"
"github.com/tendermint/basecoin/types"
"github.com/tendermint/go-wire"
eyescli "github.com/tendermint/merkleeyes/client"
)

func TestP2VPlugin(t *testing.T) {

// Basecoin initialization
eyesCli := eyescli.NewLocalClient()
chainID := "test_chain_id"
bcApp := app.NewBasecoin(eyesCli)
bcApp.SetOption("base/chainID", chainID)
t.Log(bcApp.Info())

// Add Counter plugin
P2VPluginName := "testP2V"
P2VPlugin := New(P2VPluginName)
bcApp.RegisterPlugin(P2VPlugin)

// Account initialization
test1PrivAcc := testutils.PrivAccountFromSecret("test1")

// Seed Basecoin with account
test1Acc := test1PrivAcc.Account
test1Acc.Balance = types.Coins{{"", 1000}, {"issueToken", 1000}, {"voteToken", 1000}}
bcApp.SetOption("base/account", string(wire.JSONBytes(test1Acc)))

DeliverTx := func(gas int64,
fee types.Coin,
inputCoins types.Coins,
inputSequence int,
issue string,
actionTypeByte byte,
cost2Vote,
cost2CreateIssue types.Coins) abci.Result {

// Construct an AppTx signature
tx := &types.AppTx{
Gas: gas,
Fee: fee,
Name: P2VPluginName,
Input: types.NewTxInput(test1Acc.PubKey, inputCoins, inputSequence),
Data: wire.BinaryBytes(
P2VTx{
Valid: true,
Issue: issue,
ActionTypeByte: actionTypeByte,
Cost2Vote: cost2Vote,
Cost2CreateIssue: cost2CreateIssue,
}),
}

// Sign request
signBytes := tx.SignBytes(chainID)
t.Logf("Sign bytes: %X\n", signBytes)
sig := test1PrivAcc.PrivKey.Sign(signBytes)
tx.Input.Signature = sig
t.Logf("Signed TX bytes: %X\n", wire.BinaryBytes(struct{ types.Tx }{tx}))

// Write request
txBytes := wire.BinaryBytes(struct{ types.Tx }{tx})
return bcApp.DeliverTx(txBytes)
}

//TODO: Generate tests which query the results of an issue
/* queryIssue := func(issue string) abci.Result {
key := P2VPlugin.StateKey(issue)
query := make([]byte, 1+wire.ByteSliceSize(key))
buf := query
buf[0] = 0x01 // Get TypeByte
buf = buf[1:]
wire.PutByteSlice(buf, key)
t.Log(len(query))
return bcApp.Query(query)
}*/
// REF: DeliverCounterTx(gas, fee, inputCoins, inputSequence, issue, action, cost2Vote, cost2CreateIssue)

issue1 := "free internet"
issue2 := "commutate foobar"

// Test a basic issue generation
res := DeliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 1}}, 1,
issue1, TypeByteCreateIssue, types.Coins{{"voteToken", 1}}, types.Coins{{"issueToken", 1}})
assert.True(t, res.IsOK(), res.String())

// Test a basic votes
res = DeliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 1}}, 2,
issue1, TypeByteVoteFor, types.Coins{{"voteToken", 1}}, types.Coins{{"issueToken", 1}})
assert.True(t, res.IsOK(), res.String())

res = DeliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 1}}, 3,
issue1, TypeByteVoteAgainst, types.Coins{{"voteToken", 1}}, types.Coins{{"issueToken", 1}})
assert.True(t, res.IsOK(), res.String())

res = DeliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 1}}, 4,
issue1, TypeByteVoteSpoiled, types.Coins{{"voteToken", 1}}, types.Coins{{"issueToken", 1}})
assert.True(t, res.IsOK(), res.String())

// Test prevented voting on non-existent issue
res = DeliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 1}}, 5,
issue2, TypeByteVoteFor, types.Coins{{"voteToken", 1}}, types.Coins{{"issueToken", 1}})
assert.True(t, res.IsErr(), res.String())

// Test prevented duplicate issue generation
res = DeliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 1}}, 5,
issue1, TypeByteCreateIssue, types.Coins{{"voteToken", 1}}, types.Coins{{"issueToken", 1}})
assert.True(t, res.IsErr(), res.String())

// Test prevented issue generation from insufficient funds
res = DeliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 1}}, 5,
issue2, TypeByteCreateIssue, types.Coins{{"voteToken", 1}}, types.Coins{{"issueToken", 2}})
assert.True(t, res.IsErr(), res.String())

// Test prevented voting from insufficient funds
res = DeliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 1}}, 5,
issue1, TypeByteVoteFor, types.Coins{{"voteToken", 2}}, types.Coins{{"issueToken", 1}})
assert.True(t, res.IsErr(), res.String())
}