Enable treasury vote choices. (#319)

This allows both tspend and treasury policies to be set by clients on a per-ticket basis. Preferences can be set when initially registering a ticket with `/payfee`, and can be later updated using `/setvotechoices`.

Any requests which alter treasury/tspend policy will be stored in the database using the existing accountability system.

**Note:** This does not include consistency checking, it will need to be added later when dcrwallet has an RPC to retrieve policies in batches.
This commit is contained in:
Jamie Holdstock 2022-02-04 19:14:49 +00:00 committed by GitHub
parent dcbf8a2c18
commit da1cb8f916
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 375 additions and 41 deletions

View File

@ -1,6 +1,6 @@
ISC License ISC License
Copyright (c) 2020-2021 The Decred developers Copyright (c) 2020-2022 The Decred developers
Permission to use, copy, modify, and distribute this software for any Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers // Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -244,6 +244,25 @@ func blockConnected() {
} }
} }
} }
// Set tspend policy on voting wallets.
for tspend, policy := range ticket.TSpendPolicy {
err = walletClient.SetTSpendPolicy(tspend, policy, ticket.Hash)
if err != nil {
log.Errorf("%s: dcrwallet.SetTSpendPolicy failed (wallet=%s, ticketHash=%s): %v",
funcName, walletClient.String(), ticket.Hash, err)
}
}
// Set treasury policy on voting wallets.
for key, policy := range ticket.TreasuryPolicy {
err = walletClient.SetTreasuryPolicy(key, policy, ticket.Hash)
if err != nil {
log.Errorf("%s: dcrwallet.SetTreasuryPolicy failed (wallet=%s, ticketHash=%s): %v",
funcName, walletClient.String(), ticket.Hash, err)
}
}
log.Infof("%s: Ticket added to voting wallet (wallet=%s, ticketHash=%s)", log.Infof("%s: Ticket added to voting wallet (wallet=%s, ticketHash=%s)",
funcName, walletClient.String(), ticket.Hash) funcName, walletClient.String(), ticket.Hash)
} }
@ -557,6 +576,8 @@ func checkWalletConsistency() {
} }
} }
} }
// TODO - tspend and treasury policy consistency checking.
} }
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers // Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -51,6 +51,8 @@ var (
confirmedK = []byte("Confirmed") confirmedK = []byte("Confirmed")
votingWIFK = []byte("VotingWIF") votingWIFK = []byte("VotingWIF")
voteChoicesK = []byte("VoteChoices") voteChoicesK = []byte("VoteChoices")
tSpendPolicyK = []byte("TSpendPolicy")
treasuryPolicyK = []byte("TreasuryPolicy")
feeTxHexK = []byte("FeeTxHex") feeTxHexK = []byte("FeeTxHex")
feeTxHashK = []byte("FeeTxHash") feeTxHashK = []byte("FeeTxHash")
feeTxStatusK = []byte("FeeTxStatus") feeTxStatusK = []byte("FeeTxStatus")
@ -72,9 +74,11 @@ type Ticket struct {
// VotingWIF is set in /payfee. // VotingWIF is set in /payfee.
VotingWIF string VotingWIF string
// VoteChoices is initially set in /payfee, but can be updated in // VoteChoices, TSpendPolicy and TreasuryPolicy are initially set in
// /setvotechoices. // /payfee, but can be updated in /setvotechoices.
VoteChoices map[string]string VoteChoices map[string]string
TSpendPolicy map[string]string
TreasuryPolicy map[string]string
// FeeTxHex and FeeTxHash will be set when the fee tx has been received. // FeeTxHex and FeeTxHash will be set when the fee tx has been received.
FeeTxHex string FeeTxHex string
@ -176,6 +180,22 @@ func putTicketInBucket(bkt *bolt.Bucket, ticket Ticket) error {
return err return err
} }
jsonTSpend, err := json.Marshal(ticket.TSpendPolicy)
if err != nil {
return err
}
if err = bkt.Put(tSpendPolicyK, jsonTSpend); err != nil {
return err
}
jsonTreasury, err := json.Marshal(ticket.TreasuryPolicy)
if err != nil {
return err
}
if err = bkt.Put(treasuryPolicyK, jsonTreasury); err != nil {
return err
}
jsonVoteChoices, err := json.Marshal(ticket.VoteChoices) jsonVoteChoices, err := json.Marshal(ticket.VoteChoices)
if err != nil { if err != nil {
return err return err
@ -202,10 +222,30 @@ func getTicketFromBkt(bkt *bolt.Bucket) (Ticket, error) {
ticket.Confirmed = bytesToBool(bkt.Get(confirmedK)) ticket.Confirmed = bytesToBool(bkt.Get(confirmedK))
ticket.VoteChoices = make(map[string]string) var err error
err := json.Unmarshal(bkt.Get(voteChoicesK), &ticket.VoteChoices)
voteChoicesB := bkt.Get(voteChoicesK)
if voteChoicesB != nil {
err = json.Unmarshal(voteChoicesB, &ticket.VoteChoices)
if err != nil { if err != nil {
return ticket, err return ticket, fmt.Errorf("unmarshal VoteChoices err: %w", err)
}
}
tSpendPolicyB := bkt.Get(tSpendPolicyK)
if tSpendPolicyB != nil {
err = json.Unmarshal(tSpendPolicyB, &ticket.TSpendPolicy)
if err != nil {
return ticket, fmt.Errorf("unmarshal TSpendPolicy err: %w", err)
}
}
treasuryPolicyB := bkt.Get(treasuryPolicyK)
if treasuryPolicyB != nil {
err = json.Unmarshal(treasuryPolicyB, &ticket.TreasuryPolicy)
if err != nil {
return ticket, fmt.Errorf("unmarshal TreasuryPolicy err: %w", err)
}
} }
return ticket, nil return ticket, nil

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers // Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -22,6 +22,8 @@ func exampleTicket() Ticket {
FeeExpiration: 4, FeeExpiration: 4,
Confirmed: false, Confirmed: false,
VoteChoices: map[string]string{"AgendaID": "yes"}, VoteChoices: map[string]string{"AgendaID": "yes"},
TSpendPolicy: map[string]string{randString(64, hexCharset): "no"},
TreasuryPolicy: map[string]string{randString(66, hexCharset): "abstain"},
VotingWIF: randString(53, addrCharset), VotingWIF: randString(53, addrCharset),
FeeTxHex: randString(504, hexCharset), FeeTxHex: randString(504, hexCharset),
FeeTxHash: randString(64, hexCharset), FeeTxHash: randString(64, hexCharset),

View File

@ -1,4 +1,4 @@
// Copyright (c) 2021 The Decred developers // Copyright (c) 2021-2022 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -52,7 +52,22 @@ func ticketBucketUpgrade(db *bolt.DB) error {
return fmt.Errorf("could not create new ticket bucket: %w", err) return fmt.Errorf("could not create new ticket bucket: %w", err)
} }
err = putTicketInBucket(newBkt, Ticket(ticket)) err = putTicketInBucket(newBkt, Ticket{
Hash: ticket.Hash,
PurchaseHeight: ticket.PurchaseHeight,
CommitmentAddress: ticket.CommitmentAddress,
FeeAddressIndex: ticket.FeeAddressIndex,
FeeAddress: ticket.FeeAddress,
FeeAmount: ticket.FeeAmount,
FeeExpiration: ticket.FeeExpiration,
Confirmed: ticket.Confirmed,
VotingWIF: ticket.VotingWIF,
VoteChoices: ticket.VoteChoices,
FeeTxHex: ticket.FeeTxHex,
FeeTxHash: ticket.FeeTxHash,
FeeTxStatus: ticket.FeeTxStatus,
Outcome: ticket.Outcome,
})
if err != nil { if err != nil {
return fmt.Errorf("could not put new ticket in bucket: %w", err) return fmt.Errorf("could not put new ticket in bucket: %w", err)
} }

View File

@ -161,7 +161,9 @@ for the specified ticket.
"tickethash":"484a68f7148e55d05f0b64a29fe7b148572cb5272d1ce2438cf15466d347f4f4", "tickethash":"484a68f7148e55d05f0b64a29fe7b148572cb5272d1ce2438cf15466d347f4f4",
"feetx":"010000000125...737b266ffb9a93", "feetx":"010000000125...737b266ffb9a93",
"votingkey":"PtWUJWhSXsM9ztPkdtH8REe91z7uoidX8dsMChJUZ2spagm7YvrNm", "votingkey":"PtWUJWhSXsM9ztPkdtH8REe91z7uoidX8dsMChJUZ2spagm7YvrNm",
"votechoices":{"headercommitments":"yes"} "votechoices":{"headercommitments":"yes"},
"tspendpolicy":{"<tspend tx hash>":"yes"},
"treasurypolicy":{"<treasury spending key>":"no"}
} }
``` ```
@ -212,6 +214,8 @@ its `feetxstatus` is `confirmed`.
"feetxhash":"e1c02b04b5bbdae66cf8e3c88366c4918d458a2d27a26144df37f54a2bc956ac", "feetxhash":"e1c02b04b5bbdae66cf8e3c88366c4918d458a2d27a26144df37f54a2bc956ac",
"altsignaddress":"Tsfkn6k9AoYgVZRV6ZzcgmuVSgCdJQt9JY2", "altsignaddress":"Tsfkn6k9AoYgVZRV6ZzcgmuVSgCdJQt9JY2",
"votechoices":{"headercommitments":"no"}, "votechoices":{"headercommitments":"no"},
"tspendpolicy":{"<tspend tx hash>":"yes"},
"treasurypolicy":{"<treasury spending key>":"no"},
"request": {"<Copy of request body>"} "request": {"<Copy of request body>"}
} }
``` ```
@ -220,6 +224,8 @@ its `feetxstatus` is `confirmed`.
Clients can update the voting preferences of their ticket at any time after Clients can update the voting preferences of their ticket at any time after
after calling `/payfee`. after calling `/payfee`.
This call can be used to update consensus, treasury spend key, and tspend voting
preferences.
- `POST /api/v3/setvotechoices` - `POST /api/v3/setvotechoices`
@ -229,7 +235,9 @@ after calling `/payfee`.
{ {
"timestamp":1590509066, "timestamp":1590509066,
"tickethash":"484a68f7148e55d05f0b64a29fe7b148572cb5272d1ce2438cf15466d347f4f4", "tickethash":"484a68f7148e55d05f0b64a29fe7b148572cb5272d1ce2438cf15466d347f4f4",
"votechoices":{"headercommitments":"no"} "votechoices":{"headercommitments":"no"},
"tspendpolicy":{"<tspend tx hash>":"yes"},
"treasurypolicy":{"<treasury spending key>":"no"}
} }
``` ```

