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 }