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

     1  // Copyright (c) 2020-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 ticketvote
     6  
     7  import (
     8  	"encoding/base64"
     9  	"encoding/json"
    10  	"fmt"
    11  	"time"
    12  
    13  	backend "github.com/decred/politeia/politeiad/backendv2"
    14  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins"
    15  	"github.com/decred/politeia/politeiad/plugins/ticketvote"
    16  )
    17  
    18  // hookNewRecordPre adds plugin specific validation onto the tstore backend
    19  // RecordNew method.
    20  func (p *ticketVotePlugin) hookNewRecordPre(payload string) error {
    21  	var nr plugins.HookNewRecordPre
    22  	err := json.Unmarshal([]byte(payload), &nr)
    23  	if err != nil {
    24  		return err
    25  	}
    26  
    27  	// Verify vote metadata
    28  	return p.voteMetadataVerify(nr.Files)
    29  }
    30  
    31  // hookEditRecordPre adds plugin specific validation onto the tstore backend
    32  // RecordEdit method.
    33  func (p *ticketVotePlugin) hookEditRecordPre(payload string) error {
    34  	var er plugins.HookEditRecord
    35  	err := json.Unmarshal([]byte(payload), &er)
    36  	if err != nil {
    37  		return err
    38  	}
    39  
    40  	// Verify vote metadata
    41  	return p.voteMetadataVerifyOnEdits(er.Record, er.Files)
    42  }
    43  
    44  // hookSetStatusRecordPre adds plugin specific validation onto the tstore
    45  // backend RecordSetStatus method.
    46  func (p *ticketVotePlugin) hookSetRecordStatusPre(payload string) error {
    47  	var srs plugins.HookSetRecordStatus
    48  	err := json.Unmarshal([]byte(payload), &srs)
    49  	if err != nil {
    50  		return err
    51  	}
    52  
    53  	// Verify vote metadata
    54  	return p.voteMetadataVerifyOnStatusChange(srs.RecordMetadata.Status,
    55  		srs.Record.Files)
    56  }
    57  
    58  // hookNewRecordPre caches plugin data from the tstore backend RecordSetStatus
    59  // method.
    60  func (p *ticketVotePlugin) hookSetRecordStatusPost(payload string) error {
    61  	var srs plugins.HookSetRecordStatus
    62  	err := json.Unmarshal([]byte(payload), &srs)
    63  	if err != nil {
    64  		return err
    65  	}
    66  
    67  	// ticketvote plugin commands can only be run on
    68  	// vetted records. We can skip all hooks when the
    69  	// record is not vetted.
    70  	recordMD := srs.RecordMetadata
    71  	if recordMD.State == backend.StateUnvetted {
    72  		return nil
    73  	}
    74  
    75  	// Update the cached inventory
    76  	switch recordMD.Status {
    77  	case backend.StatusPublic:
    78  		// Add a new entry to the inventory for this record
    79  		p.inv.AddEntry(recordMD.Token, ticketvote.VoteStatusUnauthorized,
    80  			recordMD.Timestamp)
    81  
    82  	case backend.StatusCensored, backend.StatusArchived:
    83  		// These statuses are not allowed to be voted on. Update the inventory
    84  		// to reflect that this record is ineligible for a vote.
    85  		p.inv.UpdateEntryPreVote(recordMD.Token, ticketvote.VoteStatusIneligible,
    86  			recordMD.Timestamp)
    87  	}
    88  
    89  	// Update the cached vote metadata
    90  	return p.voteMetadataCacheOnStatusChange(recordMD.Token,
    91  		recordMD.State, recordMD.Status, srs.Record.Files)
    92  }
    93  
    94  // linkByVerify verifies that the provided link by timestamp meets all
    95  // ticketvote plugin requirements. See the ticketvote VoteMetadata structure
    96  // for more details on the link by timestamp.
    97  func (p *ticketVotePlugin) linkByVerify(linkBy int64) error {
    98  	if linkBy == 0 {
    99  		// LinkBy as not been set
   100  		return nil
   101  	}
   102  
   103  	// Min and max link by periods are a ticketvote plugin setting
   104  	min := time.Now().Unix() + p.linkByPeriodMin
   105  	max := time.Now().Unix() + p.linkByPeriodMax
   106  	switch {
   107  	case linkBy < min:
   108  		return backend.PluginError{
   109  			PluginID:  ticketvote.PluginID,
   110  			ErrorCode: uint32(ticketvote.ErrorCodeLinkByInvalid),
   111  			ErrorContext: fmt.Sprintf("linkby %v is less than min required of %v",
   112  				linkBy, min),
   113  		}
   114  	case linkBy > max:
   115  		return backend.PluginError{
   116  			PluginID:  ticketvote.PluginID,
   117  			ErrorCode: uint32(ticketvote.ErrorCodeLinkByInvalid),
   118  			ErrorContext: fmt.Sprintf("linkby %v is more than max allowed of %v",
   119  				linkBy, max),
   120  		}
   121  	}
   122  
   123  	return nil
   124  }
   125  
   126  // linkToVerify verifies that the provided link to meets all ticketvote plugin
   127  // requirements. See the ticketvote VoteMetadata structure for more details on
   128  // the link to field.
   129  func (p *ticketVotePlugin) linkToVerify(linkTo string) error {
   130  	// LinkTo must be a public record
   131  	token, err := tokenDecode(linkTo)
   132  	if err != nil {
   133  		return backend.PluginError{
   134  			PluginID:     ticketvote.PluginID,
   135  			ErrorCode:    uint32(ticketvote.ErrorCodeLinkToInvalid),
   136  			ErrorContext: err.Error(),
   137  		}
   138  	}
   139  	r, err := p.recordAbridged(token)
   140  	if err != nil {
   141  		if err == backend.ErrRecordNotFound {
   142  			return backend.PluginError{
   143  				PluginID:     ticketvote.PluginID,
   144  				ErrorCode:    uint32(ticketvote.ErrorCodeLinkToInvalid),
   145  				ErrorContext: "record not found",
   146  			}
   147  		}
   148  		return err
   149  	}
   150  	if r.RecordMetadata.Status != backend.StatusPublic {
   151  		return backend.PluginError{
   152  			PluginID:  ticketvote.PluginID,
   153  			ErrorCode: uint32(ticketvote.ErrorCodeLinkToInvalid),
   154  			ErrorContext: fmt.Sprintf("record status is invalid: got %v, want %v",
   155  				backend.Statuses[r.RecordMetadata.Status],
   156  				backend.Statuses[backend.StatusPublic]),
   157  		}
   158  	}
   159  
   160  	// LinkTo must be a runoff vote parent record, i.e. has specified
   161  	// a LinkBy deadline.
   162  	parentVM, err := voteMetadataDecode(r.Files)
   163  	if err != nil {
   164  		return err
   165  	}
   166  	if parentVM == nil || parentVM.LinkBy == 0 {
   167  		return backend.PluginError{
   168  			PluginID:     ticketvote.PluginID,
   169  			ErrorCode:    uint32(ticketvote.ErrorCodeLinkToInvalid),
   170  			ErrorContext: "record not a runoff vote parent",
   171  		}
   172  	}
   173  
   174  	// The LinkBy deadline must not be expired
   175  	if time.Now().Unix() > parentVM.LinkBy {
   176  		return backend.PluginError{
   177  			PluginID:     ticketvote.PluginID,
   178  			ErrorCode:    uint32(ticketvote.ErrorCodeLinkToInvalid),
   179  			ErrorContext: "parent record linkby deadline has expired",
   180  		}
   181  	}
   182  
   183  	// The runoff vote parent record must have been approved in a vote.
   184  	vs, err := p.summaryByToken(token)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	if vs.Status != ticketvote.VoteStatusApproved {
   189  		return backend.PluginError{
   190  			PluginID:     ticketvote.PluginID,
   191  			ErrorCode:    uint32(ticketvote.ErrorCodeLinkToInvalid),
   192  			ErrorContext: "parent record vote is not approved",
   193  		}
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  // linkToVerifyOnEdits runs LinkTo validation that is specific to record edits.
   200  func (p *ticketVotePlugin) linkToVerifyOnEdits(r backend.Record, newFiles []backend.File) error {
   201  	// The LinkTo field is not allowed to change once the record has
   202  	// become public.
   203  	if r.RecordMetadata.State != backend.StateVetted {
   204  		// Not vetted. Nothing to do.
   205  		return nil
   206  	}
   207  	var (
   208  		oldLinkTo string
   209  		newLinkTo string
   210  	)
   211  	vm, err := voteMetadataDecode(r.Files)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	// Vote metadata is optional so one may not exist
   216  	if vm != nil {
   217  		oldLinkTo = vm.LinkTo
   218  	}
   219  	vm, err = voteMetadataDecode(newFiles)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	if vm != nil {
   224  		newLinkTo = vm.LinkTo
   225  	}
   226  	if newLinkTo != oldLinkTo {
   227  		return backend.PluginError{
   228  			PluginID:  ticketvote.PluginID,
   229  			ErrorCode: uint32(ticketvote.ErrorCodeLinkToInvalid),
   230  			ErrorContext: fmt.Sprintf("linkto cannot change on vetted record: "+
   231  				"got '%v', want '%v'", newLinkTo, oldLinkTo),
   232  		}
   233  	}
   234  	return nil
   235  }
   236  
   237  // linkToVerifyOnStatusChange runs LinkTo validation that is specific to record
   238  // status changes.
   239  func (p *ticketVotePlugin) linkToVerifyOnStatusChange(status backend.StatusT, vm ticketvote.VoteMetadata) error {
   240  	if vm.LinkTo == "" {
   241  		// Link to not set. Nothing to do.
   242  		return nil
   243  	}
   244  
   245  	// Verify that the deadline to link to this record has not expired.
   246  	// We only need to do this when a record is being made public since
   247  	// the submissions list of the parent record is only updated for
   248  	// public records.
   249  	if status != backend.StatusPublic {
   250  		// Not being made public. Nothing to do.
   251  		return nil
   252  	}
   253  
   254  	// Get the parent record
   255  	token, err := tokenDecode(vm.LinkTo)
   256  	if err != nil {
   257  		return err
   258  	}
   259  	r, err := p.recordAbridged(token)
   260  	if err != nil {
   261  		return err
   262  	}
   263  
   264  	// Verify linkby has not expired
   265  	vmParent, err := voteMetadataDecode(r.Files)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	if vmParent == nil {
   270  		return fmt.Errorf("vote metadata does not exist on parent record %v",
   271  			vm.LinkTo)
   272  	}
   273  	if time.Now().Unix() > vmParent.LinkBy {
   274  		return backend.PluginError{
   275  			PluginID:     ticketvote.PluginID,
   276  			ErrorCode:    uint32(ticketvote.ErrorCodeLinkToInvalid),
   277  			ErrorContext: "parent record linkby has expired",
   278  		}
   279  	}
   280  
   281  	return nil
   282  }
   283  
   284  // voteMetadataVerify decodes the VoteMetadata from the provided files and
   285  // verifies that it meets the ticketvote plugin requirements. Vote metadata is
   286  // optional so one may not exist.
   287  func (p *ticketVotePlugin) voteMetadataVerify(files []backend.File) error {
   288  	// Decode vote metadata. The vote metadata is optional so one may
   289  	// not exist.
   290  	vm, err := voteMetadataDecode(files)
   291  	if err != nil {
   292  		return err
   293  	}
   294  	if vm == nil {
   295  		// Vote metadata not found. Nothing to do.
   296  		return nil
   297  	}
   298  
   299  	// Verify vote metadata fields are sane
   300  	switch {
   301  	case vm.LinkBy == 0 && vm.LinkTo == "":
   302  		// Vote metadata is empty. This is not allowed.
   303  		return backend.PluginError{
   304  			PluginID:     ticketvote.PluginID,
   305  			ErrorCode:    uint32(ticketvote.ErrorCodeVoteMetadataInvalid),
   306  			ErrorContext: "metadata is empty",
   307  		}
   308  
   309  	case vm.LinkBy != 0 && vm.LinkTo != "":
   310  		// LinkBy and LinkTo cannot both be set
   311  		return backend.PluginError{
   312  			PluginID:     ticketvote.PluginID,
   313  			ErrorCode:    uint32(ticketvote.ErrorCodeVoteMetadataInvalid),
   314  			ErrorContext: "cannot set both linkby and linkto",
   315  		}
   316  
   317  	case vm.LinkBy != 0:
   318  		// LinkBy has been set. Verify that is meets plugin requirements.
   319  		err := p.linkByVerify(vm.LinkBy)
   320  		if err != nil {
   321  			return err
   322  		}
   323  
   324  	case vm.LinkTo != "":
   325  		// LinkTo has been set. Verify that is meets plugin requirements.
   326  		err := p.linkToVerify(vm.LinkTo)
   327  		if err != nil {
   328  			return err
   329  		}
   330  	}
   331  
   332  	return nil
   333  }
   334  
   335  // voteMetadataVerifyOnEdits runs vote metadata validation that is specific to
   336  // record edits.
   337  func (p *ticketVotePlugin) voteMetadataVerifyOnEdits(r backend.Record, newFiles []backend.File) error {
   338  	// Verify LinkTo has not changed. This must be run even if a vote
   339  	// metadata is not present.
   340  	err := p.linkToVerifyOnEdits(r, newFiles)
   341  	if err != nil {
   342  		return err
   343  	}
   344  
   345  	// Decode vote metadata. The vote metadata is optional so one may not
   346  	// exist.
   347  	vm, err := voteMetadataDecode(newFiles)
   348  	if err != nil {
   349  		return err
   350  	}
   351  	if vm == nil {
   352  		// Vote metadata not found. Nothing to do.
   353  		return nil
   354  	}
   355  
   356  	// Verify LinkBy
   357  	err = p.linkByVerify(vm.LinkBy)
   358  	if err != nil {
   359  		return err
   360  	}
   361  
   362  	// The LinkTo does not need to be validated since we have already
   363  	// confirmed that it has not changed from the previous record
   364  	// version and it would have already been validated when the record
   365  	// was originally submitted. It should not be possible for it to be
   366  	// invalid at this point.
   367  
   368  	return nil
   369  }
   370  
   371  // voteMetadataVerifyOnStatusChange runs vote metadata validation that is
   372  // specific to record status changes.
   373  func (p *ticketVotePlugin) voteMetadataVerifyOnStatusChange(status backend.StatusT, files []backend.File) error {
   374  	// If the record is being censored or archived then this
   375  	// vote metadata validation doesn't matter.
   376  	switch status {
   377  	case backend.StatusCensored, backend.StatusArchived:
   378  		return nil
   379  	}
   380  
   381  	// Decode vote metadata. Vote metadata is optional so one may not
   382  	// exist.
   383  	vm, err := voteMetadataDecode(files)
   384  	if err != nil {
   385  		return err
   386  	}
   387  	if vm == nil {
   388  		// Vote metadata not found. Nothing to do.
   389  		return nil
   390  	}
   391  
   392  	// Verify LinkTo
   393  	err = p.linkToVerifyOnStatusChange(status, *vm)
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	// Verify LinkBy
   399  	return p.linkByVerify(vm.LinkBy)
   400  }
   401  
   402  // voteMetadataCacheOnStatusChange performs vote metadata cache updates after
   403  // a record status change.
   404  func (p *ticketVotePlugin) voteMetadataCacheOnStatusChange(token string, state backend.StateT, status backend.StatusT, files []backend.File) error {
   405  	// Decode vote metadata. Vote metadata is optional so one may not
   406  	// exist.
   407  	vm, err := voteMetadataDecode(files)
   408  	if err != nil {
   409  		return err
   410  	}
   411  	if vm == nil {
   412  		// Vote metadata doesn't exist. Nothing to do.
   413  		return nil
   414  	}
   415  	if vm.LinkTo == "" {
   416  		// LinkTo not set. Nothing to do.
   417  		return nil
   418  	}
   419  
   420  	// LinkTo has been set. Check if the status change requires the
   421  	// submissions list of the linked record to be updated.
   422  	var (
   423  		parentToken = vm.LinkTo
   424  		childToken  = token
   425  	)
   426  	switch {
   427  	case state == backend.StateUnvetted:
   428  		// We do not update the submissions cache for unvetted records.
   429  		// Do nothing.
   430  
   431  	case status == backend.StatusPublic:
   432  		// The record has been made public. Add the child
   433  		// token to parent record's submissions list.
   434  		err := p.subs.Add(parentToken, childToken)
   435  		if err != nil {
   436  			return err
   437  		}
   438  
   439  	case status == backend.StatusCensored:
   440  		// The record has been censored. Delete the
   441  		// child token from parent's submissions list.
   442  		err := p.subs.Del(parentToken, childToken)
   443  		if err != nil {
   444  			return err
   445  		}
   446  	}
   447  
   448  	return nil
   449  }
   450  
   451  // voteMetadataDecode decodes and returns the VoteMetadata from the
   452  // provided backend files. If a VoteMetadata is not found, nil is returned.
   453  func voteMetadataDecode(files []backend.File) (*ticketvote.VoteMetadata, error) {
   454  	var voteMD *ticketvote.VoteMetadata
   455  	for _, v := range files {
   456  		if v.Name != ticketvote.FileNameVoteMetadata {
   457  			continue
   458  		}
   459  		b, err := base64.StdEncoding.DecodeString(v.Payload)
   460  		if err != nil {
   461  			return nil, err
   462  		}
   463  		var m ticketvote.VoteMetadata
   464  		err = json.Unmarshal(b, &m)
   465  		if err != nil {
   466  			return nil, err
   467  		}
   468  		voteMD = &m
   469  		break
   470  	}
   471  	return voteMD, nil
   472  }