3
go.mod
View File

@ -3,11 +3,12 @@ module github.com/decred/vspd
go 1.16 go 1.16
require ( require (
decred.org/dcrwallet/v2 v2.0.0-rc3 decred.org/dcrwallet/v2 v2.0.0
github.com/decred/dcrd/blockchain/stake/v4 v4.0.0 github.com/decred/dcrd/blockchain/stake/v4 v4.0.0
github.com/decred/dcrd/blockchain/v4 v4.0.0 github.com/decred/dcrd/blockchain/v4 v4.0.0
github.com/decred/dcrd/chaincfg/chainhash v1.0.3 github.com/decred/dcrd/chaincfg/chainhash v1.0.3
github.com/decred/dcrd/chaincfg/v3 v3.1.1 github.com/decred/dcrd/chaincfg/v3 v3.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/decred/dcrd/dcrutil/v4 v4.0.0 github.com/decred/dcrd/dcrutil/v4 v4.0.0
github.com/decred/dcrd/hdkeychain/v3 v3.1.0 github.com/decred/dcrd/hdkeychain/v3 v3.1.0
github.com/decred/dcrd/rpc/jsonrpc/types/v3 v3.0.0 github.com/decred/dcrd/rpc/jsonrpc/types/v3 v3.0.0

6
go.sum
View File

@ -1,7 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
decred.org/cspp/v2 v2.0.0-20220117153402-4f26c92d52a3/go.mod h1:USyJS44Kqxz2wT/VaNsf9iTAONegO/qKXRdLg1nvrWI= decred.org/cspp/v2 v2.0.0/go.mod h1:0shJWKTWY3LxZEWGxtbER1Y45+HVjC0WZtj4bctSzCI=
decred.org/dcrwallet/v2 v2.0.0-rc3 h1:xxmZndkTheyfORD16nf8fUsevR3dBHdI8hhdAUFumZE= decred.org/dcrwallet/v2 v2.0.0 h1:nOGChwR5RM4QLmbm/Krit6/eQryv/RvcpUxxmUThfKI=
decred.org/dcrwallet/v2 v2.0.0-rc3/go.mod h1:NJnnnOrPISrCt5oxrkXgTu0feCnxcLUsbqsvdXtFVBI= decred.org/dcrwallet/v2 v2.0.0/go.mod h1:lZXgx5OcLDaWyNWFkBekqER1gdqiVwua1w68SFC1/Nk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers // Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -219,3 +219,15 @@ func (c *WalletRPC) TicketInfo(startHeight int64) (map[string]*wallettypes.Ticke
func (c *WalletRPC) RescanFrom(fromHeight int64) error { func (c *WalletRPC) RescanFrom(fromHeight int64) error {
return c.Call(c.ctx, "rescanwallet", nil, fromHeight) return c.Call(c.ctx, "rescanwallet", nil, fromHeight)
} }
// SetTreasuryPolicy sets the specified tickets voting policy for all tspends
// published by the given treasury key.
func (c *WalletRPC) SetTreasuryPolicy(key, policy, ticket string) error {
return c.Call(c.ctx, "settreasurypolicy", nil, key, policy, ticket)
}
// SetTSpendPolicy sets the specified tickets voting policy for a single tspend
// identified by its hash.
func (c *WalletRPC) SetTSpendPolicy(tSpend, policy, ticket string) error {
return c.Call(c.ctx, "settspendpolicy", nil, tSpend, policy, ticket)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers // Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -10,7 +10,9 @@ import (
"fmt" "fmt"
"github.com/decred/dcrd/blockchain/stake/v4" "github.com/decred/dcrd/blockchain/stake/v4"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/wire" "github.com/decred/dcrd/wire"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -52,6 +54,57 @@ agendaLoop:
return nil return nil
} }
func validTreasuryPolicy(policy map[string]string) error {
for key, choice := range policy {
pikey, err := hex.DecodeString(key)
if err != nil {
return fmt.Errorf("error decoding treasury key %q: %w", key, err)
}
if len(pikey) != secp256k1.PubKeyBytesLenCompressed {
return fmt.Errorf("treasury key %q is not 33 bytes", key)
}
err = validPolicyOption(choice)
if err != nil {
return err
}
}
return nil
}
func validTSpendPolicy(policy map[string]string) error {
for hash, choice := range policy {
if len(hash) != chainhash.MaxHashStringSize {
return fmt.Errorf("wrong tspend hash length, expected %d got %d",
chainhash.MaxHashStringSize, len(hash))
}
_, err := chainhash.NewHashFromStr(hash)
if err != nil {
return fmt.Errorf("error decoding tspend hash %q: %w", hash, err)
}
err = validPolicyOption(choice)
if err != nil {
return err
}
}
return nil
}
// validPolicyOption checks that policy is one of the valid values accepted by
// dcrwallet RPCs. Invalid values return an error.
func validPolicyOption(policy string) error {
switch policy {
case "yes", "no", "abstain", "invalid", "":
return nil
default:
return fmt.Errorf("%q is not a valid policy option", policy)
}
}
func validateSignature(reqBytes []byte, commitmentAddress string, c *gin.Context) error { func validateSignature(reqBytes []byte, commitmentAddress string, c *gin.Context) error {
// Ensure a signature is provided. // Ensure a signature is provided.
signature := c.GetHeader("VSP-Client-Signature") signature := c.GetHeader("VSP-Client-Signature")

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers // Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -28,7 +28,7 @@ func TestIsValidVoteChoices(t *testing.T) {
{map[string]string{"lnsupport": "yes"}, true}, {map[string]string{"lnsupport": "yes"}, true},
{map[string]string{"sdiffalgorithm": "no", "lnsupport": "yes"}, true}, {map[string]string{"sdiffalgorithm": "no", "lnsupport": "yes"}, true},
// Invalid agenda. // Invalid agenda, valid vote choice.
{map[string]string{"": "yes"}, false}, {map[string]string{"": "yes"}, false},
{map[string]string{"Fake agenda": "yes"}, false}, {map[string]string{"Fake agenda": "yes"}, false},
@ -48,7 +48,98 @@ func TestIsValidVoteChoices(t *testing.T) {
for _, test := range tests { for _, test := range tests {
err := validConsensusVoteChoices(params, voteVersion, test.voteChoices) err := validConsensusVoteChoices(params, voteVersion, test.voteChoices)
if (err == nil) != test.valid { if (err == nil) != test.valid {
t.Fatalf("isValidVoteChoices failed for votechoices '%v'.", test.voteChoices) t.Fatalf("isValidVoteChoices failed for votechoices '%v': %v",
test.voteChoices, err)
}
}
}
func TestIsValidTSpendPolicy(t *testing.T) {
// A valid tspend hash is 32 bytes (64 characters).
validHash := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
anotherValidHash := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
var tests = []struct {
tspendPolicy map[string]string
valid bool
}{
// Empty vote choices are allowed.
{map[string]string{}, true},
// Valid tspend hash, valid vote choice.
{map[string]string{validHash: "yes"}, true},
{map[string]string{validHash: ""}, true},
{map[string]string{validHash: "no", anotherValidHash: "yes"}, true},
// Invalid tspend hash.
{map[string]string{"": "yes"}, false},
{map[string]string{"a": "yes"}, false},
{map[string]string{"non hex characters": "yes"}, false},
{map[string]string{validHash + "a": "yes"}, false},
// Valid tspend hash, invalid vote choice.
{map[string]string{validHash: "1234"}, false},
// // One valid choice, one invalid choice.
{map[string]string{validHash: "no", anotherValidHash: "1234"}, false},
{map[string]string{validHash: "1234", anotherValidHash: "no"}, false},
// One valid tspend hash, one invalid tspend hash.
{map[string]string{"fake": "abstain", anotherValidHash: "no"}, false},
{map[string]string{validHash: "abstain", "": "no"}, false},
}
for _, test := range tests {
err := validTSpendPolicy(test.tspendPolicy)
if (err == nil) != test.valid {
t.Fatalf("validTSpendPolicy failed for policy '%v': %v",
test.tspendPolicy, err)
}
}
}
func TestIsValidTreasuryPolicy(t *testing.T) {
// A valid treasury key is 33 bytes (66 characters).
validKey := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
anotherValidKey := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
var tests = []struct {
treasuryPolicy map[string]string
valid bool
}{
// Empty vote choices are allowed.
{map[string]string{}, true},
// Valid treasury key, valid vote choice.
{map[string]string{validKey: "yes"}, true},
{map[string]string{validKey: ""}, true},
{map[string]string{validKey: "no", anotherValidKey: "yes"}, true},
// Invalid treasury key.
{map[string]string{"": "yes"}, false},
{map[string]string{"a": "yes"}, false},
{map[string]string{"non hex characters": "yes"}, false},
{map[string]string{validKey + "a": "yes"}, false},
// Valid treasury key, invalid vote choice.
{map[string]string{validKey: "1234"}, false},
// // One valid choice, one invalid choice.
{map[string]string{validKey: "no", anotherValidKey: "1234"}, false},
{map[string]string{validKey: "1234", anotherValidKey: "no"}, false},
// One valid treasury key, one invalid treasury key.
{map[string]string{"fake": "abstain", anotherValidKey: "no"}, false},
{map[string]string{validKey: "abstain", "": "no"}, false},
}
for _, test := range tests {
err := validTreasuryPolicy(test.treasuryPolicy)
if (err == nil) != test.valid {
t.Fatalf("validTreasuryPolicy failed for policy '%v': %v",
test.treasuryPolicy, err)
} }
} }
} }

View File

@ -103,8 +103,9 @@ func payFee(c *gin.Context) {
return return
} }
// Validate VoteChoices. Just log a warning if vote choices are not valid // Validate voting prefences. Just log a warning if anything is invalid -
// for the current vote version - the ticket should still be registered. // the ticket should still be registered.
validVoteChoices := true validVoteChoices := true
err = validConsensusVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), request.VoteChoices) err = validConsensusVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), request.VoteChoices)
if err != nil { if err != nil {
@ -113,6 +114,22 @@ func payFee(c *gin.Context) {
funcName, c.ClientIP(), ticket.Hash, err) funcName, c.ClientIP(), ticket.Hash, err)
} }
validTreasury := true
err = validTreasuryPolicy(request.TreasuryPolicy)
if err != nil {
validTreasury = false
log.Warnf("%s: Invalid treasury policy (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
}
validTSpend := true
err = validTSpendPolicy(request.TSpendPolicy)
if err != nil {
validTSpend = false
log.Warnf("%s: Invalid tspend policy (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
}
// Validate FeeTx. // Validate FeeTx.
feeTx, err := decodeTransaction(request.FeeTx) feeTx, err := decodeTransaction(request.FeeTx)
if err != nil { if err != nil {
@ -217,6 +234,14 @@ func payFee(c *gin.Context) {
ticket.VoteChoices = request.VoteChoices ticket.VoteChoices = request.VoteChoices
} }
if validTSpend {
ticket.TSpendPolicy = request.TSpendPolicy
}
if validTreasury {
ticket.TreasuryPolicy = request.TreasuryPolicy
}
err = db.UpdateTicket(ticket) err = db.UpdateTicket(ticket)
if err != nil { if err != nil {
log.Errorf("%s: db.UpdateTicket error, failed to set fee tx (ticketHash=%s): %v", log.Errorf("%s: db.UpdateTicket error, failed to set fee tx (ticketHash=%s): %v",

View File

@ -92,6 +92,8 @@ func setVoteChoices(c *gin.Context) {
} }
} }
// Validate vote choices (consensus, tspend policy and treasury policy).
err = validConsensusVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), request.VoteChoices) err = validConsensusVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), request.VoteChoices)
if err != nil { if err != nil {
log.Warnf("%s: Invalid consensus vote choices (clientIP=%s, ticketHash=%s): %v", log.Warnf("%s: Invalid consensus vote choices (clientIP=%s, ticketHash=%s): %v",
@ -100,12 +102,35 @@ func setVoteChoices(c *gin.Context) {
return return
} }
// Update VoteChoices in the database before updating the wallets. DB is the err = validTreasuryPolicy(request.TreasuryPolicy)
// source of truth, and also is less likely to error. if err != nil {
log.Warnf("%s: Invalid treasury policy (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c)
}
err = validTSpendPolicy(request.TSpendPolicy)
if err != nil {
log.Warnf("%s: Invalid tspend policy (clientIP=%s, ticketHash=%s): %v",
funcName, c.ClientIP(), ticket.Hash, err)
sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c)
}
// Update voting preferences in the database before updating the wallets. DB
// is the source of truth, and also is less likely to error.
for newAgenda, newChoice := range request.VoteChoices { for newAgenda, newChoice := range request.VoteChoices {
ticket.VoteChoices[newAgenda] = newChoice ticket.VoteChoices[newAgenda] = newChoice
} }
for newTSpend, newChoice := range request.TSpendPolicy {
ticket.TSpendPolicy[newTSpend] = newChoice
}
for newTreasuryKey, newChoice := range request.TreasuryPolicy {
ticket.TreasuryPolicy[newTreasuryKey] = newChoice
}
err = db.UpdateTicket(ticket) err = db.UpdateTicket(ticket)
if err != nil { if err != nil {
log.Errorf("%s: db.UpdateTicket error, failed to set vote choices (ticketHash=%s): %v", log.Errorf("%s: db.UpdateTicket error, failed to set vote choices (ticketHash=%s): %v",
@ -131,6 +156,23 @@ func setVoteChoices(c *gin.Context) {
} }
} }
// Update tspend policy.
for tspend, policy := range ticket.TSpendPolicy {
err = walletClient.SetTSpendPolicy(tspend, policy, ticket.Hash)
if err != nil {
log.Errorf("%s: dcrwallet.SetTSpendPolicy failed (wallet=%s, ticketHash=%s): %v",
funcName, walletClient.String(), ticket.Hash, err)
}
}
// Update treasury policy.
for key, policy := range ticket.TreasuryPolicy {
err = walletClient.SetTreasuryPolicy(key, policy, ticket.Hash)
if err != nil {
log.Errorf("%s: dcrwallet.SetTreasuryPolicy failed (wallet=%s, ticketHash=%s): %v",
funcName, walletClient.String(), ticket.Hash, err)
}
}
} }
} }

