Skip to content

Commit

Permalink
pi: Add additional proposal metadata.
Browse files Browse the repository at this point in the history
This diff adds the following fields as proposal metadata:
- USD funding limit
- Start date
- Estimated end date
- Domain (development, marketing, documentation, etc)
 
The politeiad pi plugin has been updated with validation and sane
defaults for all newly added metadata fields. The defaults can be
overwritten using plugin setting config options. All newly added plugin
settings are returned in the pi plugin `Policy` command.

These metadata additions will enable CMS to pull in proposal funding
data that can be used to verify proposal billing.
  • Loading branch information
amass01 authored Jul 30, 2021
1 parent 503b1e5 commit 8c95e64
Show file tree
Hide file tree
Showing 12 changed files with 1,036 additions and 90 deletions.
136 changes: 136 additions & 0 deletions politeiad/backendv2/tstorebe/plugins/pi/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/json"
"fmt"
"strings"
"time"

backend "github.com/decred/politeia/politeiad/backendv2"
"github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins"
Expand Down Expand Up @@ -140,6 +141,43 @@ func (p *piPlugin) proposalNameIsValid(name string) bool {
return p.proposalNameRegexp.MatchString(name)
}

// proposalStartDateIsValid returns whether the provided start date is valid.
//
// A valid start date of a proposal must be after the minimum start date
// set by the proposalStartDateMin plugin setting.
func (p *piPlugin) proposalStartDateIsValid(start int64) bool {
return start > time.Now().Unix()+p.proposalStartDateMin
}

// proposalEndDateIsValid returns whether the provided end date is valid.
//
// A valid end date must be after the start date and before the end of the
// time interval set by the proposalEndDateMax plugin setting.
func (p *piPlugin) proposalEndDateIsValid(start int64, end int64) bool {
return end > start &&
time.Now().Unix()+p.proposalEndDateMax > end
}

// proposalAmountIsValid returns whether the provided amount is in the range
// defined by the proposalAmountMin & proposalAmountMax plugin settings.
func (p *piPlugin) proposalAmountIsValid(amount uint64) bool {
return p.proposalAmountMin <= amount &&
p.proposalAmountMax >= amount
}

// proposalDomainIsValid returns whether the provided domain is
// is a valid proposal domain.
func (p *piPlugin) proposalDomainIsValid(domain string) bool {
_, found := p.proposalDomains[domain]
return found
}

// isRFP returns true if the given vote metadata contains the metadata for
// an RFP.
func isRFP(vm *ticketvote.VoteMetadata) bool {
return vm != nil && vm.LinkBy != 0
}

// proposalFilesVerify verifies the files adhere to all pi plugin setting
// requirements. If this hook is being executed then the files have already
// passed politeiad validation so we can assume that the file has a unique
Expand Down Expand Up @@ -249,6 +287,35 @@ func (p *piPlugin) proposalFilesVerify(files []backend.File) error {
}
}

// Validate vote & proposal metadata requirements
vm, err := voteMetadataDecode(files)
if err != nil {
return err
}
// In case of an RFP ensure irrelevant proposal metadata are not provided.
if isRFP(vm) {
switch {
case pm.Amount != 0:
return backend.PluginError{
PluginID: pi.PluginID,
ErrorCode: uint32(pi.ErrorCodeProposalAmountInvalid),
ErrorContext: "RFP metadata should not include an amount",
}
case pm.StartDate != 0:
return backend.PluginError{
PluginID: pi.PluginID,
ErrorCode: uint32(pi.ErrorCodeProposalStartDateInvalid),
ErrorContext: "RFP metadata should not include a start date",
}
case pm.EndDate != 0:
return backend.PluginError{
PluginID: pi.PluginID,
ErrorCode: uint32(pi.ErrorCodeProposalEndDateInvalid),
ErrorContext: "RFP metadata should not include an end date",
}
}
}

// Verify proposal name
if !p.proposalNameIsValid(pm.Name) {
return backend.PluginError{
Expand All @@ -258,6 +325,52 @@ func (p *piPlugin) proposalFilesVerify(files []backend.File) error {
}
}

// Validate proposal domain.
if !p.proposalDomainIsValid(pm.Domain) {
return backend.PluginError{
PluginID: pi.PluginID,
ErrorCode: uint32(pi.ErrorCodeProposalDomainInvalid),
ErrorContext: fmt.Sprintf("got %v domain, "+
"supported domains are: %v", pm.Domain, p.proposalDomains),
}
}

// If not RFP validate rest of proposal metadata fields
if !isRFP(vm) {
// Validate proposal start date.
if !p.proposalStartDateIsValid(pm.StartDate) {
return backend.PluginError{
PluginID: pi.PluginID,
ErrorCode: uint32(pi.ErrorCodeProposalStartDateInvalid),
ErrorContext: fmt.Sprintf("got %v start date, min is %v",
pm.StartDate, time.Now().Unix()),
}
}

// Validate proposal end date.
if !p.proposalEndDateIsValid(pm.StartDate, pm.EndDate) {
return backend.PluginError{
PluginID: pi.PluginID,
ErrorCode: uint32(pi.ErrorCodeProposalEndDateInvalid),
ErrorContext: fmt.Sprintf("got %v end date, min is start date %v, "+
"max is %v",
pm.EndDate,
pm.StartDate,
time.Now().Unix()+pi.SettingProposalEndDateMax),
}
}

// Validate proposal amount.
if !p.proposalAmountIsValid(pm.Amount) {
return backend.PluginError{
PluginID: pi.PluginID,
ErrorCode: uint32(pi.ErrorCodeProposalAmountInvalid),
ErrorContext: fmt.Sprintf("got %v amount, min is %v, "+
"max is %v", pm.Amount, p.proposalAmountMin, p.proposalAmountMax),
}
}
}

return nil
}

Expand Down Expand Up @@ -329,3 +442,26 @@ func proposalMetadataDecode(files []backend.File) (*pi.ProposalMetadata, error)
}
return propMD, nil
}

// voteMetadataDecode decodes and returns the VoteMetadata from the
// provided backend files. If a VoteMetadata is not found, nil is returned.
func voteMetadataDecode(files []backend.File) (*ticketvote.VoteMetadata, error) {
var voteMD *ticketvote.VoteMetadata
for _, v := range files {
if v.Name != ticketvote.FileNameVoteMetadata {
continue
}
b, err := base64.StdEncoding.DecodeString(v.Payload)
if err != nil {
return nil, err
}
var m ticketvote.VoteMetadata
err = json.Unmarshal(b, &m)
if err != nil {
return nil, err
}
voteMD = &m
break
}
return voteMD, nil
}
Loading

0 comments on commit 8c95e64

Please sign in to comment.