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 }