github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/pi/proposalstatus.go (about)

     1  // Copyright (c) 2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package pi
     6  
     7  import (
     8  	"encoding/hex"
     9  
    10  	backend "github.com/decred/politeia/politeiad/backendv2"
    11  	"github.com/decred/politeia/politeiad/plugins/pi"
    12  	"github.com/decred/politeia/politeiad/plugins/ticketvote"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  // getPoposalStatus determines the proposal status at runtime, it uses the
    17  // in-memory cache to avoid retrieving the record, it's vote summary or
    18  // it's billing status changes when possible.
    19  func (p *piPlugin) getProposalStatus(token []byte) (pi.PropStatusT, error) {
    20  	var (
    21  		propStatus pi.PropStatusT
    22  		err        error
    23  		tokenStr   = hex.EncodeToString(token)
    24  
    25  		// The following fields are required to determine the proposal status and
    26  		// MUST be populated.
    27  		recordState  backend.StateT
    28  		recordStatus backend.StatusT
    29  		voteStatus   ticketvote.VoteStatusT
    30  
    31  		// The following fields are required to determine the proposal status and
    32  		// will only be populated for certain types of proposals or during certain
    33  		// stages of the proposal lifecycle.
    34  		voteMetadata         *ticketvote.VoteMetadata
    35  		billingStatuses      []pi.BillingStatusChange
    36  		billingStatusesCount int
    37  
    38  		// Declarations to prevent goto errors
    39  		voteSummary *ticketvote.SummaryReply
    40  	)
    41  
    42  	// Check if the proposal status has been cached
    43  	e := p.statuses.get(tokenStr)
    44  	if e != nil {
    45  		propStatus = e.propStatus
    46  		recordState = e.recordState
    47  		recordStatus = e.recordStatus
    48  		voteStatus = e.voteStatus
    49  		voteMetadata = e.voteMetadata
    50  		billingStatusesCount = e.billingStatusesCount
    51  	}
    52  
    53  	// Check if we need to get any additional data
    54  	if statusIsFinal(propStatus) {
    55  		// The status is final and cannot be changed.
    56  		// No need to get any additional data.
    57  		return propStatus, nil
    58  	}
    59  
    60  	// Get the record if required
    61  	if statusRequiresRecord(propStatus) {
    62  		r, err := p.record(backend.RecordRequest{
    63  			Token:     token,
    64  			Filenames: []string{ticketvote.FileNameVoteMetadata},
    65  		})
    66  		if err != nil {
    67  			return "", err
    68  		}
    69  
    70  		// Update the record data fields required to
    71  		// determine the proposal status.
    72  		recordState = r.RecordMetadata.State
    73  		recordStatus = r.RecordMetadata.Status
    74  
    75  		// Pull the vote metadata out of the record files
    76  		voteMetadata, err = voteMetadataDecode(r.Files)
    77  		if err != nil {
    78  			return "", err
    79  		}
    80  
    81  		// If the proposal is unvetted, no other data is
    82  		// required in order to determine the status.
    83  		if recordState == backend.StateUnvetted {
    84  			goto determineStatus
    85  		}
    86  	}
    87  
    88  	// If cached vote status is not final, fetch the latest vote status
    89  	if !voteStatusIsFinal(voteStatus) {
    90  		voteSummary, err = p.voteSummary(token)
    91  		if err != nil {
    92  			return "", err
    93  		}
    94  		voteStatus = voteSummary.Status
    95  	}
    96  
    97  	// Get the billing statuses if required
    98  	if statusRequiresBillingStatuses(voteStatus) {
    99  		// If the maximum allowed number of billing status changes
   100  		// have already been made for this proposal and those results
   101  		// have been cached, then we don't need to retrieve anything
   102  		// else. The proposal status cannot be changed any further.
   103  		if uint32(billingStatusesCount) >= p.billingStatusChangesMax {
   104  			return propStatus, nil
   105  		}
   106  		billingStatuses, err = p.billingStatusChanges(token)
   107  		if err != nil {
   108  			return "", err
   109  		}
   110  	}
   111  
   112  determineStatus:
   113  	// Determine the proposal status
   114  	propStatus, err = proposalStatus(recordState, recordStatus, voteStatus,
   115  		voteMetadata, billingStatuses)
   116  	if err != nil {
   117  		return "", nil
   118  	}
   119  
   120  	// Cache the results
   121  	p.statuses.set(tokenStr, statusEntry{
   122  		propStatus:           propStatus,
   123  		recordState:          recordState,
   124  		recordStatus:         recordStatus,
   125  		voteStatus:           voteStatus,
   126  		voteMetadata:         voteMetadata,
   127  		billingStatusesCount: len(billingStatuses),
   128  	})
   129  
   130  	return propStatus, nil
   131  }
   132  
   133  // statusIsFinal returns whether the proposal status is a final status and
   134  // cannot be changed any further.
   135  func statusIsFinal(s pi.PropStatusT) bool {
   136  	switch s {
   137  	case pi.PropStatusUnvettedAbandoned, pi.PropStatusUnvettedCensored,
   138  		pi.PropStatusAbandoned, pi.PropStatusCensored, pi.PropStatusApproved,
   139  		pi.PropStatusRejected:
   140  		return true
   141  	default:
   142  		return false
   143  	}
   144  }
   145  
   146  // statusRequiresRecord returns whether the proposal status requires the record
   147  // to be retrieved from the backend. This is necessary when the proposal is in
   148  // a part of the proposal lifecycle that still allows changes to the underlying
   149  // record data. For example, an unvetted proposal may still have it's record
   150  // metadata or vote metadata altered, but a proposal with the status of active
   151  // cannot.
   152  func statusRequiresRecord(s pi.PropStatusT) bool {
   153  	if statusIsFinal(s) {
   154  		// The status is final and cannot be changed
   155  		// any further, which means the record data
   156  		// is not required.
   157  		return false
   158  	}
   159  
   160  	switch s {
   161  	case pi.PropStatusVoteStarted, pi.PropStatusActive,
   162  		pi.PropStatusCompleted, pi.PropStatusClosed:
   163  		// The record cannot be changed any further for
   164  		// these statuses.
   165  		return false
   166  
   167  	case pi.PropStatusUnvetted, pi.PropStatusUnderReview,
   168  		pi.PropStatusVoteAuthorized:
   169  		// The record can still change for these statuses.
   170  		return true
   171  
   172  	default:
   173  		// Defaulting to true is the conservative default
   174  		// since it will force the record to be retrieved
   175  		// for unhandled cases.
   176  		return true
   177  	}
   178  }
   179  
   180  // statusRequiresBillingStatuses returns whether the proposal requires the
   181  // billing status changes to be retrieved. This is necessary when the proposal
   182  // is in a stage where it's billing status can still change.
   183  func statusRequiresBillingStatuses(vs ticketvote.VoteStatusT) bool {
   184  	switch vs {
   185  	case ticketvote.VoteStatusUnauthorized,
   186  		ticketvote.VoteStatusAuthorized,
   187  		ticketvote.VoteStatusStarted,
   188  		ticketvote.VoteStatusFinished,
   189  		ticketvote.VoteStatusRejected,
   190  		ticketvote.VoteStatusIneligible:
   191  		// These vote statuses cannot have billing status
   192  		// changes, so there is not need to retrieve them.
   193  		return false
   194  
   195  	case ticketvote.VoteStatusApproved:
   196  		// Approved proposals can have billing status
   197  		// changes. Retrieve them.
   198  		return true
   199  
   200  	default:
   201  		// Force the billing statuses to be retrieved for any
   202  		// unhandled cases.
   203  		return true
   204  	}
   205  }
   206  
   207  // voteStatusIsFinal returns whether the given vote status is final and
   208  // cannot be changed any further.
   209  func voteStatusIsFinal(vs ticketvote.VoteStatusT) bool {
   210  	switch vs {
   211  	case ticketvote.VoteStatusIneligible,
   212  		ticketvote.VoteStatusFinished,
   213  		ticketvote.VoteStatusRejected,
   214  		ticketvote.VoteStatusApproved:
   215  		return true
   216  
   217  	default:
   218  		return false
   219  	}
   220  }
   221  
   222  // proposalStatusApproved returns the proposal status of an approved proposal.
   223  func proposalStatusApproved(voteMD *ticketvote.VoteMetadata, bscs []pi.BillingStatusChange) (pi.PropStatusT, error) {
   224  	// If the proposal in an RFP then we don't need to
   225  	// check the billing status changes. RFP proposals
   226  	// do not bill against the treasury. This does not
   227  	// apply to RFP submission proposals.
   228  	if isRFP(voteMD) {
   229  		return pi.PropStatusApproved, nil
   230  	}
   231  
   232  	// Use the billing status to determine the proposal status.
   233  	bs := proposalBillingStatus(ticketvote.VoteStatusApproved, bscs)
   234  	switch bs {
   235  	case pi.BillingStatusClosed:
   236  		return pi.PropStatusClosed, nil
   237  	case pi.BillingStatusCompleted:
   238  		return pi.PropStatusCompleted, nil
   239  	case pi.BillingStatusActive:
   240  		return pi.PropStatusActive, nil
   241  	}
   242  
   243  	// Shouldn't happen return an error
   244  	return pi.PropStatusInvalid,
   245  		errors.Errorf(
   246  			"couldn't determine proposal status of an approved propsoal: "+
   247  				"billingStatus: %v", bs)
   248  }
   249  
   250  // proposalStatus combines record metadata and plugin metadata in order to
   251  // create a unified map of the various paths a proposal can take throughout
   252  // the proposal process.
   253  func proposalStatus(state backend.StateT, status backend.StatusT, voteStatus ticketvote.VoteStatusT, voteMD *ticketvote.VoteMetadata, bscs []pi.BillingStatusChange) (pi.PropStatusT, error) {
   254  	switch state {
   255  	case backend.StateUnvetted:
   256  		switch status {
   257  		case backend.StatusUnreviewed:
   258  			return pi.PropStatusUnvetted, nil
   259  		case backend.StatusArchived:
   260  			return pi.PropStatusUnvettedAbandoned, nil
   261  		case backend.StatusCensored:
   262  			return pi.PropStatusUnvettedCensored, nil
   263  		}
   264  	case backend.StateVetted:
   265  		switch status {
   266  		case backend.StatusArchived:
   267  			return pi.PropStatusAbandoned, nil
   268  		case backend.StatusCensored:
   269  			return pi.PropStatusCensored, nil
   270  		case backend.StatusPublic:
   271  			switch voteStatus {
   272  			case ticketvote.VoteStatusUnauthorized:
   273  				return pi.PropStatusUnderReview, nil
   274  			case ticketvote.VoteStatusAuthorized:
   275  				return pi.PropStatusVoteAuthorized, nil
   276  			case ticketvote.VoteStatusStarted:
   277  				return pi.PropStatusVoteStarted, nil
   278  			case ticketvote.VoteStatusRejected:
   279  				return pi.PropStatusRejected, nil
   280  			case ticketvote.VoteStatusApproved:
   281  				return proposalStatusApproved(voteMD, bscs)
   282  			}
   283  		}
   284  	}
   285  	// Shouldn't happen return an error
   286  	return pi.PropStatusInvalid,
   287  		errors.Errorf(
   288  			"couldn't determine proposal status: proposal state: %v, "+
   289  				"proposal status %v, vote status: %v", state, status, voteStatus)
   290  }