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 }