github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/ticketvote/activevotes.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/hex"
     9  	"sync"
    10  
    11  	"github.com/decred/politeia/politeiad/plugins/ticketvote"
    12  )
    13  
    14  // activeVotes provides a memory cache for data that is required to validate
    15  // vote ballots in a time efficient manner. An active vote is added to the
    16  // cache when a vote is started and is removed from the cache lazily when a
    17  // vote summary is created for the finished vote.
    18  //
    19  // Record locking is handled by the backend, not by individual plugins. This
    20  // makes the plugin implementations simpler and easier to reason about, but it
    21  // can also lead to performance bottlenecks for expensive plugin write
    22  // commands. The cast ballot command is one such command due to a combination
    23  // requiring external dcrdata calls to verify the largest commitment addresses
    24  // for each ticket and the fact that its possible for hundreds of ballots to be
    25  // cast concurrently. We cache the active vote data in order to alleviate this
    26  // bottleneck.
    27  type activeVotes struct {
    28  	sync.RWMutex
    29  	activeVotes map[string]activeVote // [token]activeVote
    30  }
    31  
    32  // activeVote caches the data required to validate vote ballots for a record
    33  // with an active voting period.
    34  //
    35  // A active vote with 41k tickets will cache a maximum of 10.5 MB of data.
    36  // This includes a 3 MB vote details, 4.5 MB commitment addresses map, and a
    37  // potential 3 MB cast votes map if all 41k votes are cast.
    38  type activeVote struct {
    39  	Details   *ticketvote.VoteDetails
    40  	CastVotes map[string]string // [ticket]voteBit
    41  
    42  	// Addrs contains the largest commitment address for each eligble
    43  	// ticket. The vote must be signed with the key from this address.
    44  	//
    45  	// This map is populated by an async job that is kicked off when a
    46  	// a vote is started. It takes ~1.5 minutes to fully populate this
    47  	// cache when the ticket pool is 41k tickets and when using an off
    48  	// premise dcrdata instance with minimal latency. Any functions
    49  	// that rely of this cache should fallback to fetching the
    50  	// commitment addresses manually in the event the cache has not
    51  	// been fully populated yet or has experienced unforeseen errors
    52  	// during creation (ex. network errors). If the initial job fails
    53  	// to complete it will not be retried.
    54  	Addrs map[string]string // [ticket]address
    55  }
    56  
    57  // VoteDetails returns the vote details from the active votes cache for the
    58  // provided token. If the token does not correspond to an active vote then nil
    59  // is returned.
    60  func (a *activeVotes) VoteDetails(token []byte) *ticketvote.VoteDetails {
    61  	t := hex.EncodeToString(token)
    62  
    63  	a.RLock()
    64  	defer a.RUnlock()
    65  
    66  	av, ok := a.activeVotes[t]
    67  	if !ok {
    68  		return nil
    69  	}
    70  
    71  	// Return a copy of the vote details
    72  	eligible := make([]string, len(av.Details.EligibleTickets))
    73  	copy(eligible, av.Details.EligibleTickets)
    74  
    75  	options := make([]ticketvote.VoteOption, len(av.Details.Params.Options))
    76  	copy(options, av.Details.Params.Options)
    77  
    78  	return &ticketvote.VoteDetails{
    79  		Params: ticketvote.VoteParams{
    80  			Token:            av.Details.Params.Token,
    81  			Version:          av.Details.Params.Version,
    82  			Type:             av.Details.Params.Type,
    83  			Mask:             av.Details.Params.Mask,
    84  			Duration:         av.Details.Params.Duration,
    85  			QuorumPercentage: av.Details.Params.QuorumPercentage,
    86  			PassPercentage:   av.Details.Params.PassPercentage,
    87  			Options:          options,
    88  			Parent:           av.Details.Params.Parent,
    89  		},
    90  		PublicKey:        av.Details.PublicKey,
    91  		Signature:        av.Details.Signature,
    92  		StartBlockHeight: av.Details.StartBlockHeight,
    93  		StartBlockHash:   av.Details.StartBlockHash,
    94  		EndBlockHeight:   av.Details.EndBlockHeight,
    95  		EligibleTickets:  eligible,
    96  	}
    97  }
    98  
    99  // EligibleTickets returns the eligible tickets from the active votes cache for
   100  // the provided token. If the token does not correspond to an active vote then
   101  // nil is returned.
   102  func (a *activeVotes) EligibleTickets(token []byte) map[string]struct{} {
   103  	t := hex.EncodeToString(token)
   104  
   105  	a.RLock()
   106  	defer a.RUnlock()
   107  
   108  	av, ok := a.activeVotes[t]
   109  	if !ok {
   110  		return nil
   111  	}
   112  
   113  	// Return a map of the eligible tickets
   114  	eligible := make(map[string]struct{}, len(av.Details.EligibleTickets))
   115  	for _, v := range av.Details.EligibleTickets {
   116  		eligible[v] = struct{}{}
   117  	}
   118  
   119  	return eligible
   120  }
   121  
   122  // VoteIsDuplicate returns whether the vote has already been cast. The first
   123  // bool returned represents whether the record vote exists in the active votes
   124  // cache. The second bool returned represetns whether the ticket is a duplicate
   125  // vote.
   126  func (a *activeVotes) VoteIsDuplicate(token, ticket string) (bool, bool) {
   127  	a.RLock()
   128  	defer a.RUnlock()
   129  
   130  	av, ok := a.activeVotes[token]
   131  	if !ok {
   132  		// Vote does not exist. Its possible that the vote
   133  		// ended while a ballot was being validated.
   134  		return false, false
   135  	}
   136  
   137  	_, isDup := av.CastVotes[ticket]
   138  	return true, isDup
   139  }
   140  
   141  // CommitmentAddrs returns the largest comittment address for each of the
   142  // provided tickets. The returned map is a map[ticket]commitmentAddr. Nil is
   143  // returned if the provided token does not correspond to a record in the active
   144  // votes cache.
   145  func (a *activeVotes) CommitmentAddrs(token []byte, tickets []string) map[string]commitmentAddr {
   146  	if len(tickets) == 0 {
   147  		return map[string]commitmentAddr{}
   148  	}
   149  
   150  	t := hex.EncodeToString(token)
   151  	ca := make(map[string]commitmentAddr, len(tickets))
   152  
   153  	a.RLock()
   154  	defer a.RUnlock()
   155  
   156  	av, ok := a.activeVotes[t]
   157  	if !ok {
   158  		return nil
   159  	}
   160  
   161  	for _, v := range tickets {
   162  		addr, ok := av.Addrs[v]
   163  		if ok {
   164  			ca[v] = commitmentAddr{
   165  				addr: addr,
   166  			}
   167  		}
   168  	}
   169  
   170  	return ca
   171  }
   172  
   173  // Tally returns the tally of the cast votes for each vote option in an active
   174  // vote. The returned map is a map[votebit]tally. An empty map is returned if
   175  // the requested token is not in the active votes cache.
   176  func (a *activeVotes) Tally(token string) map[string]uint32 {
   177  	tally := make(map[string]uint32, 16)
   178  
   179  	a.RLock()
   180  	defer a.RUnlock()
   181  
   182  	av, ok := a.activeVotes[token]
   183  	if !ok {
   184  		return tally
   185  	}
   186  	for _, votebit := range av.CastVotes {
   187  		tally[votebit]++
   188  	}
   189  	return tally
   190  }
   191  
   192  // AddCastVote adds a cast ticket vote to the active votes cache.
   193  func (a *activeVotes) AddCastVote(token, ticket, votebit string) {
   194  	a.Lock()
   195  	defer a.Unlock()
   196  
   197  	av, ok := a.activeVotes[token]
   198  	if !ok {
   199  		// Vote does not exist. Its possible that the vote ended after
   200  		// the cast votes passed validation but before this cache was
   201  		// able to be populated. Log a warning and exit gracefully.
   202  		log.Warnf("AddCastVote: vote not found %v", token)
   203  		return
   204  	}
   205  
   206  	av.CastVotes[ticket] = votebit
   207  }
   208  
   209  // AddCommitmentAddrs adds commitment addresses to the cache for a record.
   210  func (a *activeVotes) AddCommitmentAddrs(token string, addrs map[string]commitmentAddr) {
   211  	a.Lock()
   212  	defer a.Unlock()
   213  
   214  	av, ok := a.activeVotes[token]
   215  	if !ok {
   216  		// Vote does not exist. Its possible for the vote to end while
   217  		// in the middle of populating the commitment addresses cache.
   218  		// This is ok. Exit gracefully.
   219  		return
   220  	}
   221  
   222  	for ticket, v := range addrs {
   223  		if v.err != nil {
   224  			log.Errorf("Commitment address error %v %v %v",
   225  				token, ticket, v.err)
   226  			continue
   227  		}
   228  		av.Addrs[ticket] = v.addr
   229  	}
   230  }
   231  
   232  // Del deletes an active vote from the active votes cache.
   233  func (a *activeVotes) Del(token string) {
   234  	a.Lock()
   235  	delete(a.activeVotes, token)
   236  	a.Unlock()
   237  
   238  	log.Debugf("Active votes del %v", token)
   239  }
   240  
   241  // Add adds a active vote to the active votes cache.
   242  //
   243  // This function should NOT be called directly. The ticketvote method
   244  // activeVotesAdd(), which also kicks of an async job to fetch the commitment
   245  // addresses for this active votes entry, should be used instead.
   246  func (a *activeVotes) Add(vd ticketvote.VoteDetails) {
   247  	token := vd.Params.Token
   248  
   249  	a.Lock()
   250  	a.activeVotes[token] = activeVote{
   251  		Details:   &vd,
   252  		CastVotes: make(map[string]string, 40960), // Ticket pool size
   253  		Addrs:     make(map[string]string, 40960), // Ticket pool size
   254  	}
   255  	a.Unlock()
   256  
   257  	log.Debugf("Active votes add %v", token)
   258  }
   259  
   260  // newActiveVotes returns a new activeVotes.
   261  func newActiveVotes() *activeVotes {
   262  	return &activeVotes{
   263  		activeVotes: make(map[string]activeVote, 256),
   264  	}
   265  }
   266  
   267  // activeVotePopulateAddrs fetches the largest commitment address for each
   268  // ticket in a vote from dcrdata and caches the results.
   269  func (p *ticketVotePlugin) activeVotePopulateAddrs(vd ticketvote.VoteDetails) {
   270  	// Get largest commitment address for each eligible ticket. A
   271  	// TrimmedTxs response for 500 tickets is ~1MB. It takes ~1.5
   272  	// minutes to get the largest commitment address for 41k eligible
   273  	// tickets from an off premise dcrdata instance with minimal
   274  	// latency.
   275  	var (
   276  		token    = vd.Params.Token
   277  		pageSize = 500
   278  		startIdx int
   279  		done     bool
   280  	)
   281  	for !done {
   282  		endIdx := startIdx + pageSize
   283  		if endIdx > len(vd.EligibleTickets) {
   284  			endIdx = len(vd.EligibleTickets)
   285  			done = true
   286  		}
   287  
   288  		log.Debugf("Get %v commitment addrs %v/%v",
   289  			token, endIdx, len(vd.EligibleTickets))
   290  
   291  		tickets := vd.EligibleTickets[startIdx:endIdx]
   292  		addrs, err := p.largestCommitmentAddrs(tickets)
   293  		if err != nil {
   294  			log.Errorf("Populate commitment addresses for %v at %v: %v",
   295  				token, startIdx, err)
   296  			continue
   297  		}
   298  
   299  		// Update cached active vote
   300  		p.activeVotes.AddCommitmentAddrs(token, addrs)
   301  
   302  		startIdx += pageSize
   303  	}
   304  }
   305  
   306  // activeVotesAdd creates a active votes cache entry for the provided vote
   307  // details and kicks off an async job that fetches and caches the largest
   308  // commitment address for each eligible ticket.
   309  func (p *ticketVotePlugin) activeVotesAdd(vd ticketvote.VoteDetails) {
   310  	// Add the vote to the active votes cache
   311  	p.activeVotes.Add(vd)
   312  
   313  	// Fetch the commitment addresses asynchronously
   314  	go p.activeVotePopulateAddrs(vd)
   315  }