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  `