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:
parent
dcbf8a2c18
commit
da1cb8f916
2
LICENSE
2
LICENSE
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
12
docs/api.md
12
docs/api.md
@ -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
3
go.mod
@ -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
6
go.sum
@ -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=
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user