github.com/decred/politeia@v1.4.0/politeiawww/cmd/pictl/proposal.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 main
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/base64"
    10  	"encoding/hex"
    11  	"fmt"
    12  	"image"
    13  	"image/color"
    14  	"image/png"
    15  	"math/rand"
    16  	"os"
    17  	"path/filepath"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/decred/politeia/politeiad/api/v1/identity"
    22  	"github.com/decred/politeia/politeiad/api/v1/mime"
    23  	pi "github.com/decred/politeia/politeiad/plugins/pi"
    24  	piplugin "github.com/decred/politeia/politeiad/plugins/pi"
    25  	piv1 "github.com/decred/politeia/politeiawww/api/pi/v1"
    26  	rcv1 "github.com/decred/politeia/politeiawww/api/records/v1"
    27  	pclient "github.com/decred/politeia/politeiawww/client"
    28  	"github.com/decred/politeia/util"
    29  )
    30  
    31  const (
    32  	monthInSeconds      int64 = 30 * 24 * 60 * 60
    33  	fourMonthsInSeconds int64 = 4 * monthInSeconds
    34  )
    35  
    36  var (
    37  	// defaultStartDate is the default proposal metadata start date in
    38  	// Unix time. It defaults to one month from now.
    39  	defaultStartDate = time.Now().Unix() + monthInSeconds
    40  
    41  	// defaultEndDate is the default proposal metadata end date in
    42  	// Unix time. It defaults to four months from now.
    43  	defaultEndDate = time.Now().Unix() + fourMonthsInSeconds
    44  
    45  	// defaultAmount is the default proposal metadata amount in cents.
    46  	// It defaults to $20k in cents.
    47  	defaultAmount uint64 = 2000000
    48  )
    49  
    50  func printProposalFiles(files []rcv1.File) error {
    51  	for _, v := range files {
    52  		b, err := base64.StdEncoding.DecodeString(v.Payload)
    53  		if err != nil {
    54  			return err
    55  		}
    56  		size := byteCountSI(int64(len(b)))
    57  		printf("  %-22v %-26v %v\n", v.Name, v.MIME, size)
    58  	}
    59  
    60  	// A vote metadata file is optional
    61  	var isRFP bool
    62  	vm, err := pclient.VoteMetadataDecode(files)
    63  	if err != nil {
    64  		return err
    65  	}
    66  	if vm != nil {
    67  		if vm.LinkBy != 0 {
    68  			isRFP = true
    69  		}
    70  	}
    71  
    72  	// Its possible for a proposal metadata to not exist if the
    73  	// proposal has been censored.
    74  	pm, err := pclient.ProposalMetadataDecode(files)
    75  	if err == nil {
    76  		printf("%v\n", piv1.FileNameProposalMetadata)
    77  		switch {
    78  		case !isRFP:
    79  			printf("  Name      : %v\n", pm.Name)
    80  			printf("  Domain    : %v\n", pm.Domain)
    81  			printf("  Amount    : %v\n", dollars(int64(pm.Amount)))
    82  			printf("  Start Date: %v\n", dateAndTimeFromUnix(pm.StartDate))
    83  			printf("  End Date  : %v\n", dateAndTimeFromUnix(pm.EndDate))
    84  		case isRFP:
    85  			printf("  Name  : %v\n", pm.Name)
    86  			printf("  Domain: %v\n", pm.Domain)
    87  		}
    88  	}
    89  
    90  	// Print vote metadata if exists.
    91  	if vm != nil {
    92  		printf("%v\n", piv1.FileNameVoteMetadata)
    93  		if vm.LinkTo != "" {
    94  			printf("  LinkTo: %v\n", vm.LinkTo)
    95  		}
    96  		if vm.LinkBy != 0 {
    97  			printf("  LinkBy: %v\n", dateAndTimeFromUnix(vm.LinkBy))
    98  		}
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  func printProposal(r rcv1.Record) error {
   105  	printf("Token    : %v\n", r.CensorshipRecord.Token)
   106  	printf("Version  : %v\n", r.Version)
   107  	printf("State    : %v\n", rcv1.RecordStates[r.State])
   108  	printf("Status   : %v\n", rcv1.RecordStatuses[r.Status])
   109  	printf("Timestamp: %v\n", dateAndTimeFromUnix(r.Timestamp))
   110  	printf("Username : %v\n", r.Username)
   111  	printf("Merkle   : %v\n", r.CensorshipRecord.Merkle)
   112  	printf("Receipt  : %v\n", r.CensorshipRecord.Signature)
   113  	printf("Metadata\n")
   114  	for _, v := range r.Metadata {
   115  		size := byteCountSI(int64(len([]byte(v.Payload))))
   116  		printf("  %-8v %-2v %v\n", v.PluginID, v.StreamID, size)
   117  	}
   118  	printf("Files\n")
   119  	return printProposalFiles(r.Files)
   120  }
   121  
   122  // printProposalSummary prints a proposal summary.
   123  func printProposalSummary(token string, s piv1.Summary) {
   124  	printf("Token : %v\n", token)
   125  	printf("Status: %v\n", s.Status)
   126  }
   127  
   128  // printBillingStatusChanges prints a proposal billing status change.
   129  func printBillingStatusChange(bsc piv1.BillingStatusChange) {
   130  	printf("  Token    : %v\n", bsc.Token)
   131  	printf("  Status   : %v\n", piv1.BillingStatuses[bsc.Status])
   132  	if bsc.Reason != "" {
   133  		printf("  Reason   : %v\n", bsc.Reason)
   134  	}
   135  	printf("  PublicKey: %v\n", bsc.PublicKey)
   136  	printf("  Signature: %v\n", bsc.Signature)
   137  	printf("  Receipt  : %v\n", bsc.Receipt)
   138  	printf("  Timestamp: %v\n", dateAndTimeFromUnix(bsc.Timestamp))
   139  }
   140  
   141  // indexFileRandom returns a proposal index file filled with random data.
   142  func indexFileRandom(sizeInBytes int) (*rcv1.File, error) {
   143  	// Create lines of text that are 80 characters long
   144  	charSet := "abcdefghijklmnopqrstuvwxyz"
   145  	var b strings.Builder
   146  	for i := 0; i < sizeInBytes; i++ {
   147  		if i != 0 && i%80 == 0 {
   148  			b.WriteString("\n")
   149  			continue
   150  		}
   151  		r := rand.Intn(len(charSet))
   152  		char := charSet[r]
   153  		b.WriteString(string(char))
   154  	}
   155  	b.WriteString("\n")
   156  	payload := []byte(b.String())
   157  
   158  	return &rcv1.File{
   159  		Name:    piv1.FileNameIndexFile,
   160  		MIME:    mime.DetectMimeType(payload),
   161  		Digest:  hex.EncodeToString(util.Digest(payload)),
   162  		Payload: base64.StdEncoding.EncodeToString(payload),
   163  	}, nil
   164  }
   165  
   166  // pngFileRandom returns a record file for a randomly generated PNG image. The
   167  // size of the image will be 0.49MB.
   168  func pngFileRandom() (*rcv1.File, error) {
   169  	b := new(bytes.Buffer)
   170  	img := image.NewRGBA(image.Rect(0, 0, 500, 250))
   171  
   172  	// Fill in the pixels with random rgb colors
   173  	r := rand.New(rand.NewSource(255))
   174  	for y := 0; y < img.Bounds().Max.Y-1; y++ {
   175  		for x := 0; x < img.Bounds().Max.X-1; x++ {
   176  			a := uint8(r.Float32() * 255)
   177  			rgb := uint8(r.Float32() * 255)
   178  			img.SetRGBA(x, y, color.RGBA{rgb, rgb, rgb, a})
   179  		}
   180  	}
   181  	err := png.Encode(b, img)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	// Create a random name
   187  	rn, err := util.Random(8)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	return &rcv1.File{
   193  		Name:    hex.EncodeToString(rn) + ".png",
   194  		MIME:    mime.DetectMimeType(b.Bytes()),
   195  		Digest:  hex.EncodeToString(util.Digest(b.Bytes())),
   196  		Payload: base64.StdEncoding.EncodeToString(b.Bytes()),
   197  	}, nil
   198  }
   199  
   200  func proposalFilesRandom(textFileSize, imageFileCountMax int) ([]rcv1.File, error) {
   201  	files := make([]rcv1.File, 0, 16)
   202  
   203  	// Generate random text for the index file
   204  	f, err := indexFileRandom(textFileSize)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	files = append(files, *f)
   209  
   210  	// Generate a random number of attachment files
   211  	if imageFileCountMax > 0 {
   212  		attachmentCount := rand.Intn(imageFileCountMax)
   213  		for i := 0; i <= attachmentCount; i++ {
   214  			f, err := pngFileRandom()
   215  			if err != nil {
   216  				return nil, err
   217  			}
   218  			files = append(files, *f)
   219  		}
   220  	}
   221  
   222  	return files, nil
   223  }
   224  
   225  func proposalFilesFromDisk(indexFile string, attachments []string) ([]rcv1.File, error) {
   226  	files := make([]rcv1.File, 0, len(attachments)+1)
   227  
   228  	// Setup index file
   229  	fp := util.CleanAndExpandPath(indexFile)
   230  	var err error
   231  	payload, err := os.ReadFile(fp)
   232  	if err != nil {
   233  		return nil, fmt.Errorf("ReadFile %v: %v", fp, err)
   234  	}
   235  	files = append(files, rcv1.File{
   236  		Name:    piplugin.FileNameIndexFile,
   237  		MIME:    mime.DetectMimeType(payload),
   238  		Digest:  hex.EncodeToString(util.Digest(payload)),
   239  		Payload: base64.StdEncoding.EncodeToString(payload),
   240  	})
   241  
   242  	// Setup attachment files
   243  	for _, fn := range attachments {
   244  		fp := util.CleanAndExpandPath(fn)
   245  		payload, err := os.ReadFile(fp)
   246  		if err != nil {
   247  			return nil, fmt.Errorf("ReadFile %v: %v", fp, err)
   248  		}
   249  
   250  		files = append(files, rcv1.File{
   251  			Name:    filepath.Base(fn),
   252  			MIME:    mime.DetectMimeType(payload),
   253  			Digest:  hex.EncodeToString(util.Digest(payload)),
   254  			Payload: base64.StdEncoding.EncodeToString(payload),
   255  		})
   256  	}
   257  
   258  	return files, nil
   259  }
   260  
   261  // signedMerkleRoot returns the signed merkle root of the provided files. The
   262  // signature is created using the provided identity.
   263  func signedMerkleRoot(files []rcv1.File, fid *identity.FullIdentity) (string, error) {
   264  	if len(files) == 0 {
   265  		return "", fmt.Errorf("no proposal files found")
   266  	}
   267  	digests := make([]string, 0, len(files))
   268  	for _, v := range files {
   269  		digests = append(digests, v.Digest)
   270  	}
   271  	m, err := util.MerkleRoot(digests)
   272  	if err != nil {
   273  		return "", err
   274  	}
   275  	mr := hex.EncodeToString(m[:])
   276  	sig := fid.SignMessage([]byte(mr))
   277  	return hex.EncodeToString(sig[:]), nil
   278  }
   279  
   280  // parseProposalStatus parses a pi PropStatusT from the provided string. A
   281  // PropStatusInvalid is returned if the provided string does not correspond to
   282  // a valid proposal status.
   283  func parseProposalStatus(status string) pi.PropStatusT {
   284  	switch pi.PropStatusT(status) {
   285  	// The following are valid proposal statuses
   286  	case pi.PropStatusUnvetted,
   287  		pi.PropStatusUnvettedAbandoned,
   288  		pi.PropStatusUnvettedCensored,
   289  		pi.PropStatusUnderReview,
   290  		pi.PropStatusAbandoned,
   291  		pi.PropStatusCensored,
   292  		pi.PropStatusVoteAuthorized,
   293  		pi.PropStatusVoteStarted,
   294  		pi.PropStatusApproved,
   295  		pi.PropStatusRejected,
   296  		pi.PropStatusActive,
   297  		pi.PropStatusCompleted,
   298  		pi.PropStatusClosed:
   299  	default:
   300  		return pi.PropStatusInvalid
   301  	}
   302  	return pi.PropStatusT(status)
   303  }