View File

@ -15,7 +15,7 @@
<div class="col-md-4 col-12 footer__credit d-flex justify-content-center align-items-center"> <div class="col-md-4 col-12 footer__credit d-flex justify-content-center align-items-center">
<p class="py-4 m-0"> <p class="py-4 m-0">
Decred Developers | 2020-2021 Decred Developers | 2020-2022
<br /> <br />
The source code is available on <a href="https://github.com/decred/vspd" target="_blank" rel="noopener noreferrer">GitHub</a> The source code is available on <a href="https://github.com/decred/vspd" target="_blank" rel="noopener noreferrer">GitHub</a>
</p> </p>

View File

@ -94,6 +94,22 @@
{{ end }} {{ end }}
</td> </td>
</tr> </tr>
<tr>
<th>TSpend Policy</th>
<td>
{{ range $key, $value := .Ticket.TSpendPolicy }}
{{ $key }}: {{ $value }} <br />
{{ end }}
</td>
</tr>
<tr>
<th>Treasury Policy</th>
<td>
{{ range $key, $value := .Ticket.TreasuryPolicy }}
{{ $key }}: {{ $value }} <br />
{{ end }}
</td>
</tr>
<tr> <tr>
<th> <th>
Vote Choice Changes<br /> Vote Choice Changes<br />

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers // Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -55,5 +55,7 @@ func ticketStatus(c *gin.Context) {
FeeTxHash: ticket.FeeTxHash, FeeTxHash: ticket.FeeTxHash,
AltSignAddress: altSignAddr, AltSignAddress: altSignAddr,
VoteChoices: ticket.VoteChoices, VoteChoices: ticket.VoteChoices,
TreasuryPolicy: ticket.TreasuryPolicy,
TSpendPolicy: ticket.TSpendPolicy,
}, c) }, c)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers // Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -41,6 +41,8 @@ type payFeeRequest struct {
FeeTx string `json:"feetx" binding:"required"` FeeTx string `json:"feetx" binding:"required"`
VotingKey string `json:"votingkey" binding:"required"` VotingKey string `json:"votingkey" binding:"required"`
VoteChoices map[string]string `json:"votechoices" binding:"required"` VoteChoices map[string]string `json:"votechoices" binding:"required"`
TSpendPolicy map[string]string `json:"tspendpolicy" binding:"max=3"`
TreasuryPolicy map[string]string `json:"treasurypolicy" binding:"max=3"`
} }
type payFeeResponse struct { type payFeeResponse struct {
@ -52,6 +54,8 @@ type setVoteChoicesRequest struct {
Timestamp int64 `json:"timestamp" binding:"required"` Timestamp int64 `json:"timestamp" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"` TicketHash string `json:"tickethash" binding:"required"`
VoteChoices map[string]string `json:"votechoices" binding:"required"` VoteChoices map[string]string `json:"votechoices" binding:"required"`
TSpendPolicy map[string]string `json:"tspendpolicy" binding:"max=3"`
TreasuryPolicy map[string]string `json:"treasurypolicy" binding:"max=3"`
} }
type setVoteChoicesResponse struct { type setVoteChoicesResponse struct {
@ -70,6 +74,8 @@ type ticketStatusResponse struct {
FeeTxHash string `json:"feetxhash"` FeeTxHash string `json:"feetxhash"`
AltSignAddress string `json:"altsignaddress"` AltSignAddress string `json:"altsignaddress"`
VoteChoices map[string]string `json:"votechoices"` VoteChoices map[string]string `json:"votechoices"`
TSpendPolicy map[string]string `json:"tspendpolicy"`
TreasuryPolicy map[string]string `json:"treasurypolicy"`
Request []byte `json:"request"` Request []byte `json:"request"`
} }