github.com/decred/politeia@v1.4.0/politeiawww/cmd/pictl/cmdrfptest.go (about) 1 // Copyright (c) 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 main 6 7 import ( 8 "fmt" 9 "time" 10 11 tkv1 "github.com/decred/politeia/politeiawww/api/ticketvote/v1" 12 pclient "github.com/decred/politeia/politeiawww/client" 13 "github.com/pkg/errors" 14 ) 15 16 // cmdRFPTest runs tests to ensure the RFP workflow works as expected. 17 type cmdRFPTest struct { 18 Args struct { 19 AdminEmail string `positional-arg-name:"adminemail"` 20 AdminPassword string `positional-arg-name:"adminpassword"` 21 } `positional-args:"true" required:"true"` 22 23 // Password is the user's dcrwallet password. 24 Password string `long:"password"` 25 26 // Quorum is the percent of total votes required for a quorum. This is a 27 // pointer so that a value of 0 can be provided. A quorum of zero allows 28 // for the vote to be approved or rejected using a single DCR ticket. 29 Quorum *uint32 `long:"quorum"` 30 31 // Passing is the percent of cast votes required for a vote options to be 32 // considered as passing. 33 Passing uint32 `long:"passing"` 34 } 35 36 // Execute executes the cmdRFPTest command. 37 // 38 // This function satisfies the go-flags Commander interface. 39 func (c *cmdRFPTest) Execute(args []string) error { 40 const ( 41 // sleepInterval is the time to wait in between requests 42 // when polling the ticketvote API for vote results or when 43 // waiting for the RFP linkby deadline to expire before 44 // starting the runoff vote. 45 sleepInterval = 15 * time.Second 46 ) 47 48 // Setup vote parameters 49 var ( 50 quorum = defaultQuorum 51 passing = defaultPassing 52 ) 53 if c.Quorum != nil { 54 quorum = *c.Quorum 55 } 56 if c.Passing != 0 { 57 passing = c.Passing 58 } 59 60 fmt.Printf("Quorum : %v%%\n", quorum) 61 fmt.Printf("Passing: %v%%\n", passing) 62 63 // We don't want the output of individual commands printed. 64 cfg.Verbose = false 65 cfg.RawJSON = false 66 cfg.Silent = true 67 68 // Verify paywall is disabled 69 policyWWW, err := client.Policy() 70 if err != nil { 71 return err 72 } 73 if policyWWW.PaywallEnabled { 74 return errors.Errorf("paywall is not disabled") 75 } 76 77 // Get ticketvote API policy to verify voteduartionmin 78 // policy. 79 // 80 // Setup client 81 opts := pclient.Opts{ 82 HTTPSCert: cfg.HTTPSCert, 83 Verbose: cfg.Verbose, 84 RawJSON: cfg.RawJSON, 85 } 86 pc, err := pclient.New(cfg.Host, opts) 87 if err != nil { 88 return err 89 } 90 91 // Get policy 92 pr, err := pc.TicketVotePolicy() 93 if err != nil { 94 return err 95 } 96 if pr.VoteDurationMin > 1 { 97 return errors.Errorf("--votedurationmin flag should be <= 1, as the " + 98 "tests include RFP & submssions voting, and the RFP deadline is 6m") 99 } 100 101 // Log start time 102 fmt.Printf("Start time: %v\n", dateAndTimeFromUnix(time.Now().Unix())) 103 104 // Verify admin login credentials 105 admin := user{ 106 Email: c.Args.AdminEmail, 107 Password: c.Args.AdminPassword, 108 } 109 fmt.Printf(" Login as admin\n") 110 err = userLogin(admin) 111 if err != nil { 112 return errors.Errorf("failed to login admin: %v", err) 113 } 114 lr, err := client.Me() 115 if err != nil { 116 return err 117 } 118 if !lr.IsAdmin { 119 return errors.Errorf("provided user is not an admin") 120 } 121 admin.Username = lr.Username 122 123 // Create a RFP and make it public 124 fmt.Printf(" Create a RFP\n") 125 // The RFP deadline is in 6 minutes from now, this should be safe as we 126 // require the votedurationmin policy to be one block. 127 linkByTime := time.Now().Add(6 * time.Minute) 128 r, err := proposalPublic(admin, admin, &proposalOpts{ 129 Random: true, 130 LinkBy: time.Until(linkByTime).String(), 131 }) 132 if err != nil { 133 return err 134 } 135 tokenRFP := r.CensorshipRecord.Token 136 fmt.Printf(" RFP created: %v\n", tokenRFP) 137 138 // Authorize RFP vote 139 fmt.Printf(" Authorize vote on RFP\n") 140 err = voteAuthorize(admin, tokenRFP) 141 if err != nil { 142 return err 143 } 144 145 // Start RFP vote 146 fmt.Printf(" Start vote on RFP\n") 147 err = voteStart(admin, tokenRFP, pr.VoteDurationMin, 148 quorum, passing, false) 149 if err != nil { 150 return err 151 } 152 153 // Cast votes on RFP 154 fmt.Printf(" Cast 'yes' votes\n") 155 156 // Prompt the user for their password if they haven't already 157 // provided it. 158 password := c.Password 159 if password == "" { 160 // Temporarily enable output to prompt user for password 161 cfg.Silent = false 162 pass, err := promptWalletPassword() 163 if err != nil { 164 return err 165 } 166 password = string(pass) 167 cfg.Silent = true 168 } 169 170 err = castBallot(tokenRFP, tkv1.VoteOptionIDApprove, password) 171 if err != nil { 172 return err 173 } 174 175 // Wait to RFP to finish voting 176 var ( 177 approvedRFP bool 178 vs tkv1.Summary 179 ) 180 for !approvedRFP { 181 // Fetch vote summary 182 var cvs cmdVoteSummaries 183 cvs.Args.Tokens = []string{tokenRFP} 184 summaries, err := voteSummaries(&cvs) 185 if err != nil { 186 return err 187 } 188 vs = summaries[tokenRFP] 189 190 if vs.Status != tkv1.VoteStatusApproved { 191 fmt.Printf(" RFP voting still going on, block %v/%v \n", 192 vs.BestBlock, vs.EndBlockHeight) 193 time.Sleep(sleepInterval) 194 } else { 195 approvedRFP = true 196 } 197 } 198 fmt.Printf(" RFP approved successfully\n") 199 fmt.Printf("%v\n", voteSummaryString(tokenRFP, vs, 4)) 200 201 // Create 1 unvetted censored RFP submission 202 fmt.Printf(" Create 1 unvetted censored RFP submission\n") 203 r, err = proposalUnvettedCensored(admin, admin, &proposalOpts{ 204 Random: true, 205 LinkTo: tokenRFP, 206 }) 207 if err != nil { 208 return err 209 } 210 tokenUnvettedCensored := r.CensorshipRecord.Token 211 212 // Create 1 vetted censored RFP submission 213 fmt.Printf(" Create 1 vetted censored RFP submission\n") 214 r, err = proposalVettedCensored(admin, admin, &proposalOpts{ 215 Random: true, 216 LinkTo: tokenRFP, 217 }) 218 if err != nil { 219 return err 220 } 221 tokenVettedCensored := r.CensorshipRecord.Token 222 223 // Create 1 vetted abandoned RFP submission 224 fmt.Printf(" Create 1 vetted abandoned RFP submission\n") 225 r, err = proposalAbandoned(admin, admin, &proposalOpts{ 226 Random: true, 227 LinkTo: tokenRFP, 228 }) 229 if err != nil { 230 return err 231 } 232 tokenAbandoned := r.CensorshipRecord.Token 233 234 // Create 3 public RFP submissions 235 fmt.Printf(" Create 3 public RFP submissions\n") 236 var tokensPublic [3]string 237 r, err = proposalPublic(admin, admin, &proposalOpts{ 238 Random: true, 239 LinkTo: tokenRFP, 240 }) 241 if err != nil { 242 return err 243 } 244 tokensPublic[0] = r.CensorshipRecord.Token 245 r, err = proposalPublic(admin, admin, &proposalOpts{ 246 Random: true, 247 LinkTo: tokenRFP, 248 }) 249 if err != nil { 250 return err 251 } 252 tokensPublic[1] = r.CensorshipRecord.Token 253 r, err = proposalPublic(admin, admin, &proposalOpts{ 254 Random: true, 255 LinkTo: tokenRFP, 256 }) 257 if err != nil { 258 return err 259 } 260 tokensPublic[2] = r.CensorshipRecord.Token 261 262 // Wait for the rfp deadline to expire 263 for linkByTime.Unix() >= time.Now().Unix() { 264 fmt.Printf(" Waiting for the RFP deadline to expire, remaining: %v\n", 265 time.Until(linkByTime).Round(time.Second)) 266 time.Sleep(sleepInterval) 267 } 268 269 // Start runoff vote for the submissions 270 fmt.Printf(" Start runoff vote for the submissions\n") 271 err = voteStart(admin, tokenRFP, pr.VoteDurationMin, quorum, passing, true) 272 if err != nil { 273 return err 274 } 275 276 // Verify that the runoff vote contains only the 3 public proposals 277 fmt.Printf(" Verify that the runoff vote contains only the 3 public " + 278 "proposals\n") 279 280 // Fetch vote summaries of public proposals 281 var cvs cmdVoteSummaries 282 tokens := tokensPublic[:] 283 cvs.Args.Tokens = tokens 284 summaries, err := voteSummaries(&cvs) 285 if err != nil { 286 return err 287 } 288 // Ensure public proposals are voting 289 for _, t := range tokens { 290 s := summaries[t] 291 if s.Status != tkv1.VoteStatusStarted { 292 return errors.Errorf("submission %v invalid vote status, "+ 293 "expected: %v, got: %v", t, tkv1.VoteStatuses[tkv1.VoteStatusStarted], 294 tkv1.VoteStatuses[s.Status]) 295 } 296 } 297 298 // Fetch vote summaries of abandoned/consored proposals 299 tokens = []string{tokenUnvettedCensored, tokenVettedCensored, tokenAbandoned} 300 cvs.Args.Tokens = tokens 301 summaries, err = voteSummaries(&cvs) 302 if err != nil { 303 return err 304 } 305 // Ensure abandoned/censored proposals are not voting 306 for _, t := range tokens { 307 s := summaries[t] 308 if s.Status != tkv1.VoteStatusIneligible { 309 return errors.Errorf("submission %v invalid vote status, "+ 310 "expected: %v, got: %v", t, 311 tkv1.VoteStatuses[tkv1.VoteStatusIneligible], 312 tkv1.VoteStatuses[s.Status]) 313 } 314 } 315 316 // Vote 'yes' on first public proposal, 'no' on the second and 317 // don't vote on third. 318 fmt.Printf(" Vote 'yes' on first public proposal, 'no' on the second and" + 319 " don't vote on third\n") 320 321 tokenFirst := tokensPublic[0] 322 err = castBallot(tokenFirst, tkv1.VoteOptionIDApprove, password) 323 if err != nil { 324 return err 325 } 326 327 tokenSecond := tokensPublic[1] 328 err = castBallot(tokenSecond, tkv1.VoteOptionIDReject, password) 329 if err != nil { 330 return err 331 } 332 333 // Wait for the runoff vote to finish 334 var approvedSubmission bool 335 for !approvedSubmission { 336 // Fetch vote summary 337 var cvs cmdVoteSummaries 338 cvs.Args.Tokens = []string{tokenFirst} 339 summaries, err := voteSummaries(&cvs) 340 if err != nil { 341 return err 342 } 343 vs = summaries[tokenFirst] 344 345 if vs.Status != tkv1.VoteStatusApproved { 346 fmt.Printf(" Runoff voting still going on, block %v/%v \n", 347 vs.BestBlock, vs.EndBlockHeight) 348 time.Sleep(sleepInterval) 349 } else { 350 approvedSubmission = true 351 } 352 } 353 fmt.Printf(" First submission was approved successfully\n") 354 fmt.Printf("%v\n", voteSummaryString(tokenFirst, vs, 4)) 355 356 // Fetch vote summary of rejected proposal 357 cvs = cmdVoteSummaries{} 358 tokenThird := tokensPublic[2] 359 tokens = []string{tokenSecond, tokenThird} 360 cvs.Args.Tokens = tokens 361 summaries, err = voteSummaries(&cvs) 362 if err != nil { 363 return err 364 } 365 for _, t := range tokens { 366 s := summaries[t] 367 if s.Status != tkv1.VoteStatusRejected { 368 return errors.Errorf("public submission %v invalid vote status, "+ 369 "expected: %v, got: %v", t, tkv1.VoteStatuses[tkv1.VoteStatusRejected], 370 tkv1.VoteStatuses[s.Status]) 371 } 372 } 373 fmt.Printf(" The other two submissions were rejected successfully\n") 374 for i, t := range tokens { 375 fmt.Printf("%v\n", voteSummaryString(t, summaries[t], 4)) 376 if i != len(tokens)-1 { 377 fmt.Printf(" -----\n") 378 } 379 } 380 381 ts := dateAndTimeFromUnix(time.Now().Unix()) 382 fmt.Printf("Done!\n") 383 fmt.Printf("Stop time: %v\n", ts) 384 385 return nil 386 } 387 388 // RFPTestHelpMsg is the printed to stdout by the help command. 389 const RFPTestHelpMsg = `rfptest "adminemail" "adminpassword" 390 391 Run tests to ensure the RFP workflow works as expected. 392 393 Arguments: 394 1. adminemail (string, required) Email for admin account. 395 2. adminpassword (string, required) Password for admin account. 396 397 Flags: 398 --password (string) dcrwallet password. The user will be prompted for their 399 password if one is not provided using this flag. 400 --quorum (uint32) Percent of total votes required to reach a quorum. A 401 quorum of 0 means that the vote can be approved or 402 rejected using a single DCR ticket. 403 (default: 0) 404 --passing (uint32) Percent of cast votes required for a vote option to be 405 considered as passing. 406 (default: 60) 407 `