github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/ticketvote/ticketvote.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/json"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  
    14  	"github.com/decred/dcrd/chaincfg/v3"
    15  	"github.com/decred/politeia/politeiad/api/v1/identity"
    16  	backend "github.com/decred/politeia/politeiad/backendv2"
    17  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins"
    18  	"github.com/decred/politeia/politeiad/plugins/dcrdata"
    19  	"github.com/decred/politeia/politeiad/plugins/ticketvote"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  var (
    24  	_ plugins.PluginClient = (*ticketVotePlugin)(nil)
    25  )
    26  
    27  // ticketVotePlugin is the tstore backend implementation of the ticketvote
    28  // plugin. The ticketvote plugin extends a record with dcr ticket voting
    29  // functionality.
    30  //
    31  // ticketVotePlugin satisfies the plugins PluginClient interface.
    32  type ticketVotePlugin struct {
    33  	backend         backend.Backend
    34  	tstore          plugins.TstoreClient
    35  	activeNetParams *chaincfg.Params
    36  
    37  	// dataDir is the ticket vote plugin data directory. The only data
    38  	// that is stored here is cached data that can be re-created at any
    39  	// time by walking the trillian trees. Ex, the vote summary once a
    40  	// record vote has ended.
    41  	dataDir string
    42  
    43  	// identity contains the full identity that the plugin uses to
    44  	// create receipts, i.e. signatures of user provided data that
    45  	// prove the backend received and processed a plugin command.
    46  	identity *identity.FullIdentity
    47  
    48  	// activeVotes is a memeory cache that contains data required to
    49  	// validate vote ballots in a time efficient manner.
    50  	activeVotes *activeVotes
    51  
    52  	// inv provides an API for managing the cached vote inventory. The
    53  	// data is cached in the tstore provided plugin cache.
    54  	inv *invClient
    55  
    56  	// summaries provides an API for interacting with the vote summaries
    57  	// cache. The data is saved to the tstore provided plugin cache.
    58  	summaries *summariesClient
    59  
    60  	// subs provides an API for interacting with the runoff vote submissions
    61  	// cache. The data is saved to the tstore provided plugin cache.
    62  	subs *subsClient
    63  
    64  	// Plugin settings
    65  	linkByPeriodMin    int64  // In seconds
    66  	linkByPeriodMax    int64  // In seconds
    67  	voteDurationMin    uint32 // In blocks
    68  	voteDurationMax    uint32 // In blocks
    69  	summariesPageSize  uint32
    70  	inventoryPageSize  uint32
    71  	timestampsPageSize uint32
    72  }
    73  
    74  // Setup performs any plugin setup that is required.
    75  //
    76  // This function satisfies the plugins PluginClient interface.
    77  func (p *ticketVotePlugin) Setup() error {
    78  	log.Tracef("ticketvote Setup")
    79  
    80  	// Verify plugin dependencies
    81  	var dcrdataFound bool
    82  	for _, v := range p.backend.PluginInventory() {
    83  		if v.ID == dcrdata.PluginID {
    84  			dcrdataFound = true
    85  		}
    86  	}
    87  	if !dcrdataFound {
    88  		return errors.Errorf("%v plugin dependency not registered",
    89  			dcrdata.PluginID)
    90  	}
    91  
    92  	// Build the active votes cache
    93  	log.Infof("Building active votes cache")
    94  
    95  	var (
    96  		// started is populated with the tokens of all records
    97  		// that have a vote status of VoteStatusStarted.
    98  		started = make([]string, 0, 256)
    99  
   100  		page uint32 = 1
   101  	)
   102  	bestBlock, err := p.bestBlock()
   103  	if err != nil {
   104  		return err
   105  	}
   106  	for {
   107  		entries, err := p.inv.GetPageForStatus(bestBlock,
   108  			ticketvote.VoteStatusStarted, page)
   109  		if err != nil {
   110  			return err
   111  		}
   112  		if len(entries) == 0 {
   113  			// We've reached the end of the inventory
   114  			// for the VoteStatusStarted entries.
   115  			break
   116  		}
   117  		started = append(started, entryTokens(entries)...)
   118  		page++
   119  	}
   120  	// Retrieve the data required to build the active
   121  	// votes cache for the records with ongoing votes.
   122  	for _, v := range started {
   123  		// Get the vote details
   124  		token, err := tokenDecode(v)
   125  		if err != nil {
   126  			return err
   127  		}
   128  
   129  		reply, err := p.backend.PluginRead(token, ticketvote.PluginID,
   130  			ticketvote.CmdDetails, "")
   131  		if err != nil {
   132  			return errors.Errorf("PluginRead %x %v %v: %v", token,
   133  				ticketvote.PluginID, ticketvote.CmdDetails, err)
   134  		}
   135  		var dr ticketvote.DetailsReply
   136  		err = json.Unmarshal([]byte(reply), &dr)
   137  		if err != nil {
   138  			return err
   139  		}
   140  		if dr.Vote == nil {
   141  			// Sanity check
   142  			return errors.Errorf("vote details not found "+
   143  				"for record in started inventory %x", token)
   144  		}
   145  
   146  		// Add the record to the active votes cache
   147  		p.activeVotesAdd(*dr.Vote)
   148  
   149  		// Get the cast votes
   150  		reply, err = p.backend.PluginRead(token, ticketvote.PluginID,
   151  			ticketvote.CmdResults, "")
   152  		if err != nil {
   153  			return errors.Errorf("PluginRead %x %v %v: %v", token,
   154  				ticketvote.PluginID, ticketvote.CmdResults, err)
   155  		}
   156  		var rr ticketvote.ResultsReply
   157  		err = json.Unmarshal([]byte(reply), &rr)
   158  		if err != nil {
   159  			return err
   160  		}
   161  
   162  		// Add the cast votes to the cached active vote entry
   163  		for _, v := range rr.Votes {
   164  			p.activeVotes.AddCastVote(v.Token, v.Ticket, v.VoteBit)
   165  		}
   166  	}
   167  
   168  	return nil
   169  }
   170  
   171  // Cmd executes a plugin command.
   172  //
   173  // This function satisfies the plugins PluginClient interface.
   174  func (p *ticketVotePlugin) Cmd(token []byte, cmd, payload string) (string, error) {
   175  	log.Tracef("ticketvote Cmd: %x %v %v", token, cmd, payload)
   176  
   177  	switch cmd {
   178  	case ticketvote.CmdAuthorize:
   179  		return p.cmdAuthorize(token, payload)
   180  	case ticketvote.CmdStart:
   181  		return p.cmdStart(token, payload)
   182  	case ticketvote.CmdCastBallot:
   183  		return p.cmdCastBallot(token, payload)
   184  	case ticketvote.CmdDetails:
   185  		return p.cmdDetails(token)
   186  	case ticketvote.CmdResults:
   187  		return p.cmdResults(token)
   188  	case ticketvote.CmdSummary:
   189  		return p.cmdSummary(token)
   190  	case ticketvote.CmdSubmissions:
   191  		return p.cmdSubmissions(token)
   192  	case ticketvote.CmdInventory:
   193  		return p.cmdInventory(payload)
   194  	case ticketvote.CmdTimestamps:
   195  		return p.cmdTimestamps(token, payload)
   196  
   197  		// Internal plugin commands
   198  	case cmdStartRunoffSubmission:
   199  		return p.cmdStartRunoffSubmission(token, payload)
   200  	case cmdRunoffDetails:
   201  		return p.cmdRunoffDetails(token)
   202  	}
   203  
   204  	return "", backend.ErrPluginCmdInvalid
   205  }
   206  
   207  // Hook executes a plugin hook.
   208  //
   209  // This function satisfies the plugins PluginClient interface.
   210  func (p *ticketVotePlugin) Hook(h plugins.HookT, payload string) error {
   211  	log.Tracef("ticketvote Hook: %v", plugins.Hooks[h])
   212  
   213  	switch h {
   214  	case plugins.HookTypeNewRecordPre:
   215  		return p.hookNewRecordPre(payload)
   216  	case plugins.HookTypeEditRecordPre:
   217  		return p.hookEditRecordPre(payload)
   218  	case plugins.HookTypeSetRecordStatusPre:
   219  		return p.hookSetRecordStatusPre(payload)
   220  	case plugins.HookTypeSetRecordStatusPost:
   221  		return p.hookSetRecordStatusPost(payload)
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  // Fsck performs a plugin filesystem check. The plugin is provided with the
   228  // tokens for all records in the backend.
   229  //
   230  // This function satisfies the plugins PluginClient interface.
   231  func (p *ticketVotePlugin) Fsck(tokens [][]byte) error {
   232  	log.Tracef("ticketvote Fsck")
   233  
   234  	return p.fsck(tokens)
   235  }
   236  
   237  // Settings returns the plugin's settings.
   238  //
   239  // This function satisfies the plugins PluginClient interface.
   240  func (p *ticketVotePlugin) Settings() []backend.PluginSetting {
   241  	log.Tracef("ticketvote Settings")
   242  
   243  	return []backend.PluginSetting{
   244  		{
   245  			Key:   ticketvote.SettingKeyLinkByPeriodMin,
   246  			Value: strconv.FormatInt(p.linkByPeriodMin, 10),
   247  		},
   248  		{
   249  			Key:   ticketvote.SettingKeyLinkByPeriodMax,
   250  			Value: strconv.FormatInt(p.linkByPeriodMax, 10),
   251  		},
   252  		{
   253  			Key:   ticketvote.SettingKeyVoteDurationMin,
   254  			Value: strconv.FormatUint(uint64(p.voteDurationMin), 10),
   255  		},
   256  		{
   257  			Key:   ticketvote.SettingKeyVoteDurationMax,
   258  			Value: strconv.FormatUint(uint64(p.voteDurationMax), 10),
   259  		},
   260  		{
   261  			Key:   ticketvote.SettingKeySummariesPageSize,
   262  			Value: strconv.FormatUint(uint64(p.summariesPageSize), 10),
   263  		},
   264  		{
   265  			Key:   ticketvote.SettingKeyInventoryPageSize,
   266  			Value: strconv.FormatUint(uint64(p.inventoryPageSize), 10),
   267  		},
   268  		{
   269  			Key:   ticketvote.SettingKeyTimestampsPageSize,
   270  			Value: strconv.FormatUint(uint64(p.timestampsPageSize), 10),
   271  		},
   272  	}
   273  }
   274  
   275  func New(backend backend.Backend, tstore plugins.TstoreClient, settings []backend.PluginSetting, dataDir string, id *identity.FullIdentity, activeNetParams *chaincfg.Params) (*ticketVotePlugin, error) {
   276  	// Plugin settings
   277  	var (
   278  		linkByPeriodMin    int64
   279  		linkByPeriodMax    int64
   280  		voteDurationMin    uint32
   281  		voteDurationMax    uint32
   282  		summariesPageSize  = ticketvote.SettingSummariesPageSize
   283  		inventoryPageSize  = ticketvote.SettingInventoryPageSize
   284  		timestampsPageSize = ticketvote.SettingTimestampsPageSize
   285  	)
   286  
   287  	// Set plugin settings to defaults. These will be overwritten if
   288  	// the setting was specified by the user.
   289  	switch activeNetParams.Name {
   290  	case chaincfg.MainNetParams().Name:
   291  		linkByPeriodMin = ticketvote.SettingMainNetLinkByPeriodMin
   292  		linkByPeriodMax = ticketvote.SettingMainNetLinkByPeriodMax
   293  		voteDurationMin = ticketvote.SettingMainNetVoteDurationMin
   294  		voteDurationMax = ticketvote.SettingMainNetVoteDurationMax
   295  	case chaincfg.TestNet3Params().Name:
   296  		linkByPeriodMin = ticketvote.SettingTestNetLinkByPeriodMin
   297  		linkByPeriodMax = ticketvote.SettingTestNetLinkByPeriodMax
   298  		voteDurationMin = ticketvote.SettingTestNetVoteDurationMin
   299  		voteDurationMax = ticketvote.SettingTestNetVoteDurationMax
   300  	case chaincfg.SimNetParams().Name:
   301  		// Use testnet defaults for simnet
   302  		linkByPeriodMin = ticketvote.SettingTestNetLinkByPeriodMin
   303  		linkByPeriodMax = ticketvote.SettingTestNetLinkByPeriodMax
   304  		voteDurationMin = ticketvote.SettingTestNetVoteDurationMin
   305  		voteDurationMax = ticketvote.SettingTestNetVoteDurationMax
   306  	default:
   307  		return nil, fmt.Errorf("unknown active net: %v", activeNetParams.Name)
   308  	}
   309  
   310  	// Override defaults with any passed in settings
   311  	for _, v := range settings {
   312  		switch v.Key {
   313  		case ticketvote.SettingKeyLinkByPeriodMin:
   314  			i, err := strconv.ParseInt(v.Value, 10, 64)
   315  			if err != nil {
   316  				return nil, fmt.Errorf("plugin setting '%v': ParseInt(%v): %v",
   317  					v.Key, v.Value, err)
   318  			}
   319  			linkByPeriodMin = i
   320  			log.Infof("Plugin setting updated: ticketvote %v %v",
   321  				ticketvote.SettingKeyLinkByPeriodMin, linkByPeriodMin)
   322  
   323  		case ticketvote.SettingKeyLinkByPeriodMax:
   324  			i, err := strconv.ParseInt(v.Value, 10, 64)
   325  			if err != nil {
   326  				return nil, fmt.Errorf("plugin setting '%v': ParseInt(%v): %v",
   327  					v.Key, v.Value, err)
   328  			}
   329  			linkByPeriodMax = i
   330  			log.Infof("Plugin setting updated: ticketvote %v %v",
   331  				ticketvote.SettingKeyLinkByPeriodMax, linkByPeriodMax)
   332  
   333  		case ticketvote.SettingKeyVoteDurationMin:
   334  			u, err := strconv.ParseUint(v.Value, 10, 64)
   335  			if err != nil {
   336  				return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v",
   337  					v.Key, v.Value, err)
   338  			}
   339  			voteDurationMin = uint32(u)
   340  			log.Infof("Plugin setting updated: ticketvote %v %v",
   341  				ticketvote.SettingKeyVoteDurationMin, voteDurationMin)
   342  
   343  		case ticketvote.SettingKeyVoteDurationMax:
   344  			u, err := strconv.ParseUint(v.Value, 10, 64)
   345  			if err != nil {
   346  				return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v",
   347  					v.Key, v.Value, err)
   348  			}
   349  			voteDurationMax = uint32(u)
   350  			log.Infof("Plugin setting updated: ticketvote %v %v",
   351  				ticketvote.SettingKeyVoteDurationMax, voteDurationMax)
   352  
   353  		case ticketvote.SettingKeySummariesPageSize:
   354  			u, err := strconv.ParseUint(v.Value, 10, 64)
   355  			if err != nil {
   356  				return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v",
   357  					v.Key, v.Value, err)
   358  			}
   359  			summariesPageSize = uint32(u)
   360  			log.Infof("Plugin setting updated: ticketvote %v %v",
   361  				ticketvote.SettingKeySummariesPageSize, summariesPageSize)
   362  
   363  		case ticketvote.SettingKeyInventoryPageSize:
   364  			u, err := strconv.ParseUint(v.Value, 10, 64)
   365  			if err != nil {
   366  				return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v",
   367  					v.Key, v.Value, err)
   368  			}
   369  			inventoryPageSize = uint32(u)
   370  			log.Infof("Plugin setting updated: ticketvote %v %v",
   371  				ticketvote.SettingKeyInventoryPageSize, inventoryPageSize)
   372  
   373  		case ticketvote.SettingKeyTimestampsPageSize:
   374  			u, err := strconv.ParseUint(v.Value, 10, 64)
   375  			if err != nil {
   376  				return nil, fmt.Errorf("plugin setting '%v': ParseUint(%v): %v",
   377  					v.Key, v.Value, err)
   378  			}
   379  			timestampsPageSize = uint32(u)
   380  			log.Infof("Plugin setting updated: ticketvote %v %v",
   381  				ticketvote.SettingKeyTimestampsPageSize, timestampsPageSize)
   382  
   383  		default:
   384  			return nil, fmt.Errorf("invalid plugin setting '%v'", v.Key)
   385  		}
   386  	}
   387  
   388  	// Create the plugin data directory
   389  	dataDir = filepath.Join(dataDir, ticketvote.PluginID)
   390  	err := os.MkdirAll(dataDir, 0700)
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  
   395  	return &ticketVotePlugin{
   396  		activeNetParams:    activeNetParams,
   397  		backend:            backend,
   398  		tstore:             tstore,
   399  		dataDir:            dataDir,
   400  		identity:           id,
   401  		activeVotes:        newActiveVotes(),
   402  		inv:                newInvClient(tstore, backend, inventoryPageSize),
   403  		summaries:          newSummariesClient(tstore),
   404  		subs:               newSubsClient(tstore),
   405  		linkByPeriodMin:    linkByPeriodMin,
   406  		linkByPeriodMax:    linkByPeriodMax,
   407  		voteDurationMin:    voteDurationMin,
   408  		voteDurationMax:    voteDurationMax,
   409  		summariesPageSize:  summariesPageSize,
   410  		inventoryPageSize:  inventoryPageSize,
   411  		timestampsPageSize: timestampsPageSize,
   412  	}, nil
   413